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,701 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LibraryPanel - Full library browser with alphabet filtering and pagination
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Alphabet navigation (A-Z, #)
|
|
6
|
+
* - Pagination (100 songs per page)
|
|
7
|
+
* - Search functionality
|
|
8
|
+
* - Table view with sorting
|
|
9
|
+
* - Add to queue / Load song actions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
13
|
+
import { getFormatIcon, formatDuration } from '../formatUtils.js';
|
|
14
|
+
|
|
15
|
+
function SongInfoModal({ song, onClose }) {
|
|
16
|
+
if (!song) return null;
|
|
17
|
+
|
|
18
|
+
console.log('🎵 SongInfoModal rendering for:', song.title);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className="fixed inset-0 bg-black/70 flex items-center justify-center z-[10000]"
|
|
23
|
+
onClick={onClose}
|
|
24
|
+
>
|
|
25
|
+
<div
|
|
26
|
+
className="bg-gray-800 dark:bg-gray-900 border border-gray-600 dark:border-gray-700 rounded-lg w-[90%] max-w-[500px] max-h-[80vh] overflow-hidden flex flex-col"
|
|
27
|
+
onClick={(e) => e.stopPropagation()}
|
|
28
|
+
>
|
|
29
|
+
<div className="flex justify-between items-center px-5 py-4 border-b border-gray-700 dark:border-gray-800">
|
|
30
|
+
<h2 className="m-0 text-lg font-semibold text-white">Song Information</h2>
|
|
31
|
+
<button
|
|
32
|
+
className="bg-none border-none text-white text-[32px] leading-none cursor-pointer p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-gray-700 dark:hover:bg-gray-800"
|
|
33
|
+
onClick={onClose}
|
|
34
|
+
>
|
|
35
|
+
×
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="p-5 overflow-y-auto">
|
|
39
|
+
<div className="space-y-0">
|
|
40
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5 border-b border-gray-700/50 dark:border-gray-800/50">
|
|
41
|
+
<span className="font-semibold text-gray-300 dark:text-gray-400">Title:</span>
|
|
42
|
+
<span className="text-white">{song.title}</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5 border-b border-gray-700/50 dark:border-gray-800/50">
|
|
45
|
+
<span className="font-semibold text-gray-300 dark:text-gray-400">Artist:</span>
|
|
46
|
+
<span className="text-white">{song.artist}</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5 border-b border-gray-700/50 dark:border-gray-800/50">
|
|
49
|
+
<span className="font-semibold text-gray-300 dark:text-gray-400">Album:</span>
|
|
50
|
+
<span className="text-white">{song.album || 'N/A'}</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5 border-b border-gray-700/50 dark:border-gray-800/50">
|
|
53
|
+
<span className="font-semibold text-gray-300 dark:text-gray-400">Genre:</span>
|
|
54
|
+
<span className="text-white">{song.genre || 'N/A'}</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5 border-b border-gray-700/50 dark:border-gray-800/50">
|
|
57
|
+
<span className="font-semibold text-gray-300 dark:text-gray-400">Key:</span>
|
|
58
|
+
<span className="text-white">{song.key || 'N/A'}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5 border-b border-gray-700/50 dark:border-gray-800/50">
|
|
61
|
+
<span className="font-semibold text-gray-300 dark:text-gray-400">Year:</span>
|
|
62
|
+
<span className="text-white">{song.year || 'N/A'}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5 border-b border-gray-700/50 dark:border-gray-800/50">
|
|
65
|
+
<span className="font-semibold text-gray-300 dark:text-gray-400">Duration:</span>
|
|
66
|
+
<span className="text-white">{formatDuration(song.duration)}</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5 border-b border-gray-700/50 dark:border-gray-800/50">
|
|
69
|
+
<span className="font-semibold text-gray-300 dark:text-gray-400">Format:</span>
|
|
70
|
+
<span className="text-white">{song.format || 'N/A'}</span>
|
|
71
|
+
</div>
|
|
72
|
+
{song.tags && song.tags.length > 0 && (
|
|
73
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5 border-b border-gray-700/50 dark:border-gray-800/50">
|
|
74
|
+
<span className="font-semibold text-gray-300 dark:text-gray-400">Tags:</span>
|
|
75
|
+
<span className="text-white flex flex-wrap gap-1.5">
|
|
76
|
+
{song.tags.map((tag) => (
|
|
77
|
+
<span
|
|
78
|
+
key={tag}
|
|
79
|
+
className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
80
|
+
tag === 'edited'
|
|
81
|
+
? 'bg-blue-600/30 text-blue-300'
|
|
82
|
+
: tag === 'ai_corrected'
|
|
83
|
+
? 'bg-purple-600/30 text-purple-300'
|
|
84
|
+
: 'bg-gray-600/30 text-gray-300'
|
|
85
|
+
}`}
|
|
86
|
+
>
|
|
87
|
+
{tag}
|
|
88
|
+
</span>
|
|
89
|
+
))}
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
<div className="grid grid-cols-[120px_1fr] gap-3 py-2.5">
|
|
94
|
+
<span className="font-semibold text-gray-300 dark:text-gray-400">Path:</span>
|
|
95
|
+
<span className="text-white break-all text-[11px]">{song.path}</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function LibraryPanel({ bridge, showSetFolder = false, showFullRefresh = false }) {
|
|
105
|
+
const [songs, setSongs] = useState([]);
|
|
106
|
+
const [filteredSongs, setFilteredSongs] = useState([]);
|
|
107
|
+
const [currentLetter, setCurrentLetter] = useState(null);
|
|
108
|
+
const [availableLetters, setAvailableLetters] = useState([]);
|
|
109
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
110
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
111
|
+
const [songsFolder, setSongsFolder] = useState(null);
|
|
112
|
+
const [loading, setLoading] = useState(false);
|
|
113
|
+
const [modalSong, setModalSong] = useState(null);
|
|
114
|
+
const [scanProgress, setScanProgress] = useState(null); // { current, total }
|
|
115
|
+
|
|
116
|
+
const pageSize = 100;
|
|
117
|
+
|
|
118
|
+
// Wrap functions in useCallback to stabilize references for useEffect dependencies
|
|
119
|
+
const loadLetterPage = useCallback((letter, page, songsList) => {
|
|
120
|
+
setCurrentLetter(letter);
|
|
121
|
+
setCurrentPage(page);
|
|
122
|
+
setSearchTerm('');
|
|
123
|
+
|
|
124
|
+
// Filter songs by first letter of artist
|
|
125
|
+
const letterSongs = songsList.filter((song) => {
|
|
126
|
+
const artist = song.artist || song.title || song.name;
|
|
127
|
+
if (!artist) return false;
|
|
128
|
+
|
|
129
|
+
const firstChar = artist.trim()[0].toUpperCase();
|
|
130
|
+
if (letter === '#') {
|
|
131
|
+
return !/[A-Z]/.test(firstChar);
|
|
132
|
+
}
|
|
133
|
+
return firstChar === letter;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Sort and paginate
|
|
137
|
+
const sortedSongs = letterSongs.sort((a, b) => {
|
|
138
|
+
const artistA = (a.artist || a.title || '').toLowerCase();
|
|
139
|
+
const artistB = (b.artist || b.title || '').toLowerCase();
|
|
140
|
+
return artistA.localeCompare(artistB);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
setFilteredSongs(sortedSongs);
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
const calculateAvailableLetters = useCallback(
|
|
147
|
+
(songsList, shouldAutoSelect = true) => {
|
|
148
|
+
const letterSet = new Set();
|
|
149
|
+
|
|
150
|
+
songsList.forEach((song) => {
|
|
151
|
+
const artist = song.artist || song.title || song.name;
|
|
152
|
+
if (artist) {
|
|
153
|
+
const firstChar = artist.trim()[0].toUpperCase();
|
|
154
|
+
if (/[A-Z]/.test(firstChar)) {
|
|
155
|
+
letterSet.add(firstChar);
|
|
156
|
+
} else {
|
|
157
|
+
letterSet.add('#');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let letters = Array.from(letterSet).sort();
|
|
163
|
+
// Put '#' at the end
|
|
164
|
+
if (letters.includes('#')) {
|
|
165
|
+
letters = letters.filter((l) => l !== '#');
|
|
166
|
+
letters.push('#');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setAvailableLetters(letters);
|
|
170
|
+
|
|
171
|
+
// Auto-select first letter if requested and songs exist
|
|
172
|
+
if (shouldAutoSelect && letters.length > 0) {
|
|
173
|
+
const firstLetter = letters.includes('A') ? 'A' : letters[0];
|
|
174
|
+
loadLetterPage(firstLetter, 1, songsList);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
[loadLetterPage]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const loadLibrary = useCallback(async () => {
|
|
181
|
+
try {
|
|
182
|
+
setLoading(true);
|
|
183
|
+
const folder = await bridge.getSongsFolder();
|
|
184
|
+
console.log('📁 Songs folder:', folder);
|
|
185
|
+
setSongsFolder(folder);
|
|
186
|
+
|
|
187
|
+
if (folder) {
|
|
188
|
+
const result = await bridge.getCachedLibrary();
|
|
189
|
+
console.log('📚 Cached library result:', result);
|
|
190
|
+
const librarySongs = result.files || [];
|
|
191
|
+
console.log('🎵 Library songs count:', librarySongs.length);
|
|
192
|
+
setSongs(librarySongs);
|
|
193
|
+
calculateAvailableLetters(librarySongs); // This will auto-select first letter
|
|
194
|
+
} else {
|
|
195
|
+
console.log('❌ No songs folder set');
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error('Failed to load library:', error);
|
|
199
|
+
} finally {
|
|
200
|
+
setLoading(false);
|
|
201
|
+
}
|
|
202
|
+
}, [bridge, calculateAvailableLetters]);
|
|
203
|
+
|
|
204
|
+
// Listen for scan progress events (Electron renderer only)
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (typeof window !== 'undefined' && window.kaiAPI?.events) {
|
|
207
|
+
const handleScanProgress = (event, data) => {
|
|
208
|
+
setScanProgress({ current: data.current, total: data.total });
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const handleScanComplete = (event, data) => {
|
|
212
|
+
console.log(`📚 Background scan complete: ${data.count} songs`);
|
|
213
|
+
setScanProgress(null);
|
|
214
|
+
loadLibrary(); // Reload library with new scanned songs
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const handleFolderSet = (event, folder) => {
|
|
218
|
+
console.log(`📁 Songs folder updated: ${folder}`);
|
|
219
|
+
setSongsFolder(folder);
|
|
220
|
+
loadLibrary(); // Reload library with new folder
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const handleSongUpdated = (event, data) => {
|
|
224
|
+
console.log(`🎵 Song updated: ${data.path}`);
|
|
225
|
+
// Update the song in the songs list
|
|
226
|
+
setSongs((prevSongs) => {
|
|
227
|
+
const songIndex = prevSongs.findIndex((s) => s.path === data.path);
|
|
228
|
+
if (songIndex !== -1) {
|
|
229
|
+
const updatedSongs = [...prevSongs];
|
|
230
|
+
updatedSongs[songIndex] = { ...updatedSongs[songIndex], ...data.metadata };
|
|
231
|
+
return updatedSongs;
|
|
232
|
+
}
|
|
233
|
+
return prevSongs;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Update filtered songs if they're currently displayed
|
|
237
|
+
setFilteredSongs((prevFiltered) => {
|
|
238
|
+
const songIndex = prevFiltered.findIndex((s) => s.path === data.path);
|
|
239
|
+
if (songIndex !== -1) {
|
|
240
|
+
const updatedFiltered = [...prevFiltered];
|
|
241
|
+
updatedFiltered[songIndex] = { ...updatedFiltered[songIndex], ...data.metadata };
|
|
242
|
+
return updatedFiltered;
|
|
243
|
+
}
|
|
244
|
+
return prevFiltered;
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
window.kaiAPI.events.on('library:scanProgress', handleScanProgress);
|
|
249
|
+
window.kaiAPI.events.on('library:scanComplete', handleScanComplete);
|
|
250
|
+
window.kaiAPI.events.on('library:folderSet', handleFolderSet);
|
|
251
|
+
window.kaiAPI.events.on('library:songUpdated', handleSongUpdated);
|
|
252
|
+
|
|
253
|
+
return () => {
|
|
254
|
+
window.kaiAPI.events.removeListener('library:scanProgress', handleScanProgress);
|
|
255
|
+
window.kaiAPI.events.removeListener('library:scanComplete', handleScanComplete);
|
|
256
|
+
window.kaiAPI.events.removeListener('library:folderSet', handleFolderSet);
|
|
257
|
+
window.kaiAPI.events.removeListener('library:songUpdated', handleSongUpdated);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}, [loadLibrary]);
|
|
261
|
+
|
|
262
|
+
// Listen for library updates from socket (Web admin only)
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (bridge?.socket) {
|
|
265
|
+
const handleLibraryRefreshed = (data) => {
|
|
266
|
+
console.log(`📚 Library refreshed from remote: ${data.count} songs`);
|
|
267
|
+
loadLibrary(); // Reload library
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const handleSongUpdated = (data) => {
|
|
271
|
+
console.log(`🎵 Song updated: ${data.path}`);
|
|
272
|
+
// Update the song in the songs list
|
|
273
|
+
setSongs((prevSongs) => {
|
|
274
|
+
const songIndex = prevSongs.findIndex((s) => s.path === data.path);
|
|
275
|
+
if (songIndex !== -1) {
|
|
276
|
+
const updatedSongs = [...prevSongs];
|
|
277
|
+
updatedSongs[songIndex] = { ...updatedSongs[songIndex], ...data.metadata };
|
|
278
|
+
return updatedSongs;
|
|
279
|
+
}
|
|
280
|
+
return prevSongs;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Update filtered songs if they're currently displayed
|
|
284
|
+
setFilteredSongs((prevFiltered) => {
|
|
285
|
+
const songIndex = prevFiltered.findIndex((s) => s.path === data.path);
|
|
286
|
+
if (songIndex !== -1) {
|
|
287
|
+
const updatedFiltered = [...prevFiltered];
|
|
288
|
+
updatedFiltered[songIndex] = { ...updatedFiltered[songIndex], ...data.metadata };
|
|
289
|
+
return updatedFiltered;
|
|
290
|
+
}
|
|
291
|
+
return prevFiltered;
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
bridge.socket.on('library-refreshed', handleLibraryRefreshed);
|
|
296
|
+
bridge.socket.on('library:songUpdated', handleSongUpdated);
|
|
297
|
+
|
|
298
|
+
return () => {
|
|
299
|
+
bridge.socket.off('library-refreshed', handleLibraryRefreshed);
|
|
300
|
+
bridge.socket.off('library:songUpdated', handleSongUpdated);
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}, [bridge, loadLibrary]);
|
|
304
|
+
|
|
305
|
+
// Load library on mount
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
loadLibrary();
|
|
308
|
+
}, [loadLibrary]);
|
|
309
|
+
|
|
310
|
+
const handleSearch = (term) => {
|
|
311
|
+
setSearchTerm(term);
|
|
312
|
+
setCurrentLetter(null);
|
|
313
|
+
setCurrentPage(1);
|
|
314
|
+
|
|
315
|
+
if (!term.trim()) {
|
|
316
|
+
setFilteredSongs([]);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const searchLower = term.toLowerCase();
|
|
321
|
+
const results = songs.filter((song) => {
|
|
322
|
+
return (
|
|
323
|
+
(song.title || '').toLowerCase().includes(searchLower) ||
|
|
324
|
+
(song.artist || '').toLowerCase().includes(searchLower) ||
|
|
325
|
+
(song.album || '').toLowerCase().includes(searchLower)
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
setFilteredSongs(results);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const handleSetFolder = async () => {
|
|
333
|
+
try {
|
|
334
|
+
const folder = await bridge.setSongsFolder();
|
|
335
|
+
if (folder) {
|
|
336
|
+
setSongsFolder(folder);
|
|
337
|
+
await loadLibrary();
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.error('Failed to set folder:', error);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const handleSync = async () => {
|
|
345
|
+
if (!songsFolder) return;
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
setLoading(true);
|
|
349
|
+
await bridge.syncLibrary();
|
|
350
|
+
await loadLibrary();
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error('Failed to sync:', error);
|
|
353
|
+
} finally {
|
|
354
|
+
setLoading(false);
|
|
355
|
+
setScanProgress(null); // Clear progress when done
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const handleRefresh = async () => {
|
|
360
|
+
if (!songsFolder) return;
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
setLoading(true);
|
|
364
|
+
await bridge.scanLibrary();
|
|
365
|
+
await loadLibrary();
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.error('Failed to refresh:', error);
|
|
368
|
+
} finally {
|
|
369
|
+
setLoading(false);
|
|
370
|
+
setScanProgress(null); // Clear progress when done
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const handleAddToQueue = async (song) => {
|
|
375
|
+
try {
|
|
376
|
+
await bridge.addToQueue({
|
|
377
|
+
path: song.path,
|
|
378
|
+
title: song.title,
|
|
379
|
+
artist: song.artist,
|
|
380
|
+
duration: song.duration,
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error('Failed to add to queue:', error);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const handleShowInfo = (song) => {
|
|
388
|
+
console.log('📋 Opening song info modal for:', song.title);
|
|
389
|
+
setModalSong(song);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Pagination
|
|
393
|
+
const totalPages = Math.ceil(filteredSongs.length / pageSize);
|
|
394
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
395
|
+
const endIndex = startIndex + pageSize;
|
|
396
|
+
const currentPageSongs = filteredSongs.slice(startIndex, endIndex);
|
|
397
|
+
|
|
398
|
+
// Smart pagination - show limited page numbers around current page
|
|
399
|
+
const getPageNumbers = () => {
|
|
400
|
+
const maxButtons = 7; // Show max 7 page buttons
|
|
401
|
+
if (totalPages <= maxButtons) {
|
|
402
|
+
// Show all pages if total is small
|
|
403
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const pages = [];
|
|
407
|
+
const halfRange = Math.floor((maxButtons - 3) / 2); // Reserve 3 for first, last, and ellipsis
|
|
408
|
+
|
|
409
|
+
// Always show first page
|
|
410
|
+
pages.push(1);
|
|
411
|
+
|
|
412
|
+
let startPage = Math.max(2, currentPage - halfRange);
|
|
413
|
+
let endPage = Math.min(totalPages - 1, currentPage + halfRange);
|
|
414
|
+
|
|
415
|
+
// Adjust if we're near the beginning
|
|
416
|
+
if (currentPage <= halfRange + 2) {
|
|
417
|
+
endPage = Math.min(maxButtons - 1, totalPages - 1);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Adjust if we're near the end
|
|
421
|
+
if (currentPage >= totalPages - halfRange - 1) {
|
|
422
|
+
startPage = Math.max(2, totalPages - maxButtons + 2);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Add ellipsis if needed before
|
|
426
|
+
if (startPage > 2) {
|
|
427
|
+
pages.push('...');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Add middle pages
|
|
431
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
432
|
+
pages.push(i);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Add ellipsis if needed after
|
|
436
|
+
if (endPage < totalPages - 1) {
|
|
437
|
+
pages.push('...');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Always show last page
|
|
441
|
+
if (totalPages > 1) {
|
|
442
|
+
pages.push(totalPages);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return pages;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const allLetters = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), '#'];
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div className="flex flex-col h-full gap-1 overflow-hidden">
|
|
452
|
+
{/* Header Controls */}
|
|
453
|
+
<div className="flex flex-col gap-1.5 pb-1 border-b border-gray-200 dark:border-gray-700 shrink-0">
|
|
454
|
+
<div className="flex gap-2 flex-wrap items-center">
|
|
455
|
+
{showSetFolder && (
|
|
456
|
+
<button
|
|
457
|
+
onClick={handleSetFolder}
|
|
458
|
+
className="flex items-center gap-1.5 px-3 py-2 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded text-gray-900 dark:text-white cursor-pointer text-sm hover:bg-gray-200 dark:hover:bg-gray-750 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
459
|
+
>
|
|
460
|
+
<span className="material-icons text-lg">folder_open</span>
|
|
461
|
+
Set Songs Folder
|
|
462
|
+
</button>
|
|
463
|
+
)}
|
|
464
|
+
<button
|
|
465
|
+
onClick={handleSync}
|
|
466
|
+
className="flex items-center gap-1.5 px-3 py-2 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded text-gray-900 dark:text-white cursor-pointer text-sm hover:bg-gray-200 dark:hover:bg-gray-750 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
467
|
+
disabled={!songsFolder || loading}
|
|
468
|
+
>
|
|
469
|
+
<span className="material-icons text-lg">sync</span>
|
|
470
|
+
Sync
|
|
471
|
+
</button>
|
|
472
|
+
{showFullRefresh && (
|
|
473
|
+
<button
|
|
474
|
+
onClick={handleRefresh}
|
|
475
|
+
className="flex items-center gap-1.5 px-3 py-2 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded text-gray-900 dark:text-white cursor-pointer text-sm hover:bg-gray-200 dark:hover:bg-gray-750 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
476
|
+
disabled={!songsFolder || loading}
|
|
477
|
+
>
|
|
478
|
+
<span className="material-icons text-lg">refresh</span>
|
|
479
|
+
Full Refresh
|
|
480
|
+
</button>
|
|
481
|
+
)}
|
|
482
|
+
<div className="flex-1 min-w-[200px]">
|
|
483
|
+
<input
|
|
484
|
+
type="text"
|
|
485
|
+
className="w-full px-3 py-2 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded text-gray-900 dark:text-white text-sm focus:outline-none focus:border-blue-500"
|
|
486
|
+
placeholder="Search songs..."
|
|
487
|
+
value={searchTerm}
|
|
488
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
489
|
+
/>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
<div className="flex gap-4 text-xs text-gray-500 dark:text-gray-400">
|
|
493
|
+
<span>{songs.length} songs</span>
|
|
494
|
+
{songsFolder && <span>{songsFolder}</span>}
|
|
495
|
+
</div>
|
|
496
|
+
{scanProgress && (
|
|
497
|
+
<div className="flex flex-col gap-1 py-2">
|
|
498
|
+
<div className="w-full h-5 bg-gray-100 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded overflow-hidden">
|
|
499
|
+
<div
|
|
500
|
+
className="h-full bg-blue-600 transition-all duration-300"
|
|
501
|
+
style={{
|
|
502
|
+
width: `${scanProgress.total > 0 ? Math.round((scanProgress.current / scanProgress.total) * 100) : 0}%`,
|
|
503
|
+
}}
|
|
504
|
+
/>
|
|
505
|
+
</div>
|
|
506
|
+
<div className="text-xs text-gray-700 dark:text-gray-300 text-center">
|
|
507
|
+
{scanProgress.message ? (
|
|
508
|
+
scanProgress.message
|
|
509
|
+
) : (
|
|
510
|
+
<>
|
|
511
|
+
Scanning: {scanProgress.current} / {scanProgress.total} files (
|
|
512
|
+
{scanProgress.total > 0
|
|
513
|
+
? Math.round((scanProgress.current / scanProgress.total) * 100)
|
|
514
|
+
: 0}
|
|
515
|
+
%)
|
|
516
|
+
</>
|
|
517
|
+
)}
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
)}
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
{/* Alphabet Navigation */}
|
|
524
|
+
{!searchTerm && !scanProgress && (
|
|
525
|
+
<div className="flex flex-col gap-1 py-1 px-2 bg-gray-100 dark:bg-gray-800/50 rounded-md shrink-0">
|
|
526
|
+
<div className="text-xs font-semibold text-gray-600 dark:text-gray-300">
|
|
527
|
+
Browse by Artist:
|
|
528
|
+
</div>
|
|
529
|
+
<div className="flex flex-wrap gap-1">
|
|
530
|
+
{allLetters.map((letter) => {
|
|
531
|
+
const isAvailable = availableLetters.includes(letter);
|
|
532
|
+
const isActive = currentLetter === letter;
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<button
|
|
536
|
+
key={letter}
|
|
537
|
+
className={`w-8 h-8 p-0 rounded text-sm font-semibold cursor-pointer transition-all ${isActive ? 'bg-blue-600 border-blue-600 text-white' : isAvailable ? 'bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-750 hover:scale-105' : 'bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-400 dark:text-gray-600 opacity-30 cursor-not-allowed'}`}
|
|
538
|
+
onClick={() => isAvailable && loadLetterPage(letter, 1, songs)}
|
|
539
|
+
disabled={!isAvailable}
|
|
540
|
+
>
|
|
541
|
+
{letter}
|
|
542
|
+
</button>
|
|
543
|
+
);
|
|
544
|
+
})}
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
|
|
549
|
+
{/* Library Table */}
|
|
550
|
+
{loading ? (
|
|
551
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-3 p-16 text-center text-base text-gray-700 dark:text-gray-300">
|
|
552
|
+
Loading library...
|
|
553
|
+
</div>
|
|
554
|
+
) : filteredSongs.length > 0 ? (
|
|
555
|
+
<>
|
|
556
|
+
<div className="flex-1 min-h-0 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-md">
|
|
557
|
+
<table className="w-full border-collapse">
|
|
558
|
+
<thead className="sticky top-0 bg-gray-100 dark:bg-gray-800 z-10">
|
|
559
|
+
<tr>
|
|
560
|
+
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 border-b-2 border-gray-200 dark:border-gray-700">
|
|
561
|
+
Title
|
|
562
|
+
</th>
|
|
563
|
+
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 border-b-2 border-gray-200 dark:border-gray-700">
|
|
564
|
+
Artist
|
|
565
|
+
</th>
|
|
566
|
+
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 border-b-2 border-gray-200 dark:border-gray-700">
|
|
567
|
+
Album
|
|
568
|
+
</th>
|
|
569
|
+
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 border-b-2 border-gray-200 dark:border-gray-700">
|
|
570
|
+
Genre
|
|
571
|
+
</th>
|
|
572
|
+
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 border-b-2 border-gray-200 dark:border-gray-700">
|
|
573
|
+
Key
|
|
574
|
+
</th>
|
|
575
|
+
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 border-b-2 border-gray-200 dark:border-gray-700">
|
|
576
|
+
Duration
|
|
577
|
+
</th>
|
|
578
|
+
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 border-b-2 border-gray-200 dark:border-gray-700">
|
|
579
|
+
Year
|
|
580
|
+
</th>
|
|
581
|
+
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 border-b-2 border-gray-200 dark:border-gray-700">
|
|
582
|
+
Actions
|
|
583
|
+
</th>
|
|
584
|
+
</tr>
|
|
585
|
+
</thead>
|
|
586
|
+
<tbody className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
|
587
|
+
{currentPageSongs.map((song, index) => (
|
|
588
|
+
<tr key={index} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
|
589
|
+
<td className="px-3 py-1.5 text-xs leading-relaxed border-b border-gray-200 dark:border-gray-800/50">
|
|
590
|
+
<span className="mr-1.5 text-base">{getFormatIcon(song.format)}</span>
|
|
591
|
+
{song.title}
|
|
592
|
+
</td>
|
|
593
|
+
<td className="px-3 py-1.5 text-xs leading-relaxed border-b border-gray-200 dark:border-gray-800/50">
|
|
594
|
+
{song.artist}
|
|
595
|
+
</td>
|
|
596
|
+
<td className="px-3 py-1.5 text-xs leading-relaxed border-b border-gray-200 dark:border-gray-800/50">
|
|
597
|
+
{song.album || '-'}
|
|
598
|
+
</td>
|
|
599
|
+
<td className="px-3 py-1.5 text-xs leading-relaxed border-b border-gray-200 dark:border-gray-800/50">
|
|
600
|
+
{song.genre || '-'}
|
|
601
|
+
</td>
|
|
602
|
+
<td className="px-3 py-1.5 text-xs leading-relaxed border-b border-gray-200 dark:border-gray-800/50">
|
|
603
|
+
{song.key || '-'}
|
|
604
|
+
</td>
|
|
605
|
+
<td className="px-3 py-1.5 text-xs leading-relaxed border-b border-gray-200 dark:border-gray-800/50">
|
|
606
|
+
{formatDuration(song.duration)}
|
|
607
|
+
</td>
|
|
608
|
+
<td className="px-3 py-1.5 text-xs leading-relaxed border-b border-gray-200 dark:border-gray-800/50">
|
|
609
|
+
{song.year || '-'}
|
|
610
|
+
</td>
|
|
611
|
+
<td className="px-3 py-1.5 text-xs leading-relaxed border-b border-gray-200 dark:border-gray-800/50">
|
|
612
|
+
<div className="flex flex-row gap-1 items-center">
|
|
613
|
+
<button
|
|
614
|
+
className="w-7 h-7 min-w-[28px] min-h-[28px] max-w-[28px] max-h-[28px] p-0 flex items-center justify-center bg-transparent border border-gray-200 dark:border-gray-700 rounded text-gray-700 dark:text-white cursor-pointer transition-all flex-shrink-0 hover:bg-gray-100 dark:hover:bg-gray-800 hover:border-blue-600"
|
|
615
|
+
onClick={() => handleAddToQueue(song)}
|
|
616
|
+
title="Add to Queue"
|
|
617
|
+
>
|
|
618
|
+
<span className="material-icons text-base leading-none">
|
|
619
|
+
playlist_add
|
|
620
|
+
</span>
|
|
621
|
+
</button>
|
|
622
|
+
<button
|
|
623
|
+
className="w-7 h-7 min-w-[28px] min-h-[28px] max-w-[28px] max-h-[28px] p-0 flex items-center justify-center bg-transparent border border-gray-200 dark:border-gray-700 rounded text-gray-700 dark:text-white cursor-pointer transition-all flex-shrink-0 hover:bg-gray-100 dark:hover:bg-gray-800 hover:border-blue-600"
|
|
624
|
+
onClick={() => handleShowInfo(song)}
|
|
625
|
+
title="Song Info"
|
|
626
|
+
>
|
|
627
|
+
<span className="material-icons text-base leading-none">info</span>
|
|
628
|
+
</button>
|
|
629
|
+
</div>
|
|
630
|
+
</td>
|
|
631
|
+
</tr>
|
|
632
|
+
))}
|
|
633
|
+
</tbody>
|
|
634
|
+
</table>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
{/* Pagination */}
|
|
638
|
+
{totalPages > 1 && !scanProgress && (
|
|
639
|
+
<div className="flex items-center justify-center gap-3 py-1.5 px-2 bg-gray-100 dark:bg-gray-800/50 rounded-md text-sm shrink-0">
|
|
640
|
+
<button
|
|
641
|
+
className="px-4 py-1.5 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded text-gray-900 dark:text-white cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-750 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
642
|
+
onClick={() => setCurrentPage(currentPage - 1)}
|
|
643
|
+
disabled={currentPage === 1}
|
|
644
|
+
>
|
|
645
|
+
Previous
|
|
646
|
+
</button>
|
|
647
|
+
<div className="flex gap-1 items-center">
|
|
648
|
+
{getPageNumbers().map((page, index) => {
|
|
649
|
+
if (page === '...') {
|
|
650
|
+
return (
|
|
651
|
+
<span
|
|
652
|
+
key={`ellipsis-${index}`}
|
|
653
|
+
className="px-2 py-1.5 text-gray-700 dark:text-gray-300 select-none"
|
|
654
|
+
>
|
|
655
|
+
...
|
|
656
|
+
</span>
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
return (
|
|
660
|
+
<button
|
|
661
|
+
key={page}
|
|
662
|
+
className={`min-w-[36px] px-2 py-1.5 rounded cursor-pointer ${page === currentPage ? 'bg-blue-600 border-blue-600 font-semibold text-white' : 'bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-750'}`}
|
|
663
|
+
onClick={() => setCurrentPage(page)}
|
|
664
|
+
>
|
|
665
|
+
{page}
|
|
666
|
+
</button>
|
|
667
|
+
);
|
|
668
|
+
})}
|
|
669
|
+
</div>
|
|
670
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
671
|
+
({filteredSongs.length} songs)
|
|
672
|
+
</span>
|
|
673
|
+
<button
|
|
674
|
+
className="px-4 py-1.5 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded text-gray-900 dark:text-white cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-750 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
675
|
+
onClick={() => setCurrentPage(currentPage + 1)}
|
|
676
|
+
disabled={currentPage === totalPages}
|
|
677
|
+
>
|
|
678
|
+
Next
|
|
679
|
+
</button>
|
|
680
|
+
</div>
|
|
681
|
+
)}
|
|
682
|
+
</>
|
|
683
|
+
) : (
|
|
684
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-3 p-16 text-center">
|
|
685
|
+
<div className="text-5xl opacity-50">🎵</div>
|
|
686
|
+
<div className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
687
|
+
{songsFolder ? 'No songs found' : 'No songs library set'}
|
|
688
|
+
</div>
|
|
689
|
+
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
690
|
+
{songsFolder
|
|
691
|
+
? 'Try syncing or refreshing your library'
|
|
692
|
+
: 'Click "Set Songs Folder" to choose your music library'}
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
)}
|
|
696
|
+
|
|
697
|
+
{/* Song Info Modal */}
|
|
698
|
+
{modalSong && <SongInfoModal song={modalSong} onClose={() => setModalSong(null)} />}
|
|
699
|
+
</div>
|
|
700
|
+
);
|
|
701
|
+
}
|