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,44 @@
1
+ /**
2
+ * React Entry Point for Electron Renderer
3
+ *
4
+ * Single entry point - mounts ONE React app with shared context
5
+ */
6
+
7
+ import React from 'react';
8
+ import ReactDOM from 'react-dom/client';
9
+ import './styles/tailwind.css';
10
+ import { ElectronBridge } from './adapters/ElectronBridge.js';
11
+ import { AppRoot } from './components/AppRoot.jsx';
12
+ import { App } from './components/App.jsx';
13
+ import { verifyButterchurn } from './js/butterchurnVerify.js';
14
+
15
+ console.log('🚀 Initializing application...');
16
+
17
+ // Verify Butterchurn libraries loaded correctly
18
+ verifyButterchurn();
19
+
20
+ // Get the ElectronBridge singleton instance
21
+ const bridge = ElectronBridge.getInstance();
22
+
23
+ // Connect bridge and mount React app
24
+ bridge.connect().then(() => {
25
+ console.log('✅ ElectronBridge connected');
26
+
27
+ // Mount single React app to root
28
+ const root = document.getElementById('root');
29
+ if (root) {
30
+ ReactDOM.createRoot(root).render(
31
+ <React.StrictMode>
32
+ <AppRoot>
33
+ <App bridge={bridge} />
34
+ </AppRoot>
35
+ </React.StrictMode>
36
+ );
37
+ console.log('✅ React app mounted');
38
+ }
39
+ });
40
+
41
+ // Cleanup on window unload
42
+ window.addEventListener('beforeunload', () => {
43
+ bridge.disconnect();
44
+ });
@@ -0,0 +1,106 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Custom scrollbar for dark mode */
6
+ @layer base {
7
+ :root {
8
+ color-scheme: light dark;
9
+ }
10
+
11
+ * {
12
+ @apply border-gray-200 dark:border-gray-700;
13
+ }
14
+
15
+ body {
16
+ @apply bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100;
17
+ margin: 0;
18
+ padding: 0;
19
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
20
+ overflow: hidden;
21
+ }
22
+
23
+ /* Custom scrollbar */
24
+ ::-webkit-scrollbar {
25
+ @apply w-2;
26
+ }
27
+
28
+ ::-webkit-scrollbar-track {
29
+ @apply bg-gray-100 dark:bg-gray-800;
30
+ }
31
+
32
+ ::-webkit-scrollbar-thumb {
33
+ @apply bg-gray-300 dark:bg-gray-600 rounded-full;
34
+ }
35
+
36
+ ::-webkit-scrollbar-thumb:hover {
37
+ @apply bg-gray-400 dark:bg-gray-500;
38
+ }
39
+
40
+ /* Fullscreen canvas styling */
41
+ #karaokeCanvas:fullscreen {
42
+ width: 100vw;
43
+ height: 100vh;
44
+ object-fit: contain;
45
+ background: #000;
46
+ }
47
+
48
+ /* Webkit prefix for Safari */
49
+ #karaokeCanvas:-webkit-full-screen {
50
+ width: 100vw;
51
+ height: 100vh;
52
+ object-fit: contain;
53
+ background: #000;
54
+ }
55
+
56
+ /* Firefox prefix */
57
+ #karaokeCanvas:-moz-full-screen {
58
+ width: 100vw;
59
+ height: 100vh;
60
+ object-fit: contain;
61
+ background: #000;
62
+ }
63
+ }
64
+
65
+ /* Utility classes */
66
+ @layer components {
67
+ .btn-primary {
68
+ @apply px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors;
69
+ }
70
+
71
+ .btn-secondary {
72
+ @apply px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100 rounded-lg font-medium transition-colors;
73
+ }
74
+
75
+ .input {
76
+ @apply px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500;
77
+ }
78
+
79
+ .card {
80
+ @apply bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm;
81
+ }
82
+ }
83
+
84
+ /* Material Icons font */
85
+ @font-face {
86
+ font-family: 'Material Icons';
87
+ font-style: normal;
88
+ font-weight: 400;
89
+ src: url('../../../static/fonts/material-icons.woff2') format('woff2');
90
+ }
91
+
92
+ .material-icons {
93
+ font-family: 'Material Icons';
94
+ font-weight: normal;
95
+ font-style: normal;
96
+ font-size: 24px;
97
+ line-height: 1;
98
+ letter-spacing: normal;
99
+ text-transform: none;
100
+ display: inline-block;
101
+ white-space: nowrap;
102
+ word-wrap: normal;
103
+ direction: ltr;
104
+ -webkit-font-feature-settings: 'liga';
105
+ -webkit-font-smoothing: antialiased;
106
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * QR Code Generator Utility
3
+ * Generates QR codes to canvas for server URLs
4
+ */
5
+
6
+ import QRCode from 'qrcode';
7
+
8
+ /**
9
+ * Generate QR code data URL
10
+ * @param {string} text - Text to encode (server URL)
11
+ * @param {Object} options - QR code options
12
+ * @returns {Promise<string>} Data URL of QR code image
13
+ */
14
+ export async function generateQRCode(text, options = {}) {
15
+ const defaultOptions = {
16
+ width: 200,
17
+ margin: 2,
18
+ color: {
19
+ dark: '#000000',
20
+ light: '#FFFFFF',
21
+ },
22
+ errorCorrectionLevel: 'M',
23
+ ...options,
24
+ };
25
+
26
+ try {
27
+ const dataUrl = await QRCode.toDataURL(text, defaultOptions);
28
+ return dataUrl;
29
+ } catch (error) {
30
+ console.error('Error generating QR code:', error);
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Generate QR code to an offscreen canvas
37
+ * @param {string} text - Text to encode (server URL)
38
+ * @param {number} size - Size of QR code in pixels
39
+ * @returns {Promise<HTMLCanvasElement>} Canvas with QR code
40
+ */
41
+ export async function generateQRCodeCanvas(text, size = 200) {
42
+ const canvas = document.createElement('canvas');
43
+ canvas.width = size;
44
+ canvas.height = size;
45
+
46
+ try {
47
+ await QRCode.toCanvas(canvas, text, {
48
+ width: size,
49
+ margin: 2,
50
+ color: {
51
+ dark: '#000000',
52
+ light: '#FFFFFF',
53
+ },
54
+ errorCorrectionLevel: 'M',
55
+ });
56
+ return canvas;
57
+ } catch (error) {
58
+ console.error('Error generating QR code canvas:', error);
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Generate QR code with label overlay
65
+ * @param {string} text - Text to encode (server URL)
66
+ * @param {string} label - Label text to display below QR code
67
+ * @param {number} qrSize - Size of QR code
68
+ * @returns {Promise<HTMLCanvasElement>} Canvas with QR code and label
69
+ */
70
+ export async function generateQRCodeWithLabel(text, label, qrSize = 200) {
71
+ // Generate QR code
72
+ const qrCanvas = await generateQRCodeCanvas(text, qrSize);
73
+ if (!qrCanvas) return null;
74
+
75
+ // Create final canvas with extra space for label
76
+ const padding = 20;
77
+ const labelHeight = 40;
78
+ const finalCanvas = document.createElement('canvas');
79
+ finalCanvas.width = qrSize + padding * 2;
80
+ finalCanvas.height = qrSize + labelHeight + padding * 2;
81
+
82
+ const ctx = finalCanvas.getContext('2d');
83
+
84
+ // White background
85
+ ctx.fillStyle = '#FFFFFF';
86
+ ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
87
+
88
+ // Draw QR code
89
+ ctx.drawImage(qrCanvas, padding, padding);
90
+
91
+ // Draw label
92
+ ctx.fillStyle = '#000000';
93
+ ctx.font = 'bold 16px Arial';
94
+ ctx.textAlign = 'center';
95
+ ctx.fillText(label, finalCanvas.width / 2, qrSize + padding + 25);
96
+
97
+ return finalCanvas;
98
+ }
@@ -0,0 +1,31 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ export default defineConfig({
9
+ plugins: [react()],
10
+ root: __dirname,
11
+ base: './', // Use relative paths for Electron
12
+ build: {
13
+ outDir: 'dist',
14
+ emptyOutDir: true,
15
+ sourcemap: true,
16
+ rollupOptions: {
17
+ input: path.resolve(__dirname, 'react-entry.jsx'),
18
+ output: {
19
+ entryFileNames: 'renderer.js',
20
+ assetFileNames: 'renderer.[ext]',
21
+ chunkFileNames: 'assets/[name]-[hash].js',
22
+ },
23
+ },
24
+ },
25
+ resolve: {
26
+ alias: {
27
+ '@shared': path.resolve(__dirname, '../shared'),
28
+ '@renderer': path.resolve(__dirname, '.'),
29
+ },
30
+ },
31
+ });
@@ -0,0 +1,195 @@
1
+ /**
2
+ * BridgeInterface - Abstract base class for platform-specific communication
3
+ *
4
+ * This defines the contract that both ElectronBridge and WebBridge must implement.
5
+ * Components use this interface and don't care about the underlying transport.
6
+ *
7
+ * Platform-specific implementations:
8
+ * - ElectronBridge: Uses window.kaiAPI (IPC to main process)
9
+ * - WebBridge: Uses fetch() and Socket.IO (REST + WebSocket)
10
+ */
11
+
12
+ export class BridgeInterface {
13
+ // ===== Player Controls =====
14
+
15
+ play() {
16
+ return Promise.reject(new Error('play() not implemented'));
17
+ }
18
+
19
+ pause() {
20
+ return Promise.reject(new Error('pause() not implemented'));
21
+ }
22
+
23
+ restart() {
24
+ return Promise.reject(new Error('restart() not implemented'));
25
+ }
26
+
27
+ seek(_positionSec) {
28
+ return Promise.reject(new Error('seek() not implemented'));
29
+ }
30
+
31
+ getPlaybackState() {
32
+ return Promise.reject(new Error('getPlaybackState() not implemented'));
33
+ }
34
+
35
+ // ===== Queue Management =====
36
+
37
+ getQueue() {
38
+ return Promise.reject(new Error('getQueue() not implemented'));
39
+ }
40
+
41
+ addToQueue(_song) {
42
+ return Promise.reject(new Error('addToQueue() not implemented'));
43
+ }
44
+
45
+ removeFromQueue(_id) {
46
+ return Promise.reject(new Error('removeFromQueue() not implemented'));
47
+ }
48
+
49
+ clearQueue() {
50
+ return Promise.reject(new Error('clearQueue() not implemented'));
51
+ }
52
+
53
+ reorderQueue(_fromIndex, _toIndex) {
54
+ return Promise.reject(new Error('reorderQueue() not implemented'));
55
+ }
56
+
57
+ playNext() {
58
+ return Promise.reject(new Error('playNext() not implemented'));
59
+ }
60
+
61
+ // ===== Mixer Controls =====
62
+
63
+ getMixerState() {
64
+ return Promise.reject(new Error('getMixerState() not implemented'));
65
+ }
66
+
67
+ setMasterGain(_bus, _gainDb) {
68
+ return Promise.reject(new Error('setMasterGain() not implemented'));
69
+ }
70
+
71
+ toggleMasterMute(_bus) {
72
+ return Promise.reject(new Error('toggleMasterMute() not implemented'));
73
+ }
74
+
75
+ setMasterMute(_bus, _muted) {
76
+ return Promise.reject(new Error('setMasterMute() not implemented'));
77
+ }
78
+
79
+ // ===== Effects Controls =====
80
+
81
+ getEffects() {
82
+ return Promise.reject(new Error('getEffects() not implemented'));
83
+ }
84
+
85
+ selectEffect(_effectName) {
86
+ return Promise.reject(new Error('selectEffect() not implemented'));
87
+ }
88
+
89
+ toggleEffect(_effectName, _enabled) {
90
+ return Promise.reject(new Error('toggleEffect() not implemented'));
91
+ }
92
+
93
+ nextEffect() {
94
+ return Promise.reject(new Error('nextEffect() not implemented'));
95
+ }
96
+
97
+ previousEffect() {
98
+ return Promise.reject(new Error('previousEffect() not implemented'));
99
+ }
100
+
101
+ randomEffect() {
102
+ return Promise.reject(new Error('randomEffect() not implemented'));
103
+ }
104
+
105
+ // ===== Library Management =====
106
+
107
+ getLibrary() {
108
+ return Promise.reject(new Error('getLibrary() not implemented'));
109
+ }
110
+
111
+ scanLibrary() {
112
+ return Promise.reject(new Error('scanLibrary() not implemented'));
113
+ }
114
+
115
+ searchSongs(_query) {
116
+ return Promise.reject(new Error('searchSongs() not implemented'));
117
+ }
118
+
119
+ loadSongForEditing(_path) {
120
+ return Promise.reject(new Error('loadSongForEditing() not implemented'));
121
+ }
122
+
123
+ saveSongEdits(_updates) {
124
+ return Promise.reject(new Error('saveSongEdits() not implemented'));
125
+ }
126
+
127
+ // ===== Preferences =====
128
+
129
+ getPreferences() {
130
+ return Promise.reject(new Error('getPreferences() not implemented'));
131
+ }
132
+
133
+ updateAutoTunePreferences(_prefs) {
134
+ return Promise.reject(new Error('updateAutoTunePreferences() not implemented'));
135
+ }
136
+
137
+ updateMicrophonePreferences(_prefs) {
138
+ return Promise.reject(new Error('updateMicrophonePreferences() not implemented'));
139
+ }
140
+
141
+ updateEffectsPreferences(_prefs) {
142
+ return Promise.reject(new Error('updateEffectsPreferences() not implemented'));
143
+ }
144
+
145
+ // ===== Song Requests =====
146
+
147
+ getRequests() {
148
+ return Promise.reject(new Error('getRequests() not implemented'));
149
+ }
150
+
151
+ approveRequest(_requestId) {
152
+ return Promise.reject(new Error('approveRequest() not implemented'));
153
+ }
154
+
155
+ rejectRequest(_requestId) {
156
+ return Promise.reject(new Error('rejectRequest() not implemented'));
157
+ }
158
+
159
+ // ===== State Subscriptions =====
160
+
161
+ /**
162
+ * Subscribe to state changes for a specific domain
163
+ * @param {string} domain - State domain (mixer, queue, playback, effects, etc.)
164
+ * @param {Function} callback - Callback function (receives updated state)
165
+ * @returns {Function} Unsubscribe function
166
+ */
167
+ onStateChange(_domain, _callback) {
168
+ throw new Error('onStateChange() not implemented');
169
+ }
170
+
171
+ /**
172
+ * Unsubscribe from state changes
173
+ * @param {string} domain - State domain
174
+ * @param {Function} callback - Callback to remove
175
+ */
176
+ offStateChange(_domain, _callback) {
177
+ throw new Error('offStateChange() not implemented');
178
+ }
179
+
180
+ // ===== Lifecycle =====
181
+
182
+ /**
183
+ * Initialize the bridge (e.g., connect sockets)
184
+ */
185
+ async connect() {
186
+ // Optional - override if needed
187
+ }
188
+
189
+ /**
190
+ * Clean up resources (e.g., disconnect sockets)
191
+ */
192
+ async disconnect() {
193
+ // Optional - override if needed
194
+ }
195
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * EffectsPanel - Unified effects browser and control panel
3
+ *
4
+ * Based on renderer's effects design (grid layout with categories)
5
+ * Works with both ElectronBridge and WebBridge via callbacks
6
+ */
7
+
8
+ export function EffectsPanel({
9
+ effects = [],
10
+ currentEffect = null,
11
+ disabledEffects = [],
12
+ searchTerm = '',
13
+ currentCategory = 'all',
14
+ onSearch,
15
+ onCategoryChange,
16
+ onSelectEffect,
17
+ onRandomEffect,
18
+ onEnableEffect,
19
+ onDisableEffect,
20
+ }) {
21
+ // Filter effects based on category and search
22
+ let filteredEffects = [...effects];
23
+
24
+ if (currentCategory !== 'all') {
25
+ filteredEffects = filteredEffects.filter((effect) => effect.category === currentCategory);
26
+ }
27
+
28
+ if (searchTerm.trim()) {
29
+ const searchLower = searchTerm.toLowerCase();
30
+ filteredEffects = filteredEffects.filter(
31
+ (effect) =>
32
+ effect.name?.toLowerCase().includes(searchLower) ||
33
+ effect.displayName?.toLowerCase().includes(searchLower) ||
34
+ effect.author?.toLowerCase().includes(searchLower)
35
+ );
36
+ }
37
+
38
+ const categories = [
39
+ { id: 'all', label: 'All' },
40
+ { id: 'geiss', label: 'Geiss' },
41
+ { id: 'martin', label: 'Martin' },
42
+ { id: 'flexi', label: 'Flexi' },
43
+ { id: 'shifter', label: 'Shifter' },
44
+ { id: 'other', label: 'Other' },
45
+ ];
46
+
47
+ const sanitizeFilename = (name) => {
48
+ return name.replace(/[^a-zA-Z0-9-_\s]/g, '_');
49
+ };
50
+
51
+ return (
52
+ <div className="h-full flex flex-col bg-gray-50 dark:bg-gray-900">
53
+ <div className="p-4 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
54
+ <div className="flex-1 max-w-md">
55
+ <input
56
+ type="text"
57
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:outline-none focus:border-blue-500"
58
+ placeholder="Search effects..."
59
+ value={searchTerm}
60
+ onChange={(e) => onSearch && onSearch(e.target.value)}
61
+ />
62
+ </div>
63
+ <div className="flex items-center gap-4 text-gray-600 dark:text-gray-400">
64
+ <span id="effectsCount" className="text-sm">
65
+ {currentCategory === 'all' && !searchTerm.trim()
66
+ ? `${effects.length} effects`
67
+ : `${filteredEffects.length} of ${effects.length} effects`}
68
+ </span>
69
+ {onRandomEffect && (
70
+ <button
71
+ onClick={onRandomEffect}
72
+ className="px-4 py-2 bg-blue-600 border-none rounded text-white cursor-pointer text-sm transition-colors flex items-center gap-1.5 hover:bg-blue-700"
73
+ >
74
+ <span className="material-icons text-lg">casino</span>
75
+ Random
76
+ </button>
77
+ )}
78
+ </div>
79
+ </div>
80
+
81
+ <div className="flex-1 flex flex-col overflow-hidden">
82
+ <div className="p-2.5 px-4 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex gap-2.5">
83
+ {categories.map((cat) => (
84
+ <button
85
+ key={cat.id}
86
+ className={`px-3 py-1.5 rounded text-xs cursor-pointer transition-all ${currentCategory === cat.id ? 'bg-blue-600 border-blue-600 text-white' : 'bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white'}`}
87
+ onClick={() => onCategoryChange && onCategoryChange(cat.id)}
88
+ >
89
+ {cat.label}
90
+ </button>
91
+ ))}
92
+ </div>
93
+
94
+ <div className="flex-1 overflow-y-auto p-4">
95
+ {filteredEffects.length === 0 ? (
96
+ <div className="text-center p-10 text-gray-500 dark:text-gray-400 flex flex-col items-center">
97
+ <span className="material-icons text-5xl mb-2.5">search_off</span>
98
+ <div className="text-base">No effects found</div>
99
+ </div>
100
+ ) : (
101
+ <div className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4">
102
+ {filteredEffects.map((effect) => {
103
+ const isActive = currentEffect === effect.name;
104
+ const isDisabled = disabledEffects.includes(effect.name);
105
+ const sanitizedName = sanitizeFilename(effect.name);
106
+ const screenshotPath = `../../static/images/butterchurn-screenshots/${sanitizedName}.png`;
107
+
108
+ return (
109
+ <div
110
+ key={effect.name}
111
+ className={`rounded-md p-0 cursor-pointer transition-all overflow-hidden flex flex-col ${isActive ? 'bg-blue-100 dark:bg-blue-900/40 border border-blue-600' : 'bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-blue-600'} ${isDisabled ? 'opacity-60' : ''}`}
112
+ onClick={() => !isDisabled && onSelectEffect && onSelectEffect(effect.name)}
113
+ >
114
+ <div className="relative w-full h-[150px] bg-gray-200 dark:bg-gray-900 overflow-hidden">
115
+ <img
116
+ src={screenshotPath}
117
+ alt={effect.displayName}
118
+ className="w-full h-full object-cover transition-transform hover:scale-105"
119
+ onError={(e) => {
120
+ e.target.style.display = 'none';
121
+ e.target.nextElementSibling.style.display = 'flex';
122
+ }}
123
+ />
124
+ <div className="absolute top-0 left-0 w-full h-full hidden items-center justify-center bg-gray-200 dark:bg-gray-900 text-gray-400 dark:text-gray-600">
125
+ <span className="material-icons text-5xl">image_not_supported</span>
126
+ </div>
127
+ </div>
128
+ <div className={`p-4 flex-1 ${isDisabled ? 'opacity-60' : ''}`}>
129
+ <div className="inline-block bg-blue-600 text-white px-1.5 py-0.5 rounded text-[11px] mb-2">
130
+ {effect.category}
131
+ </div>
132
+ <div
133
+ className={`font-bold mb-1.5 text-sm ${isDisabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-white'}`}
134
+ >
135
+ {effect.displayName}
136
+ </div>
137
+ <div
138
+ className={`text-xs mb-1.5 ${isDisabled ? 'text-gray-400 dark:text-gray-600' : 'text-gray-600 dark:text-gray-400'}`}
139
+ >
140
+ by {effect.author}
141
+ </div>
142
+ <div className="flex gap-2 mt-2.5">
143
+ <button
144
+ className={`flex-1 px-3 py-1.5 rounded text-xs cursor-pointer transition-colors ${isDisabled ? 'bg-gray-300 dark:bg-gray-700 text-gray-500 dark:text-gray-500 cursor-not-allowed opacity-50' : 'bg-blue-600 border-blue-600 text-white hover:bg-blue-700'}`}
145
+ onClick={(e) => {
146
+ e.stopPropagation();
147
+ !isDisabled && onSelectEffect && onSelectEffect(effect.name);
148
+ }}
149
+ disabled={isDisabled}
150
+ >
151
+ Use
152
+ </button>
153
+ <button
154
+ className="flex-1 px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-300 text-xs cursor-pointer transition-colors hover:bg-gray-200 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white"
155
+ onClick={(e) => {
156
+ e.stopPropagation();
157
+ if (isDisabled) {
158
+ onEnableEffect && onEnableEffect(effect.name);
159
+ } else {
160
+ onDisableEffect && onDisableEffect(effect.name);
161
+ }
162
+ }}
163
+ >
164
+ {isDisabled ? 'Enable' : 'Disable'}
165
+ </button>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ );
170
+ })}
171
+ </div>
172
+ )}
173
+ </div>
174
+ </div>
175
+ </div>
176
+ );
177
+ }