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
package/src/web/App.jsx
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { WebBridge } from './adapters/WebBridge.js';
|
|
3
|
+
import { PlayerControls } from '../shared/components/PlayerControls.jsx';
|
|
4
|
+
import { QueueList } from '../shared/components/QueueList.jsx';
|
|
5
|
+
import { QuickSearch } from '../shared/components/QuickSearch.jsx';
|
|
6
|
+
import { MixerPanel } from '../shared/components/MixerPanel.jsx';
|
|
7
|
+
import { EffectsPanel } from '../shared/components/EffectsPanel.jsx';
|
|
8
|
+
import { LibraryPanel } from '../shared/components/LibraryPanel.jsx';
|
|
9
|
+
import { RequestsList } from '../shared/components/RequestsList.jsx';
|
|
10
|
+
import { SongEditor } from '../shared/components/SongEditor.jsx';
|
|
11
|
+
import { SongInfoBar } from '../shared/components/SongInfoBar.jsx';
|
|
12
|
+
import { VisualizationSettings } from '../shared/components/VisualizationSettings.jsx';
|
|
13
|
+
import { SongRequestPage } from './pages/SongRequestPage.jsx';
|
|
14
|
+
|
|
15
|
+
function LoginScreen({ onLogin, error }) {
|
|
16
|
+
const [password, setPassword] = useState('');
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
|
|
19
|
+
const handleSubmit = async (e) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
setLoading(true);
|
|
22
|
+
await onLogin(password);
|
|
23
|
+
setLoading(false);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
|
28
|
+
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-8 w-full max-w-md">
|
|
29
|
+
<div className="flex items-center justify-center gap-3 mb-6">
|
|
30
|
+
<img src="/static/loukai-logo.png" alt="Loukai" className="w-12 h-12 rounded-lg" />
|
|
31
|
+
<h1 className="text-3xl font-semibold text-gray-900 dark:text-white">Loukai Admin</h1>
|
|
32
|
+
</div>
|
|
33
|
+
<form onSubmit={handleSubmit}>
|
|
34
|
+
<div className="mb-4">
|
|
35
|
+
<label
|
|
36
|
+
htmlFor="password"
|
|
37
|
+
className="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-400"
|
|
38
|
+
>
|
|
39
|
+
Password
|
|
40
|
+
</label>
|
|
41
|
+
<input
|
|
42
|
+
id="password"
|
|
43
|
+
type="password"
|
|
44
|
+
value={password}
|
|
45
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
46
|
+
disabled={loading}
|
|
47
|
+
autoFocus
|
|
48
|
+
className="input"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
{error && (
|
|
52
|
+
<div className="mb-4 px-4 py-2 bg-red-900/20 border border-red-600 rounded text-red-500">
|
|
53
|
+
{error}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
<button type="submit" className="btn btn-primary w-full" disabled={loading}>
|
|
57
|
+
{loading ? 'Logging in...' : 'Login'}
|
|
58
|
+
</button>
|
|
59
|
+
</form>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function App() {
|
|
66
|
+
const [bridge] = useState(() => new WebBridge());
|
|
67
|
+
const [authenticated, setAuthenticated] = useState(false);
|
|
68
|
+
const [checking, setChecking] = useState(true);
|
|
69
|
+
const [loginError, setLoginError] = useState('');
|
|
70
|
+
const [isAdminPath, setIsAdminPath] = useState(false);
|
|
71
|
+
|
|
72
|
+
const [playback, setPlayback] = useState({
|
|
73
|
+
isPlaying: false,
|
|
74
|
+
position: 0,
|
|
75
|
+
duration: 0,
|
|
76
|
+
});
|
|
77
|
+
const [currentSong, setCurrentSong] = useState(null);
|
|
78
|
+
const [queue, setQueue] = useState([]);
|
|
79
|
+
const [mixer, setMixer] = useState(null);
|
|
80
|
+
const [effects, setEffects] = useState(null);
|
|
81
|
+
const [currentTab, setCurrentTab] = useState('queue');
|
|
82
|
+
const [requests, setRequests] = useState([]);
|
|
83
|
+
const [effectsSearch, setEffectsSearch] = useState('');
|
|
84
|
+
const [effectsCategory, setEffectsCategory] = useState('all');
|
|
85
|
+
const [waveformSettings, setWaveformSettings] = useState(null);
|
|
86
|
+
const [autotuneSettings, setAutotuneSettings] = useState(null);
|
|
87
|
+
|
|
88
|
+
// Check if we're on the admin path
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
setIsAdminPath(window.location.pathname.startsWith('/admin'));
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
// Check authentication on mount (only for admin path)
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!isAdminPath) {
|
|
96
|
+
setChecking(false);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fetch('/admin/check-auth', { credentials: 'include' })
|
|
101
|
+
.then((res) => res.json())
|
|
102
|
+
.then((data) => {
|
|
103
|
+
setAuthenticated(data.authenticated);
|
|
104
|
+
setChecking(false);
|
|
105
|
+
})
|
|
106
|
+
.catch(() => {
|
|
107
|
+
setAuthenticated(false);
|
|
108
|
+
setChecking(false);
|
|
109
|
+
});
|
|
110
|
+
}, [isAdminPath]);
|
|
111
|
+
|
|
112
|
+
// Connect bridge and fetch initial state when authenticated
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!authenticated) return;
|
|
115
|
+
|
|
116
|
+
let mounted = true;
|
|
117
|
+
|
|
118
|
+
// Connect bridge (initializes socket)
|
|
119
|
+
bridge.connect().then(() => {
|
|
120
|
+
if (!mounted) return;
|
|
121
|
+
|
|
122
|
+
// Fetch initial state
|
|
123
|
+
fetch('/api/state', { credentials: 'include' })
|
|
124
|
+
.then((res) => res.json())
|
|
125
|
+
.then((state) => {
|
|
126
|
+
if (!mounted) return;
|
|
127
|
+
setPlayback(state.playback || { isPlaying: false, position: 0, duration: 0 });
|
|
128
|
+
setCurrentSong(state.currentSong || null);
|
|
129
|
+
setQueue(state.queue || []);
|
|
130
|
+
setMixer(state.mixer || null);
|
|
131
|
+
})
|
|
132
|
+
.catch((err) => console.error('Failed to fetch state:', err));
|
|
133
|
+
|
|
134
|
+
// Fetch effects list
|
|
135
|
+
bridge
|
|
136
|
+
.getEffects()
|
|
137
|
+
.then((data) => {
|
|
138
|
+
if (!mounted) return;
|
|
139
|
+
console.log('📊 Fetched effects data:', data);
|
|
140
|
+
setEffects({
|
|
141
|
+
list: Array.isArray(data.effects) ? data.effects : [],
|
|
142
|
+
current: data.currentEffect || null,
|
|
143
|
+
disabled: Array.isArray(data.disabledEffects) ? data.disabledEffects : [],
|
|
144
|
+
});
|
|
145
|
+
})
|
|
146
|
+
.catch((err) => console.error('Failed to fetch effects:', err));
|
|
147
|
+
|
|
148
|
+
// Fetch requests
|
|
149
|
+
bridge
|
|
150
|
+
.getRequests()
|
|
151
|
+
.then((requestsData) => {
|
|
152
|
+
if (!mounted) return;
|
|
153
|
+
setRequests(requestsData);
|
|
154
|
+
})
|
|
155
|
+
.catch((err) => console.error('Failed to fetch requests:', err));
|
|
156
|
+
|
|
157
|
+
// Subscribe to real-time updates
|
|
158
|
+
bridge.onStateChange('playback', (data) => {
|
|
159
|
+
if (mounted) setPlayback(data);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
bridge.onStateChange('queue', (data) => {
|
|
163
|
+
if (!mounted) return;
|
|
164
|
+
console.log('📥 Received queue-update:', data);
|
|
165
|
+
setQueue(data.queue || []);
|
|
166
|
+
// Note: currentSong is now handled by dedicated 'current-song-update' event
|
|
167
|
+
// This prevents duplicate updates that could cause wrong highlighting
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Subscribe to current song updates (includes isLoading state)
|
|
171
|
+
bridge.onStateChange('currentSong', (song) => {
|
|
172
|
+
if (!mounted) return;
|
|
173
|
+
console.log('🎵 Received current-song-update:', song);
|
|
174
|
+
setCurrentSong(song);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
bridge.onStateChange('mixer', (newMixer) => {
|
|
178
|
+
console.log('🎚️ Received mixer-update:', newMixer);
|
|
179
|
+
if (mounted) setMixer(newMixer);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
bridge.onStateChange('effects', (data) => {
|
|
183
|
+
console.log('🎨 Received effects-update:', data);
|
|
184
|
+
if (!mounted) return;
|
|
185
|
+
setEffects((prev) => ({
|
|
186
|
+
list: data.effects && Array.isArray(data.effects) ? data.effects : prev?.list || [],
|
|
187
|
+
current:
|
|
188
|
+
data.current !== undefined
|
|
189
|
+
? data.current
|
|
190
|
+
: data.currentEffect !== undefined
|
|
191
|
+
? data.currentEffect
|
|
192
|
+
: prev?.current || null,
|
|
193
|
+
disabled:
|
|
194
|
+
data.disabled && Array.isArray(data.disabled) ? data.disabled : prev?.disabled || [],
|
|
195
|
+
}));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Additional socket events for song changes
|
|
199
|
+
if (bridge.socket) {
|
|
200
|
+
bridge.socket.on('song-loaded', (data) => {
|
|
201
|
+
if (!mounted) return;
|
|
202
|
+
console.log('🎵 song-loaded event:', data);
|
|
203
|
+
setCurrentSong({
|
|
204
|
+
title: data.title,
|
|
205
|
+
artist: data.artist,
|
|
206
|
+
duration: data.duration,
|
|
207
|
+
path: data.path,
|
|
208
|
+
requester: data.requester,
|
|
209
|
+
queueItemId: data.queueItemId,
|
|
210
|
+
isLoading: data.isLoading,
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
bridge.socket.on('effects:disabled', (data) => {
|
|
215
|
+
console.log('🎨 Effect disabled:', data);
|
|
216
|
+
if (!mounted) return;
|
|
217
|
+
setEffects((prev) => ({
|
|
218
|
+
...prev,
|
|
219
|
+
disabled: data.disabled || [],
|
|
220
|
+
}));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
bridge.socket.on('effects:enabled', (data) => {
|
|
224
|
+
console.log('🎨 Effect enabled:', data);
|
|
225
|
+
if (!mounted) return;
|
|
226
|
+
setEffects((prev) => ({
|
|
227
|
+
...prev,
|
|
228
|
+
disabled: data.disabled || [],
|
|
229
|
+
}));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
bridge.socket.on('song-request', (request) => {
|
|
233
|
+
if (mounted) setRequests((prev) => [request, ...prev]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
bridge.socket.on('request-approved', (request) => {
|
|
237
|
+
if (!mounted) return;
|
|
238
|
+
setRequests((prev) =>
|
|
239
|
+
prev.map((r) => (r.id === request.id ? { ...r, status: 'queued' } : r))
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
bridge.socket.on('request-rejected', (request) => {
|
|
244
|
+
if (!mounted) return;
|
|
245
|
+
setRequests((prev) =>
|
|
246
|
+
prev.map((r) => (r.id === request.id ? { ...r, status: 'rejected' } : r))
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Listen for settings changes from renderer
|
|
251
|
+
bridge.socket.on('settings:waveform', (settings) => {
|
|
252
|
+
console.log('🎨 Received waveform settings update:', settings);
|
|
253
|
+
if (mounted) setWaveformSettings(settings);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
bridge.socket.on('settings:autotune', (settings) => {
|
|
257
|
+
console.log('🎵 Received autotune settings update:', settings);
|
|
258
|
+
if (mounted) setAutotuneSettings(settings);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return () => {
|
|
264
|
+
mounted = false;
|
|
265
|
+
bridge.disconnect();
|
|
266
|
+
};
|
|
267
|
+
}, [authenticated, bridge]);
|
|
268
|
+
|
|
269
|
+
const handleLogin = async (password) => {
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch('/admin/login', {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: { 'Content-Type': 'application/json' },
|
|
274
|
+
credentials: 'include',
|
|
275
|
+
body: JSON.stringify({ password }),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const data = await res.json();
|
|
279
|
+
|
|
280
|
+
if (res.ok) {
|
|
281
|
+
setAuthenticated(true);
|
|
282
|
+
setLoginError('');
|
|
283
|
+
} else {
|
|
284
|
+
setLoginError(data.error || 'Login failed');
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
setLoginError('Network error. Please try again.');
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const handleLogout = async () => {
|
|
292
|
+
try {
|
|
293
|
+
await fetch('/admin/logout', {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
credentials: 'include',
|
|
296
|
+
});
|
|
297
|
+
setAuthenticated(false);
|
|
298
|
+
bridge.disconnect();
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.error('Logout failed:', err);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Player control handlers using bridge
|
|
305
|
+
const handlePlay = () => bridge.play().catch((err) => console.error('Play failed:', err));
|
|
306
|
+
const handlePause = () => bridge.pause().catch((err) => console.error('Pause failed:', err));
|
|
307
|
+
const handleRestart = () =>
|
|
308
|
+
bridge.restart().catch((err) => console.error('Restart failed:', err));
|
|
309
|
+
const handleNext = () => bridge.playNext().catch((err) => console.error('Next failed:', err));
|
|
310
|
+
const handleSeek = (position) =>
|
|
311
|
+
bridge.seek(position).catch((err) => console.error('Seek failed:', err));
|
|
312
|
+
|
|
313
|
+
// Queue handlers using bridge
|
|
314
|
+
const handlePlayFromQueue = (songId) => {
|
|
315
|
+
bridge.playFromQueue(songId).catch((err) => console.error('Play from queue failed:', err));
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const handleRemoveFromQueue = (songId) => {
|
|
319
|
+
bridge.removeFromQueue(songId).catch((err) => console.error('Remove from queue failed:', err));
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const handleClearQueue = () => {
|
|
323
|
+
bridge.clearQueue().catch((err) => console.error('Clear queue failed:', err));
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const handleReorderQueue = (songId, newIndex) => {
|
|
327
|
+
bridge
|
|
328
|
+
.reorderQueue(songId, newIndex)
|
|
329
|
+
.catch((err) => console.error('Reorder queue failed:', err));
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Mixer handlers using bridge
|
|
333
|
+
const handleGainChange = (bus, gain) => {
|
|
334
|
+
bridge.setMasterGain(bus, gain).catch((err) => console.error('Gain change failed:', err));
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const handleMuteToggle = (bus) => {
|
|
338
|
+
bridge.toggleMasterMute(bus).catch((err) => console.error('Mute toggle failed:', err));
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Effects handlers using bridge
|
|
342
|
+
const handleEffectPrevious = () => {
|
|
343
|
+
bridge.previousEffect().catch((err) => console.error('Previous effect failed:', err));
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const handleEffectNext = () => {
|
|
347
|
+
bridge.nextEffect().catch((err) => console.error('Next effect failed:', err));
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const handleEffectRandom = () => {
|
|
351
|
+
bridge.randomEffect().catch((err) => console.error('Random effect failed:', err));
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const handleEffectSelect = (effectName) => {
|
|
355
|
+
bridge.selectEffect(effectName).catch((err) => console.error('Select effect failed:', err));
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const handleEffectToggle = (effectName, isDisabled) => {
|
|
359
|
+
const action = isDisabled ? bridge.enableEffect(effectName) : bridge.disableEffect(effectName);
|
|
360
|
+
action.catch((err) => console.error('Toggle effect failed:', err));
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Show public song request page if not on admin path
|
|
364
|
+
if (!isAdminPath) {
|
|
365
|
+
return <SongRequestPage />;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Admin path only - check authentication
|
|
369
|
+
if (checking) {
|
|
370
|
+
return (
|
|
371
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
|
372
|
+
<div className="text-lg text-gray-600 dark:text-gray-400">Loading...</div>
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!authenticated) {
|
|
378
|
+
return <LoginScreen onLogin={handleLogin} error={loginError} />;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<div className="flex flex-col h-screen overflow-hidden bg-gray-50 dark:bg-gray-900">
|
|
383
|
+
<header className="flex justify-between items-center px-6 py-4 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
384
|
+
<div className="flex items-center gap-3">
|
|
385
|
+
<img src="/static/loukai-logo.png" alt="Loukai" className="w-8 h-8 rounded-lg" />
|
|
386
|
+
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Loukai Admin</h1>
|
|
387
|
+
</div>
|
|
388
|
+
<button className="btn btn-sm" onClick={handleLogout}>
|
|
389
|
+
Logout
|
|
390
|
+
</button>
|
|
391
|
+
</header>
|
|
392
|
+
|
|
393
|
+
<SongInfoBar currentSong={currentSong} />
|
|
394
|
+
|
|
395
|
+
<div className="flex bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
396
|
+
<button
|
|
397
|
+
className={`relative px-6 py-3 border-b-2 transition-colors font-medium flex items-center gap-2 ${
|
|
398
|
+
currentTab === 'queue'
|
|
399
|
+
? 'text-blue-600 dark:text-blue-400 border-blue-600 dark:border-blue-400 bg-gray-50 dark:bg-gray-900'
|
|
400
|
+
: 'text-gray-600 dark:text-gray-400 border-transparent hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
401
|
+
}`}
|
|
402
|
+
onClick={() => setCurrentTab('queue')}
|
|
403
|
+
>
|
|
404
|
+
🎵 Queue
|
|
405
|
+
</button>
|
|
406
|
+
<button
|
|
407
|
+
className={`relative px-6 py-3 border-b-2 transition-colors font-medium flex items-center gap-2 ${
|
|
408
|
+
currentTab === 'library'
|
|
409
|
+
? 'text-blue-600 dark:text-blue-400 border-blue-600 dark:border-blue-400 bg-gray-50 dark:bg-gray-900'
|
|
410
|
+
: 'text-gray-600 dark:text-gray-400 border-transparent hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
411
|
+
}`}
|
|
412
|
+
onClick={() => setCurrentTab('library')}
|
|
413
|
+
>
|
|
414
|
+
📚 Library
|
|
415
|
+
</button>
|
|
416
|
+
<button
|
|
417
|
+
className={`relative px-6 py-3 border-b-2 transition-colors font-medium flex items-center gap-2 ${
|
|
418
|
+
currentTab === 'mixer'
|
|
419
|
+
? 'text-blue-600 dark:text-blue-400 border-blue-600 dark:border-blue-400 bg-gray-50 dark:bg-gray-900'
|
|
420
|
+
: 'text-gray-600 dark:text-gray-400 border-transparent hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
421
|
+
}`}
|
|
422
|
+
onClick={() => setCurrentTab('mixer')}
|
|
423
|
+
>
|
|
424
|
+
🎛️ Audio
|
|
425
|
+
</button>
|
|
426
|
+
<button
|
|
427
|
+
className={`relative px-6 py-3 border-b-2 transition-colors font-medium flex items-center gap-2 ${
|
|
428
|
+
currentTab === 'effects'
|
|
429
|
+
? 'text-blue-600 dark:text-blue-400 border-blue-600 dark:border-blue-400 bg-gray-50 dark:bg-gray-900'
|
|
430
|
+
: 'text-gray-600 dark:text-gray-400 border-transparent hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
431
|
+
}`}
|
|
432
|
+
onClick={() => setCurrentTab('effects')}
|
|
433
|
+
>
|
|
434
|
+
✨ Effects
|
|
435
|
+
</button>
|
|
436
|
+
<button
|
|
437
|
+
className={`relative px-6 py-3 border-b-2 transition-colors font-medium flex items-center gap-2 ${
|
|
438
|
+
currentTab === 'requests'
|
|
439
|
+
? 'text-blue-600 dark:text-blue-400 border-blue-600 dark:border-blue-400 bg-gray-50 dark:bg-gray-900'
|
|
440
|
+
: 'text-gray-600 dark:text-gray-400 border-transparent hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
441
|
+
}`}
|
|
442
|
+
onClick={() => setCurrentTab('requests')}
|
|
443
|
+
>
|
|
444
|
+
🎤 Requests
|
|
445
|
+
{requests.filter((r) => r.status === 'pending').length > 0 && (
|
|
446
|
+
<span className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 bg-red-600 text-white rounded-full text-xs font-semibold">
|
|
447
|
+
{requests.filter((r) => r.status === 'pending').length}
|
|
448
|
+
</span>
|
|
449
|
+
)}
|
|
450
|
+
</button>
|
|
451
|
+
<button
|
|
452
|
+
className={`relative px-6 py-3 border-b-2 transition-colors font-medium flex items-center gap-2 ${
|
|
453
|
+
currentTab === 'editor'
|
|
454
|
+
? 'text-blue-600 dark:text-blue-400 border-blue-600 dark:border-blue-400 bg-gray-50 dark:bg-gray-900'
|
|
455
|
+
: 'text-gray-600 dark:text-gray-400 border-transparent hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
456
|
+
}`}
|
|
457
|
+
onClick={() => setCurrentTab('editor')}
|
|
458
|
+
>
|
|
459
|
+
✏️ Edit
|
|
460
|
+
</button>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<main className="flex-1 min-h-0 overflow-hidden flex flex-col">
|
|
464
|
+
<div
|
|
465
|
+
className={`${currentTab === 'queue' ? 'flex' : 'hidden'} flex-col h-full gap-4 p-4 overflow-auto`}
|
|
466
|
+
>
|
|
467
|
+
<div className="flex flex-col gap-4 h-full overflow-hidden">
|
|
468
|
+
<div className="w-full flex-shrink-0">
|
|
469
|
+
<PlayerControls
|
|
470
|
+
playback={playback}
|
|
471
|
+
currentSong={currentSong}
|
|
472
|
+
currentEffect={effects?.current}
|
|
473
|
+
onPlay={handlePlay}
|
|
474
|
+
onPause={handlePause}
|
|
475
|
+
onRestart={handleRestart}
|
|
476
|
+
onNext={handleNext}
|
|
477
|
+
onSeek={handleSeek}
|
|
478
|
+
onPreviousEffect={handleEffectPrevious}
|
|
479
|
+
onNextEffect={handleEffectNext}
|
|
480
|
+
/>
|
|
481
|
+
</div>
|
|
482
|
+
<div className="flex gap-4 flex-1 min-h-0 overflow-hidden">
|
|
483
|
+
<div className="w-[300px] flex-shrink-0 overflow-y-auto p-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
484
|
+
<VisualizationSettings
|
|
485
|
+
bridge={bridge}
|
|
486
|
+
waveformSettings={waveformSettings}
|
|
487
|
+
autotuneSettings={autotuneSettings}
|
|
488
|
+
onWaveformChange={setWaveformSettings}
|
|
489
|
+
onAutotuneChange={setAutotuneSettings}
|
|
490
|
+
/>
|
|
491
|
+
</div>
|
|
492
|
+
<div className="flex-1 flex flex-col min-w-0 overflow-auto">
|
|
493
|
+
<QuickSearch bridge={bridge} requester="Web Admin" />
|
|
494
|
+
|
|
495
|
+
<QueueList
|
|
496
|
+
queue={queue}
|
|
497
|
+
currentSongId={currentSong?.queueItemId ?? null}
|
|
498
|
+
onLoad={handlePlayFromQueue}
|
|
499
|
+
onRemove={handleRemoveFromQueue}
|
|
500
|
+
onClear={handleClearQueue}
|
|
501
|
+
onReorderQueue={handleReorderQueue}
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<div
|
|
509
|
+
className={`${currentTab === 'library' ? 'flex' : 'hidden'} flex-col h-full gap-4 p-4 overflow-auto`}
|
|
510
|
+
>
|
|
511
|
+
<LibraryPanel bridge={bridge} />
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<div
|
|
515
|
+
className={`${currentTab === 'mixer' ? 'flex' : 'hidden'} flex-col h-full gap-4 p-4 overflow-auto`}
|
|
516
|
+
>
|
|
517
|
+
<MixerPanel
|
|
518
|
+
mixer={mixer}
|
|
519
|
+
onGainChange={handleGainChange}
|
|
520
|
+
onMuteToggle={handleMuteToggle}
|
|
521
|
+
/>
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
<div
|
|
525
|
+
className={`${currentTab === 'effects' ? 'flex' : 'hidden'} flex-col h-full gap-4 p-4 overflow-auto`}
|
|
526
|
+
>
|
|
527
|
+
<EffectsPanel
|
|
528
|
+
effects={effects?.list || []}
|
|
529
|
+
currentEffect={effects?.current}
|
|
530
|
+
disabledEffects={effects?.disabled || []}
|
|
531
|
+
searchTerm={effectsSearch}
|
|
532
|
+
currentCategory={effectsCategory}
|
|
533
|
+
onSearch={setEffectsSearch}
|
|
534
|
+
onCategoryChange={setEffectsCategory}
|
|
535
|
+
onSelectEffect={handleEffectSelect}
|
|
536
|
+
onRandomEffect={handleEffectRandom}
|
|
537
|
+
onEnableEffect={(name) => handleEffectToggle(name, true)}
|
|
538
|
+
onDisableEffect={(name) => handleEffectToggle(name, false)}
|
|
539
|
+
/>
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
<div
|
|
543
|
+
className={`${currentTab === 'requests' ? 'flex' : 'hidden'} flex-col h-full gap-4 p-4 overflow-auto`}
|
|
544
|
+
>
|
|
545
|
+
<RequestsList
|
|
546
|
+
requests={requests}
|
|
547
|
+
onApprove={async (requestId) => {
|
|
548
|
+
try {
|
|
549
|
+
await fetch(`/admin/requests/${requestId}/approve`, {
|
|
550
|
+
method: 'POST',
|
|
551
|
+
credentials: 'include',
|
|
552
|
+
});
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.error('Approve failed:', err);
|
|
555
|
+
}
|
|
556
|
+
}}
|
|
557
|
+
onReject={async (requestId) => {
|
|
558
|
+
try {
|
|
559
|
+
await fetch(`/admin/requests/${requestId}/reject`, {
|
|
560
|
+
method: 'POST',
|
|
561
|
+
credentials: 'include',
|
|
562
|
+
});
|
|
563
|
+
} catch (err) {
|
|
564
|
+
console.error('Reject failed:', err);
|
|
565
|
+
}
|
|
566
|
+
}}
|
|
567
|
+
/>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
<div
|
|
571
|
+
className={`${currentTab === 'editor' ? 'flex' : 'hidden'} flex-col h-full gap-4 p-4 overflow-auto`}
|
|
572
|
+
>
|
|
573
|
+
<SongEditor bridge={bridge} />
|
|
574
|
+
</div>
|
|
575
|
+
</main>
|
|
576
|
+
</div>
|
|
577
|
+
);
|
|
578
|
+
}
|