loukai-app 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +558 -0
- package/bin/loukai.js +32 -0
- package/package.json +243 -0
- package/src/main/appState.js +250 -0
- package/src/main/audioEngine.js +478 -0
- package/src/main/creator/conversionService.js +503 -0
- package/src/main/creator/downloadManager.js +1128 -0
- package/src/main/creator/ffmpegService.js +487 -0
- package/src/main/creator/installLogger.js +51 -0
- package/src/main/creator/keyDetection.js +212 -0
- package/src/main/creator/llmService.js +370 -0
- package/src/main/creator/lrclibService.js +340 -0
- package/src/main/creator/python/crepe_runner.py +189 -0
- package/src/main/creator/python/demucs_runner.py +158 -0
- package/src/main/creator/python/whisper_runner.py +172 -0
- package/src/main/creator/pythonRunner.js +268 -0
- package/src/main/creator/stemBuilder.js +491 -0
- package/src/main/creator/systemChecker.js +474 -0
- package/src/main/handlers/appHandlers.js +45 -0
- package/src/main/handlers/audioHandlers.js +33 -0
- package/src/main/handlers/autotuneHandlers.js +28 -0
- package/src/main/handlers/canvasHandlers.js +84 -0
- package/src/main/handlers/creatorHandlers.js +159 -0
- package/src/main/handlers/editorHandlers.js +98 -0
- package/src/main/handlers/effectsHandlers.js +100 -0
- package/src/main/handlers/fileHandlers.js +45 -0
- package/src/main/handlers/index.js +78 -0
- package/src/main/handlers/libraryHandlers.js +96 -0
- package/src/main/handlers/mixerHandlers.js +64 -0
- package/src/main/handlers/playerHandlers.js +39 -0
- package/src/main/handlers/preferencesHandlers.js +46 -0
- package/src/main/handlers/queueHandlers.js +81 -0
- package/src/main/handlers/rendererHandlers.js +63 -0
- package/src/main/handlers/settingsHandlers.js +42 -0
- package/src/main/handlers/webServerHandlers.js +105 -0
- package/src/main/main.js +2351 -0
- package/src/main/preload.js +252 -0
- package/src/main/settingsManager.js +139 -0
- package/src/main/statePersistence.js +193 -0
- package/src/main/utils/pathValidator.js +112 -0
- package/src/main/webServer.js +2535 -0
- package/src/native/autotune.js +417 -0
- package/src/renderer/adapters/ElectronBridge.js +677 -0
- package/src/renderer/canvas.html +80 -0
- package/src/renderer/components/App.jsx +303 -0
- package/src/renderer/components/AppRoot.jsx +37 -0
- package/src/renderer/components/AudioDeviceSettings.jsx +145 -0
- package/src/renderer/components/EffectsPanelWrapper.jsx +267 -0
- package/src/renderer/components/MixerTab.jsx +233 -0
- package/src/renderer/components/MixerTabWrapper.jsx +31 -0
- package/src/renderer/components/PortalSelect.jsx +239 -0
- package/src/renderer/components/QueueTab.jsx +116 -0
- package/src/renderer/components/RequestsListWrapper.jsx +78 -0
- package/src/renderer/components/ServerTab.jsx +472 -0
- package/src/renderer/components/SongInfoBarWrapper.jsx +77 -0
- package/src/renderer/components/StatusBar.jsx +92 -0
- package/src/renderer/components/TabNavigation.jsx +77 -0
- package/src/renderer/components/TransportControlsWrapper.jsx +69 -0
- package/src/renderer/components/creator/CreateTab.jsx +1236 -0
- package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js +2 -0
- package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +1 -0
- package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js +2 -0
- package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js.map +1 -0
- package/src/renderer/dist/assets/player-DVrqp7N5.js +3 -0
- package/src/renderer/dist/assets/player-DVrqp7N5.js.map +1 -0
- package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +2 -0
- package/src/renderer/dist/assets/songLoaders-BaTgGib4.js.map +1 -0
- package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js +2 -0
- package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js.map +1 -0
- package/src/renderer/dist/js/autoTuneWorklet.js +224 -0
- package/src/renderer/dist/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/dist/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/dist/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/dist/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/dist/renderer.css +1 -0
- package/src/renderer/dist/renderer.js +62 -0
- package/src/renderer/dist/renderer.js.map +1 -0
- package/src/renderer/dist/renderer.woff2 +0 -0
- package/src/renderer/hooks/useKeyboardShortcuts.js +154 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/index.html.backup +372 -0
- package/src/renderer/js/PlayerInterface.js +267 -0
- package/src/renderer/js/autoTuneWorklet.js +224 -0
- package/src/renderer/js/butterchurnVerify.js +46 -0
- package/src/renderer/js/canvas-app.js +114 -0
- package/src/renderer/js/cdgPlayer.js +685 -0
- package/src/renderer/js/kaiPlayer.js +1200 -0
- package/src/renderer/js/karaokeRenderer.js +3392 -0
- package/src/renderer/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/js/microphoneEngine.js +656 -0
- package/src/renderer/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/js/player.js +232 -0
- package/src/renderer/js/referencePitchTracker.js +130 -0
- package/src/renderer/js/songLoaders.js +334 -0
- package/src/renderer/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/js/webrtcManager.js +511 -0
- package/src/renderer/lib/butterchurn.min.js +6739 -0
- package/src/renderer/lib/butterchurnPresets.min.js +1 -0
- package/src/renderer/lib/cdgraphics-wrapper.js +16 -0
- package/src/renderer/lib/cdgraphics.js +299 -0
- package/src/renderer/public/js/autoTuneWorklet.js +224 -0
- package/src/renderer/public/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/public/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/public/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/public/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/react-entry.jsx +44 -0
- package/src/renderer/styles/tailwind.css +106 -0
- package/src/renderer/utils/qrCodeGenerator.js +98 -0
- package/src/renderer/vite.config.js +31 -0
- package/src/shared/adapters/BridgeInterface.js +195 -0
- package/src/shared/components/EffectsPanel.jsx +177 -0
- package/src/shared/components/LibraryPanel.jsx +701 -0
- package/src/shared/components/LineDetailCanvas.jsx +167 -0
- package/src/shared/components/LyricLine.jsx +505 -0
- package/src/shared/components/LyricRejection.jsx +84 -0
- package/src/shared/components/LyricSuggestion.jsx +80 -0
- package/src/shared/components/LyricsEditorCanvas.jsx +271 -0
- package/src/shared/components/MixerPanel.jsx +94 -0
- package/src/shared/components/PlayerControls.jsx +206 -0
- package/src/shared/components/PortalSelect.jsx +239 -0
- package/src/shared/components/QueueList.jsx +365 -0
- package/src/shared/components/QuickSearch.jsx +126 -0
- package/src/shared/components/RequestsList.jsx +121 -0
- package/src/shared/components/SongEditor.jsx +1362 -0
- package/src/shared/components/SongInfoBar.jsx +81 -0
- package/src/shared/components/ThemeToggle.jsx +106 -0
- package/src/shared/components/Toast.jsx +30 -0
- package/src/shared/components/VisualizationSettings.jsx +243 -0
- package/src/shared/constants.js +95 -0
- package/src/shared/context/BridgeContext.jsx +32 -0
- package/src/shared/contexts/AudioContext.jsx +37 -0
- package/src/shared/contexts/PlayerContext.jsx +66 -0
- package/src/shared/contexts/SettingsContext.jsx +50 -0
- package/src/shared/defaults.js +158 -0
- package/src/shared/formatUtils.js +59 -0
- package/src/shared/formatUtils.test.js +207 -0
- package/src/shared/hooks/useAppState.js +97 -0
- package/src/shared/hooks/useAudioEngine.js +264 -0
- package/src/shared/hooks/usePlayer.js +89 -0
- package/src/shared/hooks/useSettingsPersistence.js +74 -0
- package/src/shared/hooks/useWebRTC.js +118 -0
- package/src/shared/ipcContracts.js +299 -0
- package/src/shared/package.json +3 -0
- package/src/shared/services/creatorService.js +373 -0
- package/src/shared/services/creatorService.test.js +413 -0
- package/src/shared/services/editorService.js +213 -0
- package/src/shared/services/editorService.test.js +219 -0
- package/src/shared/services/effectsService.js +271 -0
- package/src/shared/services/effectsService.test.js +418 -0
- package/src/shared/services/libraryService.js +438 -0
- package/src/shared/services/libraryService.test.js +474 -0
- package/src/shared/services/mixerService.js +172 -0
- package/src/shared/services/mixerService.test.js +399 -0
- package/src/shared/services/playerService.js +221 -0
- package/src/shared/services/playerService.test.js +357 -0
- package/src/shared/services/preferencesService.js +219 -0
- package/src/shared/services/queueService.js +226 -0
- package/src/shared/services/queueService.test.js +430 -0
- package/src/shared/services/requestsService.js +155 -0
- package/src/shared/services/requestsService.test.js +362 -0
- package/src/shared/services/serverSettingsService.js +151 -0
- package/src/shared/services/settingsService.js +257 -0
- package/src/shared/services/settingsService.test.js +295 -0
- package/src/shared/state/StateManager.js +263 -0
- package/src/shared/utils/audio.js +42 -0
- package/src/shared/utils/format.js +32 -0
- package/src/shared/utils/lyricsUtils.js +162 -0
- package/src/test/setup.js +40 -0
- package/src/utils/cdgLoader.js +180 -0
- package/src/utils/m4aLoader.js +333 -0
- package/src/web/App.jsx +578 -0
- package/src/web/adapters/WebBridge.js +428 -0
- package/src/web/components/PlayerSettingsPanel.jsx +231 -0
- package/src/web/components/SongSearch.jsx +180 -0
- package/src/web/dist/assets/index-0H-RnRrV.js +51 -0
- package/src/web/dist/assets/index-0H-RnRrV.js.map +1 -0
- package/src/web/dist/assets/index-DYW2zB0u.css +1 -0
- package/src/web/dist/index.html +15 -0
- package/src/web/index.html +14 -0
- package/src/web/main.jsx +10 -0
- package/src/web/package-lock.json +1765 -0
- package/src/web/pages/SongRequestPage.jsx +619 -0
- package/src/web/styles/tailwind.css +68 -0
- package/src/web/vite.config.js +27 -0
- package/static/fonts/material-icons.woff2 +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Potion of Spirits.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Songflower _Moss Posy_.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Storm of the Eye _Thunder_ - mash0000 - quasi pseudo meta concentrics.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi _ Geiss - Airhandler _Kali Mix_ - Canvas Mix.png +0 -0
- package/static/images/butterchurn-screenshots/An AdamFX n Martin Infusion 2 flexi - Why The Sky Looks Diffrent Today - AdamFx n Martin Infusion - Tack Tile Disfunction B.png +0 -0
- package/static/images/butterchurn-screenshots/Cope - The Neverending Explosion of Red Liquid Fire.png +0 -0
- proton lights __Krash_s beat code_ _Phat_remix02b.png +0 -0
- package/static/images/butterchurn-screenshots/Eo_S_ _ Phat - cubetrace - v2.png +0 -0
- package/static/images/butterchurn-screenshots/Eo_S_ _ Zylot - skylight _Stained Glass Majesty mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - alien fish pond.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - area 51.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - infused with the spiral.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - mindblob _shiny mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - mindblob mix.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - predator-prey-spirals.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - smashing fractals _acid etching mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - truly soft piece of software - this is generic texturing _Jelly_ .png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ Martin - astral projection.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ Martin - cascading decay swing.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ amandio c - piercing 05 - Kopie _2_ - Kopie.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ stahlregen - jelly showoff parade.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi_ fishbrain_ Geiss _ Martin - tokamak witchery.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi_ martin _ geiss - dedicated to the sherwin maxawow.png +0 -0
- package/static/images/butterchurn-screenshots/Fumbling_Foo _ Flexi_ Martin_ Orb_ Unchained - Star Nova v7b.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Cauldron - painterly 2 _saturation remix_.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Reaction Diffusion 2.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Spiral Artifact.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Thumb Drum.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss _ Flexi _ Martin - disconnected.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss_ Flexi _ Stahlregen - Thumbdrum Tokamak _crossfiring aftermath jelly mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/Goody - The Wild Vort.png +0 -0
- package/static/images/butterchurn-screenshots/Idiot - Star Of Annon.png +0 -0
- package/static/images/butterchurn-screenshots/Krash _ Illusion - Spiral Movement.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - QBikal - Surface Turbulence IIb.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - acid wiring.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - charisma.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - liquid arrows.png +0 -0
- package/static/images/butterchurn-screenshots/Milk Artist At our Best - FED - SlowFast Ft AdamFX n Martin - HD CosmoFX.png +0 -0
- package/static/images/butterchurn-screenshots/ORB - Waaa.png +0 -0
- package/static/images/butterchurn-screenshots/Phat_fiShbRaiN_Eo_S_Mandala_Chasers_remix.png +0 -0
- package/static/images/butterchurn-screenshots/Rovastar - Oozing Resistance.png +0 -0
- package/static/images/butterchurn-screenshots/Rovastar _ Loadus _ Geiss - FractalDrop _Triple Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/TonyMilkdrop - Leonardo Da Vinci_s Balloon _Flexi - merry-go-round _ techstyle_.png +0 -0
- package/static/images/butterchurn-screenshots/TonyMilkdrop - Magellan_s Nebula _Flexi - you enter first _ multiverse_.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained - Rewop.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained - Unified Drag 2.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained _ Rovastar - Wormhole Pillars _Hall of Shadows mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - Paint Spill _Music Reactive Paint Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - Star Ornament.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - True Visionary _Final Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/_Aderrasi - Wanderer in Curved Space - mash0000 - faclempt kibitzing meshuggana schmaltz _Geiss color mix_.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - Artifact 01.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - Desert Rose 2.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - untitled.png +0 -0
- package/static/images/butterchurn-screenshots/_Mig_049.png +0 -0
- package/static/images/butterchurn-screenshots/_Mig_085.png +0 -0
- package/static/images/butterchurn-screenshots/_Rovastar _ Geiss - Hurricane Nightmare _Posterize Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _197_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _220_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _431_.png +0 -0
- package/static/images/butterchurn-screenshots/cope _ martin - mother-of-pearl.png +0 -0
- package/static/images/butterchurn-screenshots/fiShbRaiN _ Flexi - witchcraft 2_0.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - bouncing balls _double mindblob neon mix_.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - mom_ why the sky looks different today.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - patternton_ district of media_ capitol of the united abstractions of fractopia.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - swing out on the spiral.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - what is the matrix.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ amandio c - organic _random mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ amandio c - organic12-3d-2_milk.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ fishbrain - neon mindblob grafitti.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ geiss - pogo cubes vs_ tokamak vs_ game of life _stahls jelly 4_5 finish_.png +0 -0
- package/static/images/butterchurn-screenshots/high-altitude basket unraveling - singh grooves nitrogen argon nz_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - The Bridge of Khazad-Dum.png +0 -0
- package/static/images/butterchurn-screenshots/martin - angel flight.png +0 -0
- package/static/images/butterchurn-screenshots/martin - another kind of groove.png +0 -0
- package/static/images/butterchurn-screenshots/martin - bombyx mori.png +0 -0
- package/static/images/butterchurn-screenshots/martin - castle in the air.png +0 -0
- package/static/images/butterchurn-screenshots/martin - chain breaker.png +0 -0
- package/static/images/butterchurn-screenshots/martin - disco mix 4.png +0 -0
- package/static/images/butterchurn-screenshots/martin - extreme heat.png +0 -0
- package/static/images/butterchurn-screenshots/martin - frosty caves 2.png +0 -0
- package/static/images/butterchurn-screenshots/martin - fruit machine.png +0 -0
- package/static/images/butterchurn-screenshots/martin - ghost city.png +0 -0
- package/static/images/butterchurn-screenshots/martin - glass corridor.png +0 -0
- package/static/images/butterchurn-screenshots/martin - infinity _2010 update_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - mandelbox explorer - high speed demo version.png +0 -0
- package/static/images/butterchurn-screenshots/martin - mucus cervix.png +0 -0
- package/static/images/butterchurn-screenshots/martin - reflections on black tiles.png +0 -0
- package/static/images/butterchurn-screenshots/martin - stormy sea _2010 update_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - witchcraft reloaded.png +0 -0
- package/static/images/butterchurn-screenshots/martin _ flexi - diamond cutter _prismaticvortex_com_ - camille - i wish i wish i wish i was constrained.png +0 -0
- package/static/images/butterchurn-screenshots/martin _shadow harlequins shape code_ - fata morgana.png +0 -0
- package/static/images/butterchurn-screenshots/martin_ flexi_ fishbrain _ sto - enterstate _random mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/sawtooth grin roam.png +0 -0
- package/static/images/butterchurn-screenshots/shifter - dark tides bdrv mix 2.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - Rovastar - Sunflower Passion _Enlightment Mix__Phat_edit _ flexi und martin shaders - circumflex in character classes in regular expression.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - heretical crosscut playpen.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - uninitialized variabowl _hydroponic chronic_.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - vector exp 1 - couldn_t not.png +0 -0
- package/static/images/butterchurn-screenshots/yin - 191 - Temporal singularities.png +0 -0
- package/static/images/logo-512.png +0 -0
- package/static/images/logo.png +0 -0
- package/static/loukai-logo.png +0 -0
- package/static/screenshot-generator.html +610 -0
|
@@ -0,0 +1,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
|
+
}
|