loukai-app 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +558 -0
- package/bin/loukai.js +32 -0
- package/package.json +243 -0
- package/src/main/appState.js +250 -0
- package/src/main/audioEngine.js +478 -0
- package/src/main/creator/conversionService.js +503 -0
- package/src/main/creator/downloadManager.js +1128 -0
- package/src/main/creator/ffmpegService.js +487 -0
- package/src/main/creator/installLogger.js +51 -0
- package/src/main/creator/keyDetection.js +212 -0
- package/src/main/creator/llmService.js +370 -0
- package/src/main/creator/lrclibService.js +340 -0
- package/src/main/creator/python/crepe_runner.py +189 -0
- package/src/main/creator/python/demucs_runner.py +158 -0
- package/src/main/creator/python/whisper_runner.py +172 -0
- package/src/main/creator/pythonRunner.js +268 -0
- package/src/main/creator/stemBuilder.js +491 -0
- package/src/main/creator/systemChecker.js +474 -0
- package/src/main/handlers/appHandlers.js +45 -0
- package/src/main/handlers/audioHandlers.js +33 -0
- package/src/main/handlers/autotuneHandlers.js +28 -0
- package/src/main/handlers/canvasHandlers.js +84 -0
- package/src/main/handlers/creatorHandlers.js +159 -0
- package/src/main/handlers/editorHandlers.js +98 -0
- package/src/main/handlers/effectsHandlers.js +100 -0
- package/src/main/handlers/fileHandlers.js +45 -0
- package/src/main/handlers/index.js +78 -0
- package/src/main/handlers/libraryHandlers.js +96 -0
- package/src/main/handlers/mixerHandlers.js +64 -0
- package/src/main/handlers/playerHandlers.js +39 -0
- package/src/main/handlers/preferencesHandlers.js +46 -0
- package/src/main/handlers/queueHandlers.js +81 -0
- package/src/main/handlers/rendererHandlers.js +63 -0
- package/src/main/handlers/settingsHandlers.js +42 -0
- package/src/main/handlers/webServerHandlers.js +105 -0
- package/src/main/main.js +2351 -0
- package/src/main/preload.js +252 -0
- package/src/main/settingsManager.js +139 -0
- package/src/main/statePersistence.js +193 -0
- package/src/main/utils/pathValidator.js +112 -0
- package/src/main/webServer.js +2535 -0
- package/src/native/autotune.js +417 -0
- package/src/renderer/adapters/ElectronBridge.js +677 -0
- package/src/renderer/canvas.html +80 -0
- package/src/renderer/components/App.jsx +303 -0
- package/src/renderer/components/AppRoot.jsx +37 -0
- package/src/renderer/components/AudioDeviceSettings.jsx +145 -0
- package/src/renderer/components/EffectsPanelWrapper.jsx +267 -0
- package/src/renderer/components/MixerTab.jsx +233 -0
- package/src/renderer/components/MixerTabWrapper.jsx +31 -0
- package/src/renderer/components/PortalSelect.jsx +239 -0
- package/src/renderer/components/QueueTab.jsx +116 -0
- package/src/renderer/components/RequestsListWrapper.jsx +78 -0
- package/src/renderer/components/ServerTab.jsx +472 -0
- package/src/renderer/components/SongInfoBarWrapper.jsx +77 -0
- package/src/renderer/components/StatusBar.jsx +92 -0
- package/src/renderer/components/TabNavigation.jsx +77 -0
- package/src/renderer/components/TransportControlsWrapper.jsx +69 -0
- package/src/renderer/components/creator/CreateTab.jsx +1236 -0
- package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js +2 -0
- package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +1 -0
- package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js +2 -0
- package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js.map +1 -0
- package/src/renderer/dist/assets/player-DVrqp7N5.js +3 -0
- package/src/renderer/dist/assets/player-DVrqp7N5.js.map +1 -0
- package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +2 -0
- package/src/renderer/dist/assets/songLoaders-BaTgGib4.js.map +1 -0
- package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js +2 -0
- package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js.map +1 -0
- package/src/renderer/dist/js/autoTuneWorklet.js +224 -0
- package/src/renderer/dist/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/dist/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/dist/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/dist/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/dist/renderer.css +1 -0
- package/src/renderer/dist/renderer.js +62 -0
- package/src/renderer/dist/renderer.js.map +1 -0
- package/src/renderer/dist/renderer.woff2 +0 -0
- package/src/renderer/hooks/useKeyboardShortcuts.js +154 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/index.html.backup +372 -0
- package/src/renderer/js/PlayerInterface.js +267 -0
- package/src/renderer/js/autoTuneWorklet.js +224 -0
- package/src/renderer/js/butterchurnVerify.js +46 -0
- package/src/renderer/js/canvas-app.js +114 -0
- package/src/renderer/js/cdgPlayer.js +685 -0
- package/src/renderer/js/kaiPlayer.js +1200 -0
- package/src/renderer/js/karaokeRenderer.js +3392 -0
- package/src/renderer/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/js/microphoneEngine.js +656 -0
- package/src/renderer/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/js/player.js +232 -0
- package/src/renderer/js/referencePitchTracker.js +130 -0
- package/src/renderer/js/songLoaders.js +334 -0
- package/src/renderer/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/js/webrtcManager.js +511 -0
- package/src/renderer/lib/butterchurn.min.js +6739 -0
- package/src/renderer/lib/butterchurnPresets.min.js +1 -0
- package/src/renderer/lib/cdgraphics-wrapper.js +16 -0
- package/src/renderer/lib/cdgraphics.js +299 -0
- package/src/renderer/public/js/autoTuneWorklet.js +224 -0
- package/src/renderer/public/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/public/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/public/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/public/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/react-entry.jsx +44 -0
- package/src/renderer/styles/tailwind.css +106 -0
- package/src/renderer/utils/qrCodeGenerator.js +98 -0
- package/src/renderer/vite.config.js +31 -0
- package/src/shared/adapters/BridgeInterface.js +195 -0
- package/src/shared/components/EffectsPanel.jsx +177 -0
- package/src/shared/components/LibraryPanel.jsx +701 -0
- package/src/shared/components/LineDetailCanvas.jsx +167 -0
- package/src/shared/components/LyricLine.jsx +505 -0
- package/src/shared/components/LyricRejection.jsx +84 -0
- package/src/shared/components/LyricSuggestion.jsx +80 -0
- package/src/shared/components/LyricsEditorCanvas.jsx +271 -0
- package/src/shared/components/MixerPanel.jsx +94 -0
- package/src/shared/components/PlayerControls.jsx +206 -0
- package/src/shared/components/PortalSelect.jsx +239 -0
- package/src/shared/components/QueueList.jsx +365 -0
- package/src/shared/components/QuickSearch.jsx +126 -0
- package/src/shared/components/RequestsList.jsx +121 -0
- package/src/shared/components/SongEditor.jsx +1362 -0
- package/src/shared/components/SongInfoBar.jsx +81 -0
- package/src/shared/components/ThemeToggle.jsx +106 -0
- package/src/shared/components/Toast.jsx +30 -0
- package/src/shared/components/VisualizationSettings.jsx +243 -0
- package/src/shared/constants.js +95 -0
- package/src/shared/context/BridgeContext.jsx +32 -0
- package/src/shared/contexts/AudioContext.jsx +37 -0
- package/src/shared/contexts/PlayerContext.jsx +66 -0
- package/src/shared/contexts/SettingsContext.jsx +50 -0
- package/src/shared/defaults.js +158 -0
- package/src/shared/formatUtils.js +59 -0
- package/src/shared/formatUtils.test.js +207 -0
- package/src/shared/hooks/useAppState.js +97 -0
- package/src/shared/hooks/useAudioEngine.js +264 -0
- package/src/shared/hooks/usePlayer.js +89 -0
- package/src/shared/hooks/useSettingsPersistence.js +74 -0
- package/src/shared/hooks/useWebRTC.js +118 -0
- package/src/shared/ipcContracts.js +299 -0
- package/src/shared/package.json +3 -0
- package/src/shared/services/creatorService.js +373 -0
- package/src/shared/services/creatorService.test.js +413 -0
- package/src/shared/services/editorService.js +213 -0
- package/src/shared/services/editorService.test.js +219 -0
- package/src/shared/services/effectsService.js +271 -0
- package/src/shared/services/effectsService.test.js +418 -0
- package/src/shared/services/libraryService.js +438 -0
- package/src/shared/services/libraryService.test.js +474 -0
- package/src/shared/services/mixerService.js +172 -0
- package/src/shared/services/mixerService.test.js +399 -0
- package/src/shared/services/playerService.js +221 -0
- package/src/shared/services/playerService.test.js +357 -0
- package/src/shared/services/preferencesService.js +219 -0
- package/src/shared/services/queueService.js +226 -0
- package/src/shared/services/queueService.test.js +430 -0
- package/src/shared/services/requestsService.js +155 -0
- package/src/shared/services/requestsService.test.js +362 -0
- package/src/shared/services/serverSettingsService.js +151 -0
- package/src/shared/services/settingsService.js +257 -0
- package/src/shared/services/settingsService.test.js +295 -0
- package/src/shared/state/StateManager.js +263 -0
- package/src/shared/utils/audio.js +42 -0
- package/src/shared/utils/format.js +32 -0
- package/src/shared/utils/lyricsUtils.js +162 -0
- package/src/test/setup.js +40 -0
- package/src/utils/cdgLoader.js +180 -0
- package/src/utils/m4aLoader.js +333 -0
- package/src/web/App.jsx +578 -0
- package/src/web/adapters/WebBridge.js +428 -0
- package/src/web/components/PlayerSettingsPanel.jsx +231 -0
- package/src/web/components/SongSearch.jsx +180 -0
- package/src/web/dist/assets/index-0H-RnRrV.js +51 -0
- package/src/web/dist/assets/index-0H-RnRrV.js.map +1 -0
- package/src/web/dist/assets/index-DYW2zB0u.css +1 -0
- package/src/web/dist/index.html +15 -0
- package/src/web/index.html +14 -0
- package/src/web/main.jsx +10 -0
- package/src/web/package-lock.json +1765 -0
- package/src/web/pages/SongRequestPage.jsx +619 -0
- package/src/web/styles/tailwind.css +68 -0
- package/src/web/vite.config.js +27 -0
- package/static/fonts/material-icons.woff2 +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Potion of Spirits.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Songflower _Moss Posy_.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Storm of the Eye _Thunder_ - mash0000 - quasi pseudo meta concentrics.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi _ Geiss - Airhandler _Kali Mix_ - Canvas Mix.png +0 -0
- package/static/images/butterchurn-screenshots/An AdamFX n Martin Infusion 2 flexi - Why The Sky Looks Diffrent Today - AdamFx n Martin Infusion - Tack Tile Disfunction B.png +0 -0
- package/static/images/butterchurn-screenshots/Cope - The Neverending Explosion of Red Liquid Fire.png +0 -0
- proton lights __Krash_s beat code_ _Phat_remix02b.png +0 -0
- package/static/images/butterchurn-screenshots/Eo_S_ _ Phat - cubetrace - v2.png +0 -0
- package/static/images/butterchurn-screenshots/Eo_S_ _ Zylot - skylight _Stained Glass Majesty mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - alien fish pond.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - area 51.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - infused with the spiral.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - mindblob _shiny mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - mindblob mix.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - predator-prey-spirals.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - smashing fractals _acid etching mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - truly soft piece of software - this is generic texturing _Jelly_ .png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ Martin - astral projection.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ Martin - cascading decay swing.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ amandio c - piercing 05 - Kopie _2_ - Kopie.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ stahlregen - jelly showoff parade.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi_ fishbrain_ Geiss _ Martin - tokamak witchery.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi_ martin _ geiss - dedicated to the sherwin maxawow.png +0 -0
- package/static/images/butterchurn-screenshots/Fumbling_Foo _ Flexi_ Martin_ Orb_ Unchained - Star Nova v7b.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Cauldron - painterly 2 _saturation remix_.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Reaction Diffusion 2.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Spiral Artifact.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Thumb Drum.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss _ Flexi _ Martin - disconnected.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss_ Flexi _ Stahlregen - Thumbdrum Tokamak _crossfiring aftermath jelly mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/Goody - The Wild Vort.png +0 -0
- package/static/images/butterchurn-screenshots/Idiot - Star Of Annon.png +0 -0
- package/static/images/butterchurn-screenshots/Krash _ Illusion - Spiral Movement.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - QBikal - Surface Turbulence IIb.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - acid wiring.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - charisma.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - liquid arrows.png +0 -0
- package/static/images/butterchurn-screenshots/Milk Artist At our Best - FED - SlowFast Ft AdamFX n Martin - HD CosmoFX.png +0 -0
- package/static/images/butterchurn-screenshots/ORB - Waaa.png +0 -0
- package/static/images/butterchurn-screenshots/Phat_fiShbRaiN_Eo_S_Mandala_Chasers_remix.png +0 -0
- package/static/images/butterchurn-screenshots/Rovastar - Oozing Resistance.png +0 -0
- package/static/images/butterchurn-screenshots/Rovastar _ Loadus _ Geiss - FractalDrop _Triple Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/TonyMilkdrop - Leonardo Da Vinci_s Balloon _Flexi - merry-go-round _ techstyle_.png +0 -0
- package/static/images/butterchurn-screenshots/TonyMilkdrop - Magellan_s Nebula _Flexi - you enter first _ multiverse_.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained - Rewop.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained - Unified Drag 2.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained _ Rovastar - Wormhole Pillars _Hall of Shadows mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - Paint Spill _Music Reactive Paint Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - Star Ornament.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - True Visionary _Final Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/_Aderrasi - Wanderer in Curved Space - mash0000 - faclempt kibitzing meshuggana schmaltz _Geiss color mix_.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - Artifact 01.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - Desert Rose 2.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - untitled.png +0 -0
- package/static/images/butterchurn-screenshots/_Mig_049.png +0 -0
- package/static/images/butterchurn-screenshots/_Mig_085.png +0 -0
- package/static/images/butterchurn-screenshots/_Rovastar _ Geiss - Hurricane Nightmare _Posterize Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _197_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _220_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _431_.png +0 -0
- package/static/images/butterchurn-screenshots/cope _ martin - mother-of-pearl.png +0 -0
- package/static/images/butterchurn-screenshots/fiShbRaiN _ Flexi - witchcraft 2_0.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - bouncing balls _double mindblob neon mix_.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - mom_ why the sky looks different today.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - patternton_ district of media_ capitol of the united abstractions of fractopia.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - swing out on the spiral.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - what is the matrix.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ amandio c - organic _random mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ amandio c - organic12-3d-2_milk.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ fishbrain - neon mindblob grafitti.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ geiss - pogo cubes vs_ tokamak vs_ game of life _stahls jelly 4_5 finish_.png +0 -0
- package/static/images/butterchurn-screenshots/high-altitude basket unraveling - singh grooves nitrogen argon nz_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - The Bridge of Khazad-Dum.png +0 -0
- package/static/images/butterchurn-screenshots/martin - angel flight.png +0 -0
- package/static/images/butterchurn-screenshots/martin - another kind of groove.png +0 -0
- package/static/images/butterchurn-screenshots/martin - bombyx mori.png +0 -0
- package/static/images/butterchurn-screenshots/martin - castle in the air.png +0 -0
- package/static/images/butterchurn-screenshots/martin - chain breaker.png +0 -0
- package/static/images/butterchurn-screenshots/martin - disco mix 4.png +0 -0
- package/static/images/butterchurn-screenshots/martin - extreme heat.png +0 -0
- package/static/images/butterchurn-screenshots/martin - frosty caves 2.png +0 -0
- package/static/images/butterchurn-screenshots/martin - fruit machine.png +0 -0
- package/static/images/butterchurn-screenshots/martin - ghost city.png +0 -0
- package/static/images/butterchurn-screenshots/martin - glass corridor.png +0 -0
- package/static/images/butterchurn-screenshots/martin - infinity _2010 update_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - mandelbox explorer - high speed demo version.png +0 -0
- package/static/images/butterchurn-screenshots/martin - mucus cervix.png +0 -0
- package/static/images/butterchurn-screenshots/martin - reflections on black tiles.png +0 -0
- package/static/images/butterchurn-screenshots/martin - stormy sea _2010 update_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - witchcraft reloaded.png +0 -0
- package/static/images/butterchurn-screenshots/martin _ flexi - diamond cutter _prismaticvortex_com_ - camille - i wish i wish i wish i was constrained.png +0 -0
- package/static/images/butterchurn-screenshots/martin _shadow harlequins shape code_ - fata morgana.png +0 -0
- package/static/images/butterchurn-screenshots/martin_ flexi_ fishbrain _ sto - enterstate _random mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/sawtooth grin roam.png +0 -0
- package/static/images/butterchurn-screenshots/shifter - dark tides bdrv mix 2.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - Rovastar - Sunflower Passion _Enlightment Mix__Phat_edit _ flexi und martin shaders - circumflex in character classes in regular expression.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - heretical crosscut playpen.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - uninitialized variabowl _hydroponic chronic_.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - vector exp 1 - couldn_t not.png +0 -0
- package/static/images/butterchurn-screenshots/yin - 191 - Temporal singularities.png +0 -0
- package/static/images/logo-512.png +0 -0
- package/static/images/logo.png +0 -0
- package/static/loukai-logo.png +0 -0
- package/static/screenshot-generator.html +610 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ServerTab - Server settings and management component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
6
|
+
import { generateQRCode } from '../utils/qrCodeGenerator.js';
|
|
7
|
+
|
|
8
|
+
export function ServerTab({ bridge }) {
|
|
9
|
+
const [serverUrl, setServerUrl] = useState(null);
|
|
10
|
+
const [serverPort, setServerPort] = useState(null);
|
|
11
|
+
const [settings, setSettings] = useState({
|
|
12
|
+
serverName: 'Loukai Karaoke',
|
|
13
|
+
port: 3069,
|
|
14
|
+
allowSongRequests: true,
|
|
15
|
+
requireKJApproval: true,
|
|
16
|
+
streamVocalsToClients: false,
|
|
17
|
+
showQrCode: true,
|
|
18
|
+
displayQueue: true,
|
|
19
|
+
});
|
|
20
|
+
const [adminPassword, setAdminPassword] = useState('');
|
|
21
|
+
const [hasPassword, setHasPassword] = useState(false);
|
|
22
|
+
const [pendingRequests, setPendingRequests] = useState(0);
|
|
23
|
+
const [totalRequests, setTotalRequests] = useState(0);
|
|
24
|
+
const [message, setMessage] = useState(null);
|
|
25
|
+
const [qrCodeDataUrl, setQrCodeDataUrl] = useState(null);
|
|
26
|
+
|
|
27
|
+
// Wrap in useCallback to stabilize reference
|
|
28
|
+
const updateRequestsStats = useCallback(async () => {
|
|
29
|
+
try {
|
|
30
|
+
if (!bridge?.getRequests) return;
|
|
31
|
+
const requests = await bridge.getRequests();
|
|
32
|
+
const pending = requests.filter((r) => r.status === 'pending').length;
|
|
33
|
+
setPendingRequests(pending);
|
|
34
|
+
setTotalRequests(requests.length);
|
|
35
|
+
} catch {
|
|
36
|
+
// Silently fail
|
|
37
|
+
}
|
|
38
|
+
}, [bridge]);
|
|
39
|
+
|
|
40
|
+
// Load initial server status and settings
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!bridge) return;
|
|
43
|
+
|
|
44
|
+
const loadData = async () => {
|
|
45
|
+
try {
|
|
46
|
+
// Get server URL
|
|
47
|
+
const url = await bridge.getServerUrl();
|
|
48
|
+
setServerUrl(url);
|
|
49
|
+
if (url) {
|
|
50
|
+
const port = new URL(url).port;
|
|
51
|
+
setServerPort(port);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get settings
|
|
55
|
+
const serverSettings = await bridge.getServerSettings();
|
|
56
|
+
if (serverSettings) {
|
|
57
|
+
setSettings({
|
|
58
|
+
serverName: serverSettings.serverName || 'Loukai Karaoke',
|
|
59
|
+
port: serverSettings.port || 3069,
|
|
60
|
+
allowSongRequests: serverSettings.allowSongRequests !== false,
|
|
61
|
+
requireKJApproval: serverSettings.requireKJApproval !== false,
|
|
62
|
+
streamVocalsToClients: serverSettings.streamVocalsToClients === true,
|
|
63
|
+
showQrCode: serverSettings.showQrCode !== false,
|
|
64
|
+
displayQueue: serverSettings.displayQueue !== false,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check password status
|
|
69
|
+
if (bridge.getAdminPasswordStatus) {
|
|
70
|
+
const passwordSet = await bridge.getAdminPasswordStatus();
|
|
71
|
+
setHasPassword(passwordSet);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get request stats
|
|
75
|
+
updateRequestsStats();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Failed to load server data:', error);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
loadData();
|
|
82
|
+
|
|
83
|
+
// Poll for server URL and request stats
|
|
84
|
+
const pollInterval = setInterval(() => {
|
|
85
|
+
if (bridge.getServerUrl) {
|
|
86
|
+
bridge
|
|
87
|
+
.getServerUrl()
|
|
88
|
+
.then((url) => {
|
|
89
|
+
setServerUrl(url);
|
|
90
|
+
if (url) {
|
|
91
|
+
const port = new URL(url).port;
|
|
92
|
+
setServerPort(port);
|
|
93
|
+
// Generate QR code when URL is available
|
|
94
|
+
generateQRCode(url, { width: 300 }).then(setQrCodeDataUrl).catch(console.error);
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.catch(console.error);
|
|
98
|
+
}
|
|
99
|
+
updateRequestsStats();
|
|
100
|
+
}, 5000);
|
|
101
|
+
|
|
102
|
+
return () => clearInterval(pollInterval);
|
|
103
|
+
}, [bridge, updateRequestsStats]);
|
|
104
|
+
|
|
105
|
+
const handleSaveSettings = async () => {
|
|
106
|
+
try {
|
|
107
|
+
await bridge.updateServerSettings(settings);
|
|
108
|
+
showMessage('Settings saved successfully', 'success');
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Failed to save settings:', error);
|
|
111
|
+
showMessage('Failed to save settings', 'error');
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleSetPassword = async () => {
|
|
116
|
+
const password = adminPassword.trim();
|
|
117
|
+
|
|
118
|
+
if (!password) {
|
|
119
|
+
showMessage('Please enter a password', 'error');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (password.length < 6) {
|
|
124
|
+
showMessage('Password must be at least 6 characters', 'error');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
if (bridge.setAdminPassword) {
|
|
130
|
+
await bridge.setAdminPassword(password);
|
|
131
|
+
setAdminPassword('');
|
|
132
|
+
setHasPassword(true);
|
|
133
|
+
showMessage('Admin password set successfully', 'success');
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('Failed to set password:', error);
|
|
137
|
+
showMessage('Failed to set admin password', 'error');
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handleOpenServer = () => {
|
|
142
|
+
if (serverUrl && bridge?.openExternal) {
|
|
143
|
+
bridge.openExternal(serverUrl);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handleOpenAdmin = () => {
|
|
148
|
+
try {
|
|
149
|
+
if (serverPort && bridge?.openExternal) {
|
|
150
|
+
bridge.openExternal(`http://localhost:${serverPort}/admin`);
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('Failed to open admin panel:', error);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleClearRequests = async () => {
|
|
158
|
+
if (!confirm('Are you sure you want to clear all song requests? This cannot be undone.')) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
if (bridge.clearAllRequests) {
|
|
164
|
+
await bridge.clearAllRequests();
|
|
165
|
+
showMessage('All requests cleared', 'success');
|
|
166
|
+
updateRequestsStats();
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('Failed to clear requests:', error);
|
|
170
|
+
showMessage('Failed to clear requests', 'error');
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const showMessage = (text, type = 'info') => {
|
|
175
|
+
setMessage({ text, type });
|
|
176
|
+
setTimeout(() => setMessage(null), 3000);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const handleSettingChange = (key, value) => {
|
|
180
|
+
setSettings((prev) => ({ ...prev, [key]: value }));
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const isServerRunning = Boolean(serverUrl);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="p-5 h-full overflow-y-auto bg-white dark:bg-gray-900">
|
|
187
|
+
{message && (
|
|
188
|
+
<div
|
|
189
|
+
className={`fixed top-5 right-5 px-5 py-3 rounded text-white font-medium z-[10000] shadow-lg animate-slide-in ${
|
|
190
|
+
message.type === 'success'
|
|
191
|
+
? 'bg-green-600'
|
|
192
|
+
: message.type === 'error'
|
|
193
|
+
? 'bg-red-600'
|
|
194
|
+
: 'bg-blue-500'
|
|
195
|
+
}`}
|
|
196
|
+
>
|
|
197
|
+
{message.text}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
<div className="flex items-center justify-between mb-8 pb-4 border-b border-gray-200 dark:border-gray-700">
|
|
202
|
+
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
|
203
|
+
Web Server Settings
|
|
204
|
+
</h2>
|
|
205
|
+
<div className="flex items-center gap-2">
|
|
206
|
+
<span
|
|
207
|
+
className={`w-3 h-3 rounded-full ${isServerRunning ? 'bg-green-500' : 'bg-red-500'}`}
|
|
208
|
+
/>
|
|
209
|
+
<span className="text-gray-700 dark:text-gray-300">
|
|
210
|
+
{isServerRunning ? `Running on port ${serverPort}` : 'Not running'}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div className="space-y-6">
|
|
216
|
+
{/* Server Control */}
|
|
217
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
|
|
218
|
+
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
219
|
+
Server Control
|
|
220
|
+
</h3>
|
|
221
|
+
<div className="space-y-4">
|
|
222
|
+
<div className="flex items-center justify-between gap-4">
|
|
223
|
+
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
224
|
+
Server Status:
|
|
225
|
+
</label>
|
|
226
|
+
<div className="flex items-center gap-3 flex-1">
|
|
227
|
+
<span className="text-gray-900 dark:text-gray-100 flex-1">
|
|
228
|
+
{serverUrl || 'Not running'}
|
|
229
|
+
</span>
|
|
230
|
+
<button
|
|
231
|
+
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white rounded transition-colors"
|
|
232
|
+
onClick={handleOpenServer}
|
|
233
|
+
disabled={!isServerRunning}
|
|
234
|
+
>
|
|
235
|
+
<span className="material-icons text-lg">open_in_new</span>
|
|
236
|
+
Open in Browser
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
{/* Server Settings */}
|
|
244
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
|
|
245
|
+
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
246
|
+
General Settings
|
|
247
|
+
</h3>
|
|
248
|
+
<div className="space-y-4">
|
|
249
|
+
<div className="space-y-2">
|
|
250
|
+
<label
|
|
251
|
+
htmlFor="serverName"
|
|
252
|
+
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
253
|
+
>
|
|
254
|
+
Server Name:
|
|
255
|
+
</label>
|
|
256
|
+
<input
|
|
257
|
+
type="text"
|
|
258
|
+
id="serverName"
|
|
259
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
260
|
+
placeholder="Loukai Karaoke"
|
|
261
|
+
value={settings.serverName}
|
|
262
|
+
onChange={(e) => handleSettingChange('serverName', e.target.value)}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div className="space-y-2">
|
|
267
|
+
<label
|
|
268
|
+
htmlFor="serverPort"
|
|
269
|
+
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
270
|
+
>
|
|
271
|
+
Port:
|
|
272
|
+
</label>
|
|
273
|
+
<input
|
|
274
|
+
type="number"
|
|
275
|
+
id="serverPort"
|
|
276
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
277
|
+
placeholder="3069"
|
|
278
|
+
min="1024"
|
|
279
|
+
max="65535"
|
|
280
|
+
value={settings.port}
|
|
281
|
+
onChange={(e) => handleSettingChange('port', parseInt(e.target.value) || 3069)}
|
|
282
|
+
/>
|
|
283
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
284
|
+
Server must be restarted for port changes to take effect
|
|
285
|
+
</p>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div className="flex items-center">
|
|
289
|
+
<label className="flex items-center cursor-pointer select-none">
|
|
290
|
+
<input
|
|
291
|
+
type="checkbox"
|
|
292
|
+
id="allowSongRequests"
|
|
293
|
+
className="w-4 h-4 mr-2 cursor-pointer"
|
|
294
|
+
checked={settings.allowSongRequests}
|
|
295
|
+
onChange={(e) => handleSettingChange('allowSongRequests', e.target.checked)}
|
|
296
|
+
/>
|
|
297
|
+
<span className="text-gray-900 dark:text-gray-100">Allow Song Requests</span>
|
|
298
|
+
</label>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div className="flex items-center">
|
|
302
|
+
<label className="flex items-center cursor-pointer select-none">
|
|
303
|
+
<input
|
|
304
|
+
type="checkbox"
|
|
305
|
+
id="requireKJApproval"
|
|
306
|
+
className="w-4 h-4 mr-2 cursor-pointer"
|
|
307
|
+
checked={settings.requireKJApproval}
|
|
308
|
+
onChange={(e) => handleSettingChange('requireKJApproval', e.target.checked)}
|
|
309
|
+
/>
|
|
310
|
+
<span className="text-gray-900 dark:text-gray-100">Require KJ Approval</span>
|
|
311
|
+
</label>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<div className="flex items-center">
|
|
315
|
+
<label className="flex items-center cursor-pointer select-none">
|
|
316
|
+
<input
|
|
317
|
+
type="checkbox"
|
|
318
|
+
id="streamVocalsToClients"
|
|
319
|
+
className="w-4 h-4 mr-2 cursor-pointer"
|
|
320
|
+
checked={settings.streamVocalsToClients}
|
|
321
|
+
onChange={(e) => handleSettingChange('streamVocalsToClients', e.target.checked)}
|
|
322
|
+
/>
|
|
323
|
+
<span className="text-gray-900 dark:text-gray-100">
|
|
324
|
+
Stream Vocals to Clients (LAN only)
|
|
325
|
+
</span>
|
|
326
|
+
</label>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<div className="flex items-center">
|
|
330
|
+
<label className="flex items-center cursor-pointer select-none">
|
|
331
|
+
<input
|
|
332
|
+
type="checkbox"
|
|
333
|
+
id="showQrCode"
|
|
334
|
+
className="w-4 h-4 mr-2 cursor-pointer"
|
|
335
|
+
checked={settings.showQrCode}
|
|
336
|
+
onChange={(e) => handleSettingChange('showQrCode', e.target.checked)}
|
|
337
|
+
/>
|
|
338
|
+
<span className="text-gray-900 dark:text-gray-100">Show QR code</span>
|
|
339
|
+
</label>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div className="flex items-center">
|
|
343
|
+
<label className="flex items-center cursor-pointer select-none">
|
|
344
|
+
<input
|
|
345
|
+
type="checkbox"
|
|
346
|
+
id="displayQueue"
|
|
347
|
+
className="w-4 h-4 mr-2 cursor-pointer"
|
|
348
|
+
checked={settings.displayQueue}
|
|
349
|
+
onChange={(e) => handleSettingChange('displayQueue', e.target.checked)}
|
|
350
|
+
/>
|
|
351
|
+
<span className="text-gray-900 dark:text-gray-100">Display queue</span>
|
|
352
|
+
</label>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<button
|
|
356
|
+
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors"
|
|
357
|
+
onClick={handleSaveSettings}
|
|
358
|
+
>
|
|
359
|
+
<span className="material-icons text-lg">save</span>
|
|
360
|
+
Save Settings
|
|
361
|
+
</button>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
{/* Admin Password */}
|
|
366
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
|
|
367
|
+
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
368
|
+
Admin Security
|
|
369
|
+
</h3>
|
|
370
|
+
<div className="space-y-4">
|
|
371
|
+
<div className="space-y-2">
|
|
372
|
+
<label
|
|
373
|
+
htmlFor="adminPassword"
|
|
374
|
+
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
375
|
+
>
|
|
376
|
+
Admin Password:
|
|
377
|
+
</label>
|
|
378
|
+
<div className="flex gap-2">
|
|
379
|
+
<input
|
|
380
|
+
type="password"
|
|
381
|
+
id="adminPassword"
|
|
382
|
+
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
383
|
+
placeholder="Enter new admin password"
|
|
384
|
+
value={adminPassword}
|
|
385
|
+
onChange={(e) => setAdminPassword(e.target.value)}
|
|
386
|
+
/>
|
|
387
|
+
<button
|
|
388
|
+
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors"
|
|
389
|
+
onClick={handleSetPassword}
|
|
390
|
+
>
|
|
391
|
+
<span className="material-icons text-lg">security</span>
|
|
392
|
+
Set Password
|
|
393
|
+
</button>
|
|
394
|
+
</div>
|
|
395
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
396
|
+
KJs will need this password to access the admin panel
|
|
397
|
+
</p>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<div
|
|
401
|
+
className={`px-4 py-2 rounded ${hasPassword ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'}`}
|
|
402
|
+
>
|
|
403
|
+
{hasPassword ? 'Admin password is set' : 'No admin password set'}
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
{/* Song Requests */}
|
|
409
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
|
|
410
|
+
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
411
|
+
Song Requests
|
|
412
|
+
</h3>
|
|
413
|
+
<div className="space-y-4">
|
|
414
|
+
<div className="grid grid-cols-2 gap-4">
|
|
415
|
+
<div className="bg-white dark:bg-gray-700 rounded-lg p-4 text-center">
|
|
416
|
+
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
|
417
|
+
{pendingRequests}
|
|
418
|
+
</div>
|
|
419
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">Pending</div>
|
|
420
|
+
</div>
|
|
421
|
+
<div className="bg-white dark:bg-gray-700 rounded-lg p-4 text-center">
|
|
422
|
+
<div className="text-3xl font-bold text-gray-700 dark:text-gray-300">
|
|
423
|
+
{totalRequests}
|
|
424
|
+
</div>
|
|
425
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">Total</div>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
<div className="flex gap-2">
|
|
429
|
+
<button
|
|
430
|
+
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors flex-1"
|
|
431
|
+
onClick={handleOpenAdmin}
|
|
432
|
+
>
|
|
433
|
+
<span className="material-icons text-lg">admin_panel_settings</span>
|
|
434
|
+
Open Admin Panel
|
|
435
|
+
</button>
|
|
436
|
+
<button
|
|
437
|
+
className="flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded transition-colors flex-1"
|
|
438
|
+
onClick={handleClearRequests}
|
|
439
|
+
>
|
|
440
|
+
<span className="material-icons text-lg">delete_sweep</span>
|
|
441
|
+
Clear All Requests
|
|
442
|
+
</button>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
{/* QR Code */}
|
|
448
|
+
{qrCodeDataUrl && (
|
|
449
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
|
|
450
|
+
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
451
|
+
Quick Access QR Code
|
|
452
|
+
</h3>
|
|
453
|
+
<div className="flex flex-col items-center gap-4">
|
|
454
|
+
<img
|
|
455
|
+
src={qrCodeDataUrl}
|
|
456
|
+
alt="Server URL QR Code"
|
|
457
|
+
className="border-4 border-white rounded-lg shadow-lg"
|
|
458
|
+
style={{ width: '300px', height: '300px' }}
|
|
459
|
+
/>
|
|
460
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 text-center">
|
|
461
|
+
Scan this QR code to access the song request page
|
|
462
|
+
</p>
|
|
463
|
+
<p className="text-xs text-gray-500 dark:text-gray-500 text-center font-mono">
|
|
464
|
+
{serverUrl}
|
|
465
|
+
</p>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SongInfoBarWrapper - Renderer-specific wrapper for SongInfoBar
|
|
3
|
+
* Manages state and bridge integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState, useEffect } from 'react';
|
|
7
|
+
import { SongInfoBar } from '../../shared/components/SongInfoBar.jsx';
|
|
8
|
+
import { UI_DEFAULTS } from '../../shared/defaults.js';
|
|
9
|
+
|
|
10
|
+
export function SongInfoBarWrapper({ bridge }) {
|
|
11
|
+
const [currentSong, setCurrentSong] = useState(null);
|
|
12
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(UI_DEFAULTS.sidebarCollapsed);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!bridge) return;
|
|
16
|
+
|
|
17
|
+
// Subscribe to current song changes
|
|
18
|
+
const unsubscribe = bridge.onCurrentSongChanged?.((song) => {
|
|
19
|
+
setCurrentSong(song);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Fetch initial state
|
|
23
|
+
bridge
|
|
24
|
+
.getQueue?.()
|
|
25
|
+
.then((data) => {
|
|
26
|
+
if (data.currentSong) {
|
|
27
|
+
setCurrentSong(data.currentSong);
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.catch(console.error);
|
|
31
|
+
|
|
32
|
+
// Load sidebar state
|
|
33
|
+
bridge.api?.settings
|
|
34
|
+
.get('sidebarCollapsed', UI_DEFAULTS.sidebarCollapsed)
|
|
35
|
+
.then((collapsed) => {
|
|
36
|
+
setSidebarCollapsed(collapsed);
|
|
37
|
+
// Apply initial state to sidebar
|
|
38
|
+
const sidebar = document.getElementById('app-sidebar');
|
|
39
|
+
if (sidebar && collapsed) {
|
|
40
|
+
sidebar.classList.add('-ml-80', 'w-0', 'p-0', 'border-0');
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.catch(console.error);
|
|
44
|
+
|
|
45
|
+
return () => unsubscribe && unsubscribe();
|
|
46
|
+
}, [bridge]);
|
|
47
|
+
|
|
48
|
+
const handleMenuClick = async () => {
|
|
49
|
+
const sidebar = document.getElementById('app-sidebar');
|
|
50
|
+
if (!sidebar) return;
|
|
51
|
+
|
|
52
|
+
const newCollapsedState = !sidebarCollapsed;
|
|
53
|
+
setSidebarCollapsed(newCollapsedState);
|
|
54
|
+
|
|
55
|
+
// Toggle Tailwind classes
|
|
56
|
+
if (newCollapsedState) {
|
|
57
|
+
sidebar.classList.add('-ml-80', 'w-0', 'p-0', 'border-0');
|
|
58
|
+
} else {
|
|
59
|
+
sidebar.classList.remove('-ml-80', 'w-0', 'p-0', 'border-0');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Save state
|
|
63
|
+
try {
|
|
64
|
+
await bridge.api?.settings.set('sidebarCollapsed', newCollapsedState);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Failed to save sidebar state:', error);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<SongInfoBar
|
|
72
|
+
currentSong={currentSong}
|
|
73
|
+
onMenuClick={handleMenuClick}
|
|
74
|
+
sidebarCollapsed={sidebarCollapsed}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusBar - Renderer status bar component
|
|
3
|
+
*
|
|
4
|
+
* Displays status messages, server URL, latency, and xruns
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useEffect } from 'react';
|
|
8
|
+
|
|
9
|
+
export function StatusBar({ bridge }) {
|
|
10
|
+
const [statusText, _setStatusText] = useState('Ready');
|
|
11
|
+
const [webUrl, setWebUrl] = useState(null);
|
|
12
|
+
const [latency, setLatency] = useState(null);
|
|
13
|
+
const [xruns, setXruns] = useState(0);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!bridge) return;
|
|
17
|
+
|
|
18
|
+
const unsubscribers = [];
|
|
19
|
+
|
|
20
|
+
// Listen for audio latency updates
|
|
21
|
+
if (bridge.onLatencyUpdate) {
|
|
22
|
+
const unsubLatency = bridge.onLatencyUpdate((latencyMs) => {
|
|
23
|
+
setLatency(latencyMs);
|
|
24
|
+
});
|
|
25
|
+
if (unsubLatency) unsubscribers.push(unsubLatency);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Listen for xrun updates
|
|
29
|
+
if (bridge.onXRunUpdate) {
|
|
30
|
+
const unsubXrun = bridge.onXRunUpdate((count) => {
|
|
31
|
+
setXruns(count);
|
|
32
|
+
});
|
|
33
|
+
if (unsubXrun) unsubscribers.push(unsubXrun);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get initial server URL
|
|
37
|
+
if (bridge.getServerUrl) {
|
|
38
|
+
bridge
|
|
39
|
+
.getServerUrl()
|
|
40
|
+
.then((url) => {
|
|
41
|
+
setWebUrl(url || null);
|
|
42
|
+
})
|
|
43
|
+
.catch(console.error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Poll for server URL updates (every 5 seconds)
|
|
47
|
+
const pollInterval = setInterval(() => {
|
|
48
|
+
if (bridge.getServerUrl) {
|
|
49
|
+
bridge
|
|
50
|
+
.getServerUrl()
|
|
51
|
+
.then((url) => {
|
|
52
|
+
setWebUrl(url || null);
|
|
53
|
+
})
|
|
54
|
+
.catch(console.error);
|
|
55
|
+
}
|
|
56
|
+
}, 5000);
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
unsubscribers.forEach((unsub) => unsub && unsub());
|
|
60
|
+
clearInterval(pollInterval);
|
|
61
|
+
};
|
|
62
|
+
}, [bridge]);
|
|
63
|
+
|
|
64
|
+
const handleUrlClick = () => {
|
|
65
|
+
if (webUrl && bridge?.openExternal) {
|
|
66
|
+
bridge.openExternal(webUrl);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex justify-between items-center h-6 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-4 text-xs text-gray-600 dark:text-gray-400">
|
|
72
|
+
<div className="flex-1">
|
|
73
|
+
<span>{statusText}</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="flex-1 text-center">
|
|
76
|
+
{webUrl && (
|
|
77
|
+
<span
|
|
78
|
+
className="text-blue-500 dark:text-blue-400 cursor-pointer hover:underline"
|
|
79
|
+
onClick={handleUrlClick}
|
|
80
|
+
title="Click to open in browser"
|
|
81
|
+
>
|
|
82
|
+
🌐 {webUrl}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
<div className="flex-1 flex gap-4 justify-end">
|
|
87
|
+
<span>Latency: {latency !== null ? `${latency.toFixed(1)} ms` : '-- ms'}</span>
|
|
88
|
+
<span>XRuns: {xruns}</span>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|