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.
Files changed (290) hide show
  1. package/README.md +558 -0
  2. package/bin/loukai.js +32 -0
  3. package/package.json +243 -0
  4. package/src/main/appState.js +250 -0
  5. package/src/main/audioEngine.js +478 -0
  6. package/src/main/creator/conversionService.js +503 -0
  7. package/src/main/creator/downloadManager.js +1128 -0
  8. package/src/main/creator/ffmpegService.js +487 -0
  9. package/src/main/creator/installLogger.js +51 -0
  10. package/src/main/creator/keyDetection.js +212 -0
  11. package/src/main/creator/llmService.js +370 -0
  12. package/src/main/creator/lrclibService.js +340 -0
  13. package/src/main/creator/python/crepe_runner.py +189 -0
  14. package/src/main/creator/python/demucs_runner.py +158 -0
  15. package/src/main/creator/python/whisper_runner.py +172 -0
  16. package/src/main/creator/pythonRunner.js +268 -0
  17. package/src/main/creator/stemBuilder.js +491 -0
  18. package/src/main/creator/systemChecker.js +474 -0
  19. package/src/main/handlers/appHandlers.js +45 -0
  20. package/src/main/handlers/audioHandlers.js +33 -0
  21. package/src/main/handlers/autotuneHandlers.js +28 -0
  22. package/src/main/handlers/canvasHandlers.js +84 -0
  23. package/src/main/handlers/creatorHandlers.js +159 -0
  24. package/src/main/handlers/editorHandlers.js +98 -0
  25. package/src/main/handlers/effectsHandlers.js +100 -0
  26. package/src/main/handlers/fileHandlers.js +45 -0
  27. package/src/main/handlers/index.js +78 -0
  28. package/src/main/handlers/libraryHandlers.js +96 -0
  29. package/src/main/handlers/mixerHandlers.js +64 -0
  30. package/src/main/handlers/playerHandlers.js +39 -0
  31. package/src/main/handlers/preferencesHandlers.js +46 -0
  32. package/src/main/handlers/queueHandlers.js +81 -0
  33. package/src/main/handlers/rendererHandlers.js +63 -0
  34. package/src/main/handlers/settingsHandlers.js +42 -0
  35. package/src/main/handlers/webServerHandlers.js +105 -0
  36. package/src/main/main.js +2351 -0
  37. package/src/main/preload.js +252 -0
  38. package/src/main/settingsManager.js +139 -0
  39. package/src/main/statePersistence.js +193 -0
  40. package/src/main/utils/pathValidator.js +112 -0
  41. package/src/main/webServer.js +2535 -0
  42. package/src/native/autotune.js +417 -0
  43. package/src/renderer/adapters/ElectronBridge.js +677 -0
  44. package/src/renderer/canvas.html +80 -0
  45. package/src/renderer/components/App.jsx +303 -0
  46. package/src/renderer/components/AppRoot.jsx +37 -0
  47. package/src/renderer/components/AudioDeviceSettings.jsx +145 -0
  48. package/src/renderer/components/EffectsPanelWrapper.jsx +267 -0
  49. package/src/renderer/components/MixerTab.jsx +233 -0
  50. package/src/renderer/components/MixerTabWrapper.jsx +31 -0
  51. package/src/renderer/components/PortalSelect.jsx +239 -0
  52. package/src/renderer/components/QueueTab.jsx +116 -0
  53. package/src/renderer/components/RequestsListWrapper.jsx +78 -0
  54. package/src/renderer/components/ServerTab.jsx +472 -0
  55. package/src/renderer/components/SongInfoBarWrapper.jsx +77 -0
  56. package/src/renderer/components/StatusBar.jsx +92 -0
  57. package/src/renderer/components/TabNavigation.jsx +77 -0
  58. package/src/renderer/components/TransportControlsWrapper.jsx +69 -0
  59. package/src/renderer/components/creator/CreateTab.jsx +1236 -0
  60. package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js +2 -0
  61. package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +1 -0
  62. package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js +2 -0
  63. package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js.map +1 -0
  64. package/src/renderer/dist/assets/player-DVrqp7N5.js +3 -0
  65. package/src/renderer/dist/assets/player-DVrqp7N5.js.map +1 -0
  66. package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +2 -0
  67. package/src/renderer/dist/assets/songLoaders-BaTgGib4.js.map +1 -0
  68. package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js +2 -0
  69. package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js.map +1 -0
  70. package/src/renderer/dist/js/autoTuneWorklet.js +224 -0
  71. package/src/renderer/dist/js/micPitchDetectorWorklet.js +137 -0
  72. package/src/renderer/dist/js/musicAnalysisWorklet.js +216 -0
  73. package/src/renderer/dist/js/phaseVocoderWorklet.js +341 -0
  74. package/src/renderer/dist/js/soundtouch-worklet.js +1395 -0
  75. package/src/renderer/dist/renderer.css +1 -0
  76. package/src/renderer/dist/renderer.js +62 -0
  77. package/src/renderer/dist/renderer.js.map +1 -0
  78. package/src/renderer/dist/renderer.woff2 +0 -0
  79. package/src/renderer/hooks/useKeyboardShortcuts.js +154 -0
  80. package/src/renderer/index.html +24 -0
  81. package/src/renderer/index.html.backup +372 -0
  82. package/src/renderer/js/PlayerInterface.js +267 -0
  83. package/src/renderer/js/autoTuneWorklet.js +224 -0
  84. package/src/renderer/js/butterchurnVerify.js +46 -0
  85. package/src/renderer/js/canvas-app.js +114 -0
  86. package/src/renderer/js/cdgPlayer.js +685 -0
  87. package/src/renderer/js/kaiPlayer.js +1200 -0
  88. package/src/renderer/js/karaokeRenderer.js +3392 -0
  89. package/src/renderer/js/micPitchDetectorWorklet.js +137 -0
  90. package/src/renderer/js/microphoneEngine.js +656 -0
  91. package/src/renderer/js/musicAnalysisWorklet.js +216 -0
  92. package/src/renderer/js/phaseVocoderWorklet.js +341 -0
  93. package/src/renderer/js/player.js +232 -0
  94. package/src/renderer/js/referencePitchTracker.js +130 -0
  95. package/src/renderer/js/songLoaders.js +334 -0
  96. package/src/renderer/js/soundtouch-worklet.js +1395 -0
  97. package/src/renderer/js/webrtcManager.js +511 -0
  98. package/src/renderer/lib/butterchurn.min.js +6739 -0
  99. package/src/renderer/lib/butterchurnPresets.min.js +1 -0
  100. package/src/renderer/lib/cdgraphics-wrapper.js +16 -0
  101. package/src/renderer/lib/cdgraphics.js +299 -0
  102. package/src/renderer/public/js/autoTuneWorklet.js +224 -0
  103. package/src/renderer/public/js/micPitchDetectorWorklet.js +137 -0
  104. package/src/renderer/public/js/musicAnalysisWorklet.js +216 -0
  105. package/src/renderer/public/js/phaseVocoderWorklet.js +341 -0
  106. package/src/renderer/public/js/soundtouch-worklet.js +1395 -0
  107. package/src/renderer/react-entry.jsx +44 -0
  108. package/src/renderer/styles/tailwind.css +106 -0
  109. package/src/renderer/utils/qrCodeGenerator.js +98 -0
  110. package/src/renderer/vite.config.js +31 -0
  111. package/src/shared/adapters/BridgeInterface.js +195 -0
  112. package/src/shared/components/EffectsPanel.jsx +177 -0
  113. package/src/shared/components/LibraryPanel.jsx +701 -0
  114. package/src/shared/components/LineDetailCanvas.jsx +167 -0
  115. package/src/shared/components/LyricLine.jsx +505 -0
  116. package/src/shared/components/LyricRejection.jsx +84 -0
  117. package/src/shared/components/LyricSuggestion.jsx +80 -0
  118. package/src/shared/components/LyricsEditorCanvas.jsx +271 -0
  119. package/src/shared/components/MixerPanel.jsx +94 -0
  120. package/src/shared/components/PlayerControls.jsx +206 -0
  121. package/src/shared/components/PortalSelect.jsx +239 -0
  122. package/src/shared/components/QueueList.jsx +365 -0
  123. package/src/shared/components/QuickSearch.jsx +126 -0
  124. package/src/shared/components/RequestsList.jsx +121 -0
  125. package/src/shared/components/SongEditor.jsx +1362 -0
  126. package/src/shared/components/SongInfoBar.jsx +81 -0
  127. package/src/shared/components/ThemeToggle.jsx +106 -0
  128. package/src/shared/components/Toast.jsx +30 -0
  129. package/src/shared/components/VisualizationSettings.jsx +243 -0
  130. package/src/shared/constants.js +95 -0
  131. package/src/shared/context/BridgeContext.jsx +32 -0
  132. package/src/shared/contexts/AudioContext.jsx +37 -0
  133. package/src/shared/contexts/PlayerContext.jsx +66 -0
  134. package/src/shared/contexts/SettingsContext.jsx +50 -0
  135. package/src/shared/defaults.js +158 -0
  136. package/src/shared/formatUtils.js +59 -0
  137. package/src/shared/formatUtils.test.js +207 -0
  138. package/src/shared/hooks/useAppState.js +97 -0
  139. package/src/shared/hooks/useAudioEngine.js +264 -0
  140. package/src/shared/hooks/usePlayer.js +89 -0
  141. package/src/shared/hooks/useSettingsPersistence.js +74 -0
  142. package/src/shared/hooks/useWebRTC.js +118 -0
  143. package/src/shared/ipcContracts.js +299 -0
  144. package/src/shared/package.json +3 -0
  145. package/src/shared/services/creatorService.js +373 -0
  146. package/src/shared/services/creatorService.test.js +413 -0
  147. package/src/shared/services/editorService.js +213 -0
  148. package/src/shared/services/editorService.test.js +219 -0
  149. package/src/shared/services/effectsService.js +271 -0
  150. package/src/shared/services/effectsService.test.js +418 -0
  151. package/src/shared/services/libraryService.js +438 -0
  152. package/src/shared/services/libraryService.test.js +474 -0
  153. package/src/shared/services/mixerService.js +172 -0
  154. package/src/shared/services/mixerService.test.js +399 -0
  155. package/src/shared/services/playerService.js +221 -0
  156. package/src/shared/services/playerService.test.js +357 -0
  157. package/src/shared/services/preferencesService.js +219 -0
  158. package/src/shared/services/queueService.js +226 -0
  159. package/src/shared/services/queueService.test.js +430 -0
  160. package/src/shared/services/requestsService.js +155 -0
  161. package/src/shared/services/requestsService.test.js +362 -0
  162. package/src/shared/services/serverSettingsService.js +151 -0
  163. package/src/shared/services/settingsService.js +257 -0
  164. package/src/shared/services/settingsService.test.js +295 -0
  165. package/src/shared/state/StateManager.js +263 -0
  166. package/src/shared/utils/audio.js +42 -0
  167. package/src/shared/utils/format.js +32 -0
  168. package/src/shared/utils/lyricsUtils.js +162 -0
  169. package/src/test/setup.js +40 -0
  170. package/src/utils/cdgLoader.js +180 -0
  171. package/src/utils/m4aLoader.js +333 -0
  172. package/src/web/App.jsx +578 -0
  173. package/src/web/adapters/WebBridge.js +428 -0
  174. package/src/web/components/PlayerSettingsPanel.jsx +231 -0
  175. package/src/web/components/SongSearch.jsx +180 -0
  176. package/src/web/dist/assets/index-0H-RnRrV.js +51 -0
  177. package/src/web/dist/assets/index-0H-RnRrV.js.map +1 -0
  178. package/src/web/dist/assets/index-DYW2zB0u.css +1 -0
  179. package/src/web/dist/index.html +15 -0
  180. package/src/web/index.html +14 -0
  181. package/src/web/main.jsx +10 -0
  182. package/src/web/package-lock.json +1765 -0
  183. package/src/web/pages/SongRequestPage.jsx +619 -0
  184. package/src/web/styles/tailwind.css +68 -0
  185. package/src/web/vite.config.js +27 -0
  186. package/static/fonts/material-icons.woff2 +0 -0
  187. package/static/images/butterchurn-screenshots/Aderrasi - Potion of Spirits.png +0 -0
  188. package/static/images/butterchurn-screenshots/Aderrasi - Songflower _Moss Posy_.png +0 -0
  189. package/static/images/butterchurn-screenshots/Aderrasi - Storm of the Eye _Thunder_ - mash0000 - quasi pseudo meta concentrics.png +0 -0
  190. package/static/images/butterchurn-screenshots/Aderrasi _ Geiss - Airhandler _Kali Mix_ - Canvas Mix.png +0 -0
  191. 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
  192. package/static/images/butterchurn-screenshots/Cope - The Neverending Explosion of Red Liquid Fire.png +0 -0
  193. proton lights __Krash_s beat code_ _Phat_remix02b.png +0 -0
  194. package/static/images/butterchurn-screenshots/Eo_S_ _ Phat - cubetrace - v2.png +0 -0
  195. package/static/images/butterchurn-screenshots/Eo_S_ _ Zylot - skylight _Stained Glass Majesty mix_.png +0 -0
  196. package/static/images/butterchurn-screenshots/Flexi - alien fish pond.png +0 -0
  197. package/static/images/butterchurn-screenshots/Flexi - area 51.png +0 -0
  198. package/static/images/butterchurn-screenshots/Flexi - infused with the spiral.png +0 -0
  199. package/static/images/butterchurn-screenshots/Flexi - mindblob _shiny mix_.png +0 -0
  200. package/static/images/butterchurn-screenshots/Flexi - mindblob mix.png +0 -0
  201. package/static/images/butterchurn-screenshots/Flexi - predator-prey-spirals.png +0 -0
  202. package/static/images/butterchurn-screenshots/Flexi - smashing fractals _acid etching mix_.png +0 -0
  203. package/static/images/butterchurn-screenshots/Flexi - truly soft piece of software - this is generic texturing _Jelly_ .png +0 -0
  204. package/static/images/butterchurn-screenshots/Flexi _ Martin - astral projection.png +0 -0
  205. package/static/images/butterchurn-screenshots/Flexi _ Martin - cascading decay swing.png +0 -0
  206. package/static/images/butterchurn-screenshots/Flexi _ amandio c - piercing 05 - Kopie _2_ - Kopie.png +0 -0
  207. package/static/images/butterchurn-screenshots/Flexi _ stahlregen - jelly showoff parade.png +0 -0
  208. package/static/images/butterchurn-screenshots/Flexi_ fishbrain_ Geiss _ Martin - tokamak witchery.png +0 -0
  209. package/static/images/butterchurn-screenshots/Flexi_ martin _ geiss - dedicated to the sherwin maxawow.png +0 -0
  210. package/static/images/butterchurn-screenshots/Fumbling_Foo _ Flexi_ Martin_ Orb_ Unchained - Star Nova v7b.png +0 -0
  211. package/static/images/butterchurn-screenshots/Geiss - Cauldron - painterly 2 _saturation remix_.png +0 -0
  212. package/static/images/butterchurn-screenshots/Geiss - Reaction Diffusion 2.png +0 -0
  213. package/static/images/butterchurn-screenshots/Geiss - Spiral Artifact.png +0 -0
  214. package/static/images/butterchurn-screenshots/Geiss - Thumb Drum.png +0 -0
  215. package/static/images/butterchurn-screenshots/Geiss _ Flexi _ Martin - disconnected.png +0 -0
  216. package/static/images/butterchurn-screenshots/Geiss_ Flexi _ Stahlregen - Thumbdrum Tokamak _crossfiring aftermath jelly mashup_.png +0 -0
  217. package/static/images/butterchurn-screenshots/Goody - The Wild Vort.png +0 -0
  218. package/static/images/butterchurn-screenshots/Idiot - Star Of Annon.png +0 -0
  219. package/static/images/butterchurn-screenshots/Krash _ Illusion - Spiral Movement.png +0 -0
  220. package/static/images/butterchurn-screenshots/Martin - QBikal - Surface Turbulence IIb.png +0 -0
  221. package/static/images/butterchurn-screenshots/Martin - acid wiring.png +0 -0
  222. package/static/images/butterchurn-screenshots/Martin - charisma.png +0 -0
  223. package/static/images/butterchurn-screenshots/Martin - liquid arrows.png +0 -0
  224. package/static/images/butterchurn-screenshots/Milk Artist At our Best - FED - SlowFast Ft AdamFX n Martin - HD CosmoFX.png +0 -0
  225. package/static/images/butterchurn-screenshots/ORB - Waaa.png +0 -0
  226. package/static/images/butterchurn-screenshots/Phat_fiShbRaiN_Eo_S_Mandala_Chasers_remix.png +0 -0
  227. package/static/images/butterchurn-screenshots/Rovastar - Oozing Resistance.png +0 -0
  228. package/static/images/butterchurn-screenshots/Rovastar _ Loadus _ Geiss - FractalDrop _Triple Mix_.png +0 -0
  229. package/static/images/butterchurn-screenshots/TonyMilkdrop - Leonardo Da Vinci_s Balloon _Flexi - merry-go-round _ techstyle_.png +0 -0
  230. package/static/images/butterchurn-screenshots/TonyMilkdrop - Magellan_s Nebula _Flexi - you enter first _ multiverse_.png +0 -0
  231. package/static/images/butterchurn-screenshots/Unchained - Rewop.png +0 -0
  232. package/static/images/butterchurn-screenshots/Unchained - Unified Drag 2.png +0 -0
  233. package/static/images/butterchurn-screenshots/Unchained _ Rovastar - Wormhole Pillars _Hall of Shadows mix_.png +0 -0
  234. package/static/images/butterchurn-screenshots/Zylot - Paint Spill _Music Reactive Paint Mix_.png +0 -0
  235. package/static/images/butterchurn-screenshots/Zylot - Star Ornament.png +0 -0
  236. package/static/images/butterchurn-screenshots/Zylot - True Visionary _Final Mix_.png +0 -0
  237. package/static/images/butterchurn-screenshots/_Aderrasi - Wanderer in Curved Space - mash0000 - faclempt kibitzing meshuggana schmaltz _Geiss color mix_.png +0 -0
  238. package/static/images/butterchurn-screenshots/_Geiss - Artifact 01.png +0 -0
  239. package/static/images/butterchurn-screenshots/_Geiss - Desert Rose 2.png +0 -0
  240. package/static/images/butterchurn-screenshots/_Geiss - untitled.png +0 -0
  241. package/static/images/butterchurn-screenshots/_Mig_049.png +0 -0
  242. package/static/images/butterchurn-screenshots/_Mig_085.png +0 -0
  243. package/static/images/butterchurn-screenshots/_Rovastar _ Geiss - Hurricane Nightmare _Posterize Mix_.png +0 -0
  244. package/static/images/butterchurn-screenshots/___ Royal - Mashup _197_.png +0 -0
  245. package/static/images/butterchurn-screenshots/___ Royal - Mashup _220_.png +0 -0
  246. package/static/images/butterchurn-screenshots/___ Royal - Mashup _431_.png +0 -0
  247. package/static/images/butterchurn-screenshots/cope _ martin - mother-of-pearl.png +0 -0
  248. package/static/images/butterchurn-screenshots/fiShbRaiN _ Flexi - witchcraft 2_0.png +0 -0
  249. package/static/images/butterchurn-screenshots/flexi - bouncing balls _double mindblob neon mix_.png +0 -0
  250. package/static/images/butterchurn-screenshots/flexi - mom_ why the sky looks different today.png +0 -0
  251. package/static/images/butterchurn-screenshots/flexi - patternton_ district of media_ capitol of the united abstractions of fractopia.png +0 -0
  252. package/static/images/butterchurn-screenshots/flexi - swing out on the spiral.png +0 -0
  253. package/static/images/butterchurn-screenshots/flexi - what is the matrix.png +0 -0
  254. package/static/images/butterchurn-screenshots/flexi _ amandio c - organic _random mashup_.png +0 -0
  255. package/static/images/butterchurn-screenshots/flexi _ amandio c - organic12-3d-2_milk.png +0 -0
  256. package/static/images/butterchurn-screenshots/flexi _ fishbrain - neon mindblob grafitti.png +0 -0
  257. package/static/images/butterchurn-screenshots/flexi _ geiss - pogo cubes vs_ tokamak vs_ game of life _stahls jelly 4_5 finish_.png +0 -0
  258. package/static/images/butterchurn-screenshots/high-altitude basket unraveling - singh grooves nitrogen argon nz_.png +0 -0
  259. package/static/images/butterchurn-screenshots/martin - The Bridge of Khazad-Dum.png +0 -0
  260. package/static/images/butterchurn-screenshots/martin - angel flight.png +0 -0
  261. package/static/images/butterchurn-screenshots/martin - another kind of groove.png +0 -0
  262. package/static/images/butterchurn-screenshots/martin - bombyx mori.png +0 -0
  263. package/static/images/butterchurn-screenshots/martin - castle in the air.png +0 -0
  264. package/static/images/butterchurn-screenshots/martin - chain breaker.png +0 -0
  265. package/static/images/butterchurn-screenshots/martin - disco mix 4.png +0 -0
  266. package/static/images/butterchurn-screenshots/martin - extreme heat.png +0 -0
  267. package/static/images/butterchurn-screenshots/martin - frosty caves 2.png +0 -0
  268. package/static/images/butterchurn-screenshots/martin - fruit machine.png +0 -0
  269. package/static/images/butterchurn-screenshots/martin - ghost city.png +0 -0
  270. package/static/images/butterchurn-screenshots/martin - glass corridor.png +0 -0
  271. package/static/images/butterchurn-screenshots/martin - infinity _2010 update_.png +0 -0
  272. package/static/images/butterchurn-screenshots/martin - mandelbox explorer - high speed demo version.png +0 -0
  273. package/static/images/butterchurn-screenshots/martin - mucus cervix.png +0 -0
  274. package/static/images/butterchurn-screenshots/martin - reflections on black tiles.png +0 -0
  275. package/static/images/butterchurn-screenshots/martin - stormy sea _2010 update_.png +0 -0
  276. package/static/images/butterchurn-screenshots/martin - witchcraft reloaded.png +0 -0
  277. package/static/images/butterchurn-screenshots/martin _ flexi - diamond cutter _prismaticvortex_com_ - camille - i wish i wish i wish i was constrained.png +0 -0
  278. package/static/images/butterchurn-screenshots/martin _shadow harlequins shape code_ - fata morgana.png +0 -0
  279. package/static/images/butterchurn-screenshots/martin_ flexi_ fishbrain _ sto - enterstate _random mashup_.png +0 -0
  280. package/static/images/butterchurn-screenshots/sawtooth grin roam.png +0 -0
  281. package/static/images/butterchurn-screenshots/shifter - dark tides bdrv mix 2.png +0 -0
  282. 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
  283. package/static/images/butterchurn-screenshots/suksma - heretical crosscut playpen.png +0 -0
  284. package/static/images/butterchurn-screenshots/suksma - uninitialized variabowl _hydroponic chronic_.png +0 -0
  285. package/static/images/butterchurn-screenshots/suksma - vector exp 1 - couldn_t not.png +0 -0
  286. package/static/images/butterchurn-screenshots/yin - 191 - Temporal singularities.png +0 -0
  287. package/static/images/logo-512.png +0 -0
  288. package/static/images/logo.png +0 -0
  289. package/static/loukai-logo.png +0 -0
  290. package/static/screenshot-generator.html +610 -0
@@ -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
+ }