1236 lines
55 KiB
JavaScript
1236 lines
55 KiB
JavaScript
import { debounce } from './utils.js';
|
|
import { db } from './db.js';
|
|
import Fuse from 'fuse.js';
|
|
import { navigate } from './router.js';
|
|
|
|
const ICONS = {
|
|
search: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>',
|
|
house: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>',
|
|
library:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg>',
|
|
clock: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
|
|
calendar:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></svg>',
|
|
settings:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>',
|
|
info: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
|
|
download:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>',
|
|
heart: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>',
|
|
play: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3"/></svg>',
|
|
pause: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/></svg>',
|
|
skipForward:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" x2="19" y1="5" y2="19"/></svg>',
|
|
skipBack:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="19 20 9 12 19 4 19 20"/><line x1="5" x2="5" y1="19" y2="5"/></svg>',
|
|
shuffle:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 18h1.4c1.3 0 2.5-.6 3.3-1.7l6.1-8.6c.7-1.1 2-1.7 3.3-1.7H22"/><path d="m18 2 4 4-4 4"/><path d="M2 6h1.9c1.5 0 2.9.9 3.6 2.2"/><path d="M22 18h-5.9c-1.3 0-2.6-.7-3.3-1.8l-.5-.8"/><path d="m18 14 4 4-4 4"/></svg>',
|
|
repeat: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>',
|
|
volumeX:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/><line x1="22" x2="16" y1="9" y2="15"/><line x1="16" x2="22" y1="9" y2="15"/></svg>',
|
|
volume: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/><path d="M16 9a5 5 0 0 1 0 6"/></svg>',
|
|
list: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>',
|
|
trash: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>',
|
|
text: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 6.1H3"/><path d="M21 12.1H3"/><path d="M15.1 18H3"/></svg>',
|
|
maximize:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>',
|
|
sparkles:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>',
|
|
palette:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>',
|
|
sun: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>',
|
|
moon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>',
|
|
sliders:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="4" y1="21" y2="14"/><line x1="4" x2="4" y1="10" y2="3"/><line x1="12" x2="12" y1="21" y2="12"/><line x1="12" x2="12" y1="8" y2="3"/><line x1="20" x2="20" y1="21" y2="16"/><line x1="20" x2="20" y1="12" y2="3"/><line x1="2" x2="6" y1="14" y2="14"/><line x1="10" x2="14" y1="8" y2="8"/><line x1="18" x2="22" y1="16" y2="16"/></svg>',
|
|
plus: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>',
|
|
folderPlus:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 10v6"/><path d="M9 13h6"/><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>',
|
|
user: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
|
|
logOut: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>',
|
|
logIn: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" x2="3" y1="12" y2="12"/></svg>',
|
|
keyboard:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="M6 8h.001"/><path d="M10 8h.001"/><path d="M14 8h.001"/><path d="M18 8h.001"/><path d="M8 12h.001"/><path d="M12 12h.001"/><path d="M16 12h.001"/><path d="M7 16h10"/></svg>',
|
|
music: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>',
|
|
disc: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="2"/></svg>',
|
|
mic: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>',
|
|
upload: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>',
|
|
handHeart:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 14h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 16"/><path d="m7 20 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"/><path d="m2 15 6 6"/><path d="M19.5 8.5c.7-.7 1.5-1.6 1.5-2.7A2.73 2.73 0 0 0 16 4a2.78 2.78 0 0 0-5 1.8c0 1.2.8 2 1.5 2.8L16 12Z"/></svg>',
|
|
monitor:
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>',
|
|
pencil: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>',
|
|
radio: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>',
|
|
store: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"/><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"/><path d="M2 7h20"/><path d="M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"/></svg>',
|
|
};
|
|
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
class CommandPalette {
|
|
constructor() {
|
|
this.overlay = document.getElementById('command-palette-overlay');
|
|
this.input = document.getElementById('command-palette-input');
|
|
this.resultsContainer = document.getElementById('command-palette-results');
|
|
this.isOpen = false;
|
|
this.selectedIndex = 0;
|
|
this.flatItems = [];
|
|
this.allSettings = [];
|
|
this.musicSearchAbort = null;
|
|
this.debouncedMusicSearch = debounce(this.searchMusic.bind(this), 300);
|
|
this.commands = this.buildCommands();
|
|
this.fuse = new Fuse(this.commands, {
|
|
keys: [
|
|
{ name: 'label', weight: 0.6 },
|
|
{ name: 'keywords', weight: 0.3 },
|
|
{ name: 'group', weight: 0.1 },
|
|
],
|
|
threshold: 0.4,
|
|
ignoreLocation: true,
|
|
includeScore: true,
|
|
});
|
|
|
|
this.init();
|
|
}
|
|
|
|
buildCommands() {
|
|
return [
|
|
{
|
|
id: 'nav-home',
|
|
group: 'Navigation',
|
|
icon: 'house',
|
|
label: 'Go to Home',
|
|
keywords: ['home', 'main', 'start', 'landing'],
|
|
action: () => {
|
|
navigate('/');
|
|
},
|
|
},
|
|
{
|
|
id: 'nav-library',
|
|
group: 'Navigation',
|
|
icon: 'library',
|
|
label: 'Go to Library',
|
|
keywords: ['library', 'collection', 'playlists', 'favorites'],
|
|
action: () => {
|
|
navigate('/library');
|
|
},
|
|
},
|
|
{
|
|
id: 'nav-recent',
|
|
group: 'Navigation',
|
|
icon: 'clock',
|
|
label: 'Go to Recent',
|
|
keywords: ['recent', 'history', 'last played'],
|
|
action: () => {
|
|
navigate('/recent');
|
|
},
|
|
},
|
|
{
|
|
id: 'nav-unreleased',
|
|
group: 'Navigation',
|
|
icon: 'calendar',
|
|
label: 'Go to Unreleased',
|
|
keywords: ['unreleased', 'upcoming', 'tracker'],
|
|
action: () => {
|
|
navigate('/unreleased');
|
|
},
|
|
},
|
|
{
|
|
id: 'nav-settings',
|
|
group: 'Navigation',
|
|
icon: 'settings',
|
|
label: 'Go to Settings',
|
|
keywords: ['settings', 'preferences', 'config', 'options'],
|
|
shortcut: null,
|
|
action: () => {
|
|
navigate('/settings');
|
|
},
|
|
},
|
|
{
|
|
id: 'nav-about',
|
|
group: 'Navigation',
|
|
icon: 'info',
|
|
label: 'Go to About',
|
|
keywords: ['about', 'version', 'credits'],
|
|
action: () => {
|
|
navigate('/about');
|
|
},
|
|
},
|
|
{
|
|
id: 'nav-download',
|
|
group: 'Navigation',
|
|
icon: 'download',
|
|
label: 'Go to Download',
|
|
keywords: ['download', 'desktop', 'app'],
|
|
action: () => {
|
|
navigate('/download');
|
|
},
|
|
},
|
|
{
|
|
id: 'nav-donate',
|
|
group: 'Navigation',
|
|
icon: 'handHeart',
|
|
label: 'Go to Donate',
|
|
keywords: ['donate', 'support', 'contribute'],
|
|
action: () => {
|
|
navigate('/donate');
|
|
},
|
|
},
|
|
|
|
{
|
|
id: 'play-pause',
|
|
group: 'Playback',
|
|
icon: 'play',
|
|
label: 'Play / Pause',
|
|
keywords: ['play', 'pause', 'toggle', 'resume', 'stop'],
|
|
shortcut: 'Space',
|
|
action: () => {
|
|
window.monochromePlayer?.handlePlayPause();
|
|
},
|
|
},
|
|
{
|
|
id: 'play-next',
|
|
group: 'Playback',
|
|
icon: 'skipForward',
|
|
label: 'Next Track',
|
|
keywords: ['next', 'skip', 'forward'],
|
|
shortcut: 'Shift+→',
|
|
action: () => {
|
|
window.monochromePlayer?.playNext();
|
|
},
|
|
},
|
|
{
|
|
id: 'play-prev',
|
|
group: 'Playback',
|
|
icon: 'skipBack',
|
|
label: 'Previous Track',
|
|
keywords: ['previous', 'back', 'rewind'],
|
|
shortcut: 'Shift+←',
|
|
action: () => {
|
|
window.monochromePlayer?.playPrev();
|
|
},
|
|
},
|
|
{
|
|
id: 'play-shuffle',
|
|
group: 'Playback',
|
|
icon: 'shuffle',
|
|
label: 'Toggle Shuffle',
|
|
keywords: ['shuffle', 'random'],
|
|
shortcut: 'S',
|
|
action: () => {
|
|
document.getElementById('shuffle-btn')?.click();
|
|
},
|
|
},
|
|
{
|
|
id: 'play-repeat',
|
|
group: 'Playback',
|
|
icon: 'repeat',
|
|
label: 'Toggle Repeat',
|
|
keywords: ['repeat', 'loop', 'cycle'],
|
|
shortcut: 'R',
|
|
action: () => {
|
|
document.getElementById('repeat-btn')?.click();
|
|
},
|
|
},
|
|
{
|
|
id: 'play-mute',
|
|
group: 'Playback',
|
|
icon: 'volumeX',
|
|
label: 'Mute / Unmute',
|
|
keywords: ['mute', 'unmute', 'sound', 'volume', 'silent'],
|
|
shortcut: 'M',
|
|
action: () => {
|
|
const el = window.monochromePlayer?.activeElement;
|
|
if (el) el.muted = !el.muted;
|
|
},
|
|
},
|
|
{
|
|
id: 'play-vol-up',
|
|
group: 'Playback',
|
|
icon: 'volume',
|
|
label: 'Volume Up',
|
|
keywords: ['volume', 'louder'],
|
|
shortcut: '↑',
|
|
action: () => {
|
|
const p = window.monochromePlayer;
|
|
if (p) p.setVolume(p.userVolume + 0.1);
|
|
},
|
|
},
|
|
{
|
|
id: 'play-vol-down',
|
|
group: 'Playback',
|
|
icon: 'volume',
|
|
label: 'Volume Down',
|
|
keywords: ['volume', 'quieter', 'softer'],
|
|
shortcut: '↓',
|
|
action: () => {
|
|
const p = window.monochromePlayer;
|
|
if (p) p.setVolume(p.userVolume - 0.1);
|
|
},
|
|
},
|
|
|
|
{
|
|
id: 'like-current',
|
|
group: 'Now Playing',
|
|
icon: 'heart',
|
|
label: 'Like Current Track',
|
|
keywords: ['like', 'favorite', 'love', 'heart', 'save'],
|
|
action: () => {
|
|
document.querySelector('.now-playing-bar .like-btn')?.click();
|
|
},
|
|
},
|
|
{
|
|
id: 'download-current',
|
|
group: 'Now Playing',
|
|
icon: 'download',
|
|
label: 'Download Current Track',
|
|
keywords: ['download', 'save', 'current'],
|
|
action: () => {
|
|
document.querySelector('.now-playing-bar .download-btn')?.click();
|
|
},
|
|
},
|
|
|
|
{
|
|
id: 'queue-open',
|
|
group: 'Queue',
|
|
icon: 'list',
|
|
label: 'Open Queue',
|
|
keywords: ['queue', 'list', 'up next'],
|
|
shortcut: 'Q',
|
|
action: () => {
|
|
document.getElementById('queue-btn')?.click();
|
|
},
|
|
},
|
|
{
|
|
id: 'queue-wipe',
|
|
group: 'Queue',
|
|
icon: 'trash',
|
|
label: 'Clear Queue',
|
|
keywords: ['wipe', 'clear', 'empty', 'queue'],
|
|
action: () => {
|
|
window.monochromePlayer?.wipeQueue();
|
|
this.notify('Queue cleared');
|
|
},
|
|
},
|
|
{
|
|
id: 'queue-like-all',
|
|
group: 'Queue',
|
|
icon: 'heart',
|
|
label: 'Like All in Queue',
|
|
keywords: ['like', 'all', 'queue', 'heart', 'favorite'],
|
|
action: () => this.likeAllInQueue(),
|
|
},
|
|
{
|
|
id: 'queue-download',
|
|
group: 'Queue',
|
|
icon: 'download',
|
|
label: 'Download Queue',
|
|
keywords: ['download', 'queue', 'save', 'all'],
|
|
action: () => this.downloadQueue(),
|
|
},
|
|
|
|
{
|
|
id: 'lyrics-toggle',
|
|
group: 'View',
|
|
icon: 'text',
|
|
label: 'Toggle Lyrics',
|
|
keywords: ['lyrics', 'words', 'text', 'karaoke'],
|
|
shortcut: 'L',
|
|
action: () => {
|
|
document.querySelector('.now-playing-bar .cover')?.click();
|
|
},
|
|
},
|
|
{
|
|
id: 'fullscreen-open',
|
|
group: 'View',
|
|
icon: 'maximize',
|
|
label: 'Open Fullscreen View',
|
|
keywords: ['fullscreen', 'expand', 'immersive', 'cover'],
|
|
action: () => {
|
|
const cover = document.querySelector('.now-playing-bar .cover-art');
|
|
if (cover) cover.click();
|
|
},
|
|
},
|
|
{
|
|
id: 'vis-toggle',
|
|
group: 'View',
|
|
icon: 'sparkles',
|
|
label: 'Toggle Visualizer',
|
|
keywords: ['visualizer', 'visual', 'animation', 'effects'],
|
|
action: () => this.toggleVisualizer(),
|
|
},
|
|
{
|
|
id: 'vis-butterchurn',
|
|
group: 'View',
|
|
icon: 'sparkles',
|
|
label: 'Visualizer: Butterchurn',
|
|
keywords: ['butterchurn', 'milkdrop', 'preset', 'visualizer'],
|
|
action: () => this.setVisualizerPreset('butterchurn'),
|
|
},
|
|
{
|
|
id: 'vis-kawarp',
|
|
group: 'View',
|
|
icon: 'sparkles',
|
|
label: 'Visualizer: Kawarp',
|
|
keywords: ['kawarp', 'preset', 'visualizer'],
|
|
action: () => this.setVisualizerPreset('kawarp'),
|
|
},
|
|
{
|
|
id: 'vis-lcd',
|
|
group: 'View',
|
|
icon: 'sparkles',
|
|
label: 'Visualizer: LCD',
|
|
keywords: ['lcd', 'preset', 'visualizer'],
|
|
action: () => this.setVisualizerPreset('lcd'),
|
|
},
|
|
{
|
|
id: 'vis-particles',
|
|
group: 'View',
|
|
icon: 'sparkles',
|
|
label: 'Visualizer: Particles',
|
|
keywords: ['particles', 'preset', 'visualizer'],
|
|
action: () => this.setVisualizerPreset('particles'),
|
|
},
|
|
{
|
|
id: 'vis-unknown',
|
|
group: 'View',
|
|
icon: 'sparkles',
|
|
label: 'Visualizer: Unknown Pleasures',
|
|
keywords: ['unknown pleasures', 'preset', 'visualizer', 'joy division'],
|
|
action: () => this.setVisualizerPreset('unknown-pleasures'),
|
|
},
|
|
|
|
{
|
|
id: 'theme-system',
|
|
group: 'Theme',
|
|
icon: 'monitor',
|
|
label: 'Theme: System',
|
|
keywords: ['theme', 'system', 'auto', 'default'],
|
|
action: () => this.setTheme('system'),
|
|
},
|
|
{
|
|
id: 'theme-black',
|
|
group: 'Theme',
|
|
icon: 'moon',
|
|
label: 'Theme: Monochrome',
|
|
keywords: ['theme', 'monochrome', 'black', 'dark', 'amoled'],
|
|
action: () => this.setTheme('monochrome'),
|
|
},
|
|
{
|
|
id: 'theme-dark',
|
|
group: 'Theme',
|
|
icon: 'moon',
|
|
label: 'Theme: Dark',
|
|
keywords: ['theme', 'dark'],
|
|
action: () => this.setTheme('dark'),
|
|
},
|
|
{
|
|
id: 'theme-white',
|
|
group: 'Theme',
|
|
icon: 'sun',
|
|
label: 'Theme: White',
|
|
keywords: ['theme', 'white', 'light'],
|
|
action: () => this.setTheme('white'),
|
|
},
|
|
{
|
|
id: 'theme-ocean',
|
|
group: 'Theme',
|
|
icon: 'palette',
|
|
label: 'Theme: Ocean',
|
|
keywords: ['theme', 'ocean', 'blue', 'sea'],
|
|
action: () => this.setTheme('ocean'),
|
|
},
|
|
{
|
|
id: 'theme-purple',
|
|
group: 'Theme',
|
|
icon: 'palette',
|
|
label: 'Theme: Purple',
|
|
keywords: ['theme', 'purple', 'violet'],
|
|
action: () => this.setTheme('purple'),
|
|
},
|
|
{
|
|
id: 'theme-forest',
|
|
group: 'Theme',
|
|
icon: 'palette',
|
|
label: 'Theme: Forest',
|
|
keywords: ['theme', 'forest', 'green', 'nature'],
|
|
action: () => this.setTheme('forest'),
|
|
},
|
|
{
|
|
id: 'theme-mocha',
|
|
group: 'Theme',
|
|
icon: 'palette',
|
|
label: 'Theme: Mocha',
|
|
keywords: ['theme', 'mocha', 'catppuccin', 'brown', 'warm'],
|
|
action: () => this.setTheme('mocha'),
|
|
},
|
|
{
|
|
id: 'theme-macchiato',
|
|
group: 'Theme',
|
|
icon: 'palette',
|
|
label: 'Theme: Macchiato',
|
|
keywords: ['theme', 'macchiato', 'catppuccin'],
|
|
action: () => this.setTheme('machiatto'),
|
|
},
|
|
{
|
|
id: 'theme-frappe',
|
|
group: 'Theme',
|
|
icon: 'palette',
|
|
label: 'Theme: Frappé',
|
|
keywords: ['theme', 'frappe', 'catppuccin'],
|
|
action: () => this.setTheme('frappe'),
|
|
},
|
|
{
|
|
id: 'theme-latte',
|
|
group: 'Theme',
|
|
icon: 'palette',
|
|
label: 'Theme: Latte',
|
|
keywords: ['theme', 'latte', 'catppuccin', 'light'],
|
|
action: () => this.setTheme('latte'),
|
|
},
|
|
{
|
|
id: 'theme-store',
|
|
group: 'Theme',
|
|
icon: 'store',
|
|
label: 'Open Theme Store',
|
|
keywords: ['theme', 'store', 'browse', 'community', 'custom'],
|
|
action: () => {
|
|
document.getElementById('open-theme-store')?.click();
|
|
},
|
|
},
|
|
|
|
{
|
|
id: 'quality-low',
|
|
group: 'Audio',
|
|
icon: 'sliders',
|
|
label: 'Quality: Low',
|
|
keywords: ['quality', 'low', 'streaming', 'bitrate'],
|
|
action: () => this.setQuality('LOW'),
|
|
},
|
|
{
|
|
id: 'quality-high',
|
|
group: 'Audio',
|
|
icon: 'sliders',
|
|
label: 'Quality: High',
|
|
keywords: ['quality', 'high', 'streaming', 'bitrate'],
|
|
action: () => this.setQuality('HIGH'),
|
|
},
|
|
{
|
|
id: 'quality-lossless',
|
|
group: 'Audio',
|
|
icon: 'sliders',
|
|
label: 'Quality: Lossless',
|
|
keywords: ['quality', 'lossless', 'flac', 'cd', 'streaming'],
|
|
action: () => this.setQuality('LOSSLESS'),
|
|
},
|
|
{
|
|
id: 'quality-hires',
|
|
group: 'Audio',
|
|
icon: 'sliders',
|
|
label: 'Quality: Hi-Res',
|
|
keywords: ['quality', 'hires', 'hi-res', 'master', 'mqa', 'streaming'],
|
|
action: () => this.setQuality('HI_RES_LOSSLESS'),
|
|
},
|
|
{
|
|
id: 'sleep-15',
|
|
group: 'Audio',
|
|
icon: 'clock',
|
|
label: 'Sleep Timer: 15 min',
|
|
keywords: ['sleep', 'timer', '15', 'minutes'],
|
|
action: () => this.setSleepTimer(15),
|
|
},
|
|
{
|
|
id: 'sleep-30',
|
|
group: 'Audio',
|
|
icon: 'clock',
|
|
label: 'Sleep Timer: 30 min',
|
|
keywords: ['sleep', 'timer', '30', 'minutes'],
|
|
action: () => this.setSleepTimer(30),
|
|
},
|
|
{
|
|
id: 'sleep-60',
|
|
group: 'Audio',
|
|
icon: 'clock',
|
|
label: 'Sleep Timer: 60 min',
|
|
keywords: ['sleep', 'timer', '60', 'minutes', 'hour'],
|
|
action: () => this.setSleepTimer(60),
|
|
},
|
|
{
|
|
id: 'sleep-120',
|
|
group: 'Audio',
|
|
icon: 'clock',
|
|
label: 'Sleep Timer: 120 min',
|
|
keywords: ['sleep', 'timer', '120', 'minutes', 'hours'],
|
|
action: () => this.setSleepTimer(120),
|
|
},
|
|
|
|
{
|
|
id: 'lib-create-playlist',
|
|
group: 'Library',
|
|
icon: 'plus',
|
|
label: 'Create Playlist',
|
|
keywords: ['create', 'new', 'playlist', 'add'],
|
|
action: () => this.createPlaylist(),
|
|
},
|
|
{
|
|
id: 'lib-create-folder',
|
|
group: 'Library',
|
|
icon: 'folderPlus',
|
|
label: 'Create Folder',
|
|
keywords: ['create', 'new', 'folder', 'add', 'organize'],
|
|
action: () => this.createFolder(),
|
|
},
|
|
|
|
{
|
|
id: 'sys-cache',
|
|
group: 'System',
|
|
icon: 'trash',
|
|
label: 'Clear Cache',
|
|
keywords: ['cache', 'clear', 'reset', 'clean'],
|
|
action: () => this.clearCache(),
|
|
},
|
|
{
|
|
id: 'sys-shortcuts',
|
|
group: 'System',
|
|
icon: 'keyboard',
|
|
label: 'View Keyboard Shortcuts',
|
|
keywords: ['keyboard', 'shortcuts', 'keys', 'hotkeys', 'bindings'],
|
|
action: () => {
|
|
document.getElementById('shortcuts-modal')?.style.setProperty('display', 'flex');
|
|
},
|
|
},
|
|
{
|
|
id: 'sys-export',
|
|
group: 'System',
|
|
icon: 'upload',
|
|
label: 'Export Data',
|
|
keywords: ['export', 'backup', 'data', 'save'],
|
|
action: () => this.navigateToSetting({ tab: 'system', id: 'export-data-btn' }),
|
|
},
|
|
{
|
|
id: 'sys-search-setting',
|
|
group: 'System',
|
|
icon: 'search',
|
|
label: 'Search Settings...',
|
|
keywords: ['setting', 'find', 'search', 'preference', 'option', 'configure'],
|
|
action: () => this.enterSettingsMode(),
|
|
},
|
|
|
|
{
|
|
id: 'acc-profile',
|
|
group: 'Account',
|
|
icon: 'user',
|
|
label: 'View Profile',
|
|
keywords: ['profile', 'account', 'user', 'me'],
|
|
action: () => {
|
|
document.querySelector('.user-avatar-btn')?.click();
|
|
},
|
|
},
|
|
{
|
|
id: 'acc-edit-profile',
|
|
group: 'Account',
|
|
icon: 'pencil',
|
|
label: 'Edit Profile',
|
|
keywords: ['edit', 'profile', 'username', 'avatar', 'display name'],
|
|
action: async () => {
|
|
const { openEditProfile } = await import('./profile.js');
|
|
openEditProfile();
|
|
},
|
|
},
|
|
{
|
|
id: 'acc-sign-out',
|
|
group: 'Account',
|
|
icon: 'logOut',
|
|
label: 'Sign Out',
|
|
keywords: ['sign out', 'log out', 'logout', 'disconnect'],
|
|
action: async () => {
|
|
const { authManager } = await import('./accounts/auth.js');
|
|
await authManager.signOut();
|
|
},
|
|
},
|
|
{
|
|
id: 'acc-sign-in',
|
|
group: 'Account',
|
|
icon: 'logIn',
|
|
label: 'Sign In',
|
|
keywords: ['sign in', 'log in', 'login', 'account', 'connect'],
|
|
action: () => {
|
|
navigate('/account');
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
init() {
|
|
document.addEventListener('keydown', (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
this.toggle();
|
|
}
|
|
});
|
|
|
|
this.input.addEventListener('input', () => this.handleInput());
|
|
this.input.addEventListener('keydown', (e) => this.handleKeydown(e));
|
|
|
|
this.overlay.addEventListener('click', (e) => {
|
|
if (e.target === this.overlay) this.close();
|
|
});
|
|
|
|
this.cacheAllSettings();
|
|
}
|
|
|
|
toggle() {
|
|
if (this.isOpen) this.close();
|
|
else this.open();
|
|
}
|
|
|
|
open() {
|
|
this.isOpen = true;
|
|
this.settingsMode = false;
|
|
this.overlay.style.display = 'flex';
|
|
this.input.value = '';
|
|
this.input.placeholder = 'Search commands, music, settings...';
|
|
this.input.focus();
|
|
this.showDefaultCommands();
|
|
}
|
|
|
|
close() {
|
|
this.isOpen = false;
|
|
this.settingsMode = false;
|
|
this.overlay.style.display = 'none';
|
|
this.cancelMusicSearch();
|
|
}
|
|
|
|
enterSettingsMode() {
|
|
this.settingsMode = true;
|
|
this.input.value = '';
|
|
this.input.placeholder = 'Search settings...';
|
|
this.input.focus();
|
|
this.cacheAllSettings();
|
|
this.renderSettingsResults('');
|
|
}
|
|
|
|
handleInput() {
|
|
const query = this.input.value.trim();
|
|
this.selectedIndex = 0;
|
|
|
|
if (this.settingsMode) {
|
|
this.renderSettingsResults(query);
|
|
return;
|
|
}
|
|
|
|
if (!query) {
|
|
this.cancelMusicSearch();
|
|
this.showDefaultCommands();
|
|
return;
|
|
}
|
|
|
|
this.searchCommands(query);
|
|
this.debouncedMusicSearch(query);
|
|
}
|
|
|
|
handleKeydown(e) {
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
this.selectedIndex = Math.min(this.selectedIndex + 1, this.flatItems.length - 1);
|
|
this.updateSelection();
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
|
this.updateSelection();
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
this.executeSelected();
|
|
} else if (e.key === 'Escape') {
|
|
if (this.settingsMode) {
|
|
this.settingsMode = false;
|
|
this.input.value = '';
|
|
this.input.placeholder = 'Search commands, music, settings...';
|
|
this.showDefaultCommands();
|
|
} else {
|
|
this.close();
|
|
}
|
|
} else if (e.key === 'Backspace' && this.settingsMode && !this.input.value) {
|
|
this.settingsMode = false;
|
|
this.input.placeholder = 'Search commands, music, settings...';
|
|
this.showDefaultCommands();
|
|
}
|
|
}
|
|
|
|
showDefaultCommands() {
|
|
const groups = this.groupBy(
|
|
this.commands.filter((c) => {
|
|
const priority = [
|
|
'nav-home',
|
|
'nav-library',
|
|
'nav-settings',
|
|
'play-pause',
|
|
'play-next',
|
|
'play-prev',
|
|
'play-shuffle',
|
|
'queue-open',
|
|
'lyrics-toggle',
|
|
'fullscreen-open',
|
|
'sys-search-setting',
|
|
];
|
|
return priority.includes(c.id);
|
|
}),
|
|
'group'
|
|
);
|
|
|
|
this.renderGroups(groups);
|
|
}
|
|
|
|
searchCommands(query) {
|
|
const fuseResults = this.fuse.search(query).slice(0, 12);
|
|
const matched = fuseResults.map((r) => r.item);
|
|
|
|
if (matched.length === 0) {
|
|
this.renderGroups({});
|
|
return;
|
|
}
|
|
|
|
const groups = this.groupBy(matched, 'group');
|
|
this.renderGroups(groups);
|
|
}
|
|
|
|
async searchMusic(query) {
|
|
if (!query || query.length < 2) return;
|
|
|
|
const api = window.monochromeUi?.api;
|
|
if (!api) return;
|
|
|
|
this.cancelMusicSearch();
|
|
const controller = new AbortController();
|
|
this.musicSearchAbort = controller;
|
|
|
|
this.showMusicLoading();
|
|
|
|
try {
|
|
const [tracks, albums, artists] = await Promise.all([
|
|
api.searchTracks(query, { limit: 4 }),
|
|
api.searchAlbums(query, { limit: 3 }),
|
|
api.searchArtists(query, { limit: 3 }),
|
|
]);
|
|
|
|
if (controller.signal.aborted || !this.isOpen) return;
|
|
|
|
const musicGroups = {};
|
|
|
|
if (tracks?.items?.length) {
|
|
musicGroups['Tracks'] = tracks.items.map((track) => ({
|
|
id: `track-${track.id}`,
|
|
group: 'Tracks',
|
|
icon: null,
|
|
image: api.getCoverUrl(track.album?.cover, 80),
|
|
label: track.title,
|
|
description: `${track.artist?.name || 'Unknown'} \u2022 ${track.album?.title || ''}`,
|
|
action: async () => {
|
|
window.monochromePlayer.setQueue([track], 0);
|
|
await window.monochromePlayer.playTrackFromQueue();
|
|
},
|
|
}));
|
|
}
|
|
|
|
if (albums?.items?.length) {
|
|
musicGroups['Albums'] = albums.items.map((album) => ({
|
|
id: `album-${album.id}`,
|
|
group: 'Albums',
|
|
icon: null,
|
|
image: api.getCoverUrl(album.cover, 80),
|
|
label: album.title,
|
|
description: album.artist?.name || 'Unknown',
|
|
action: () => {
|
|
navigate(`/album/${album.id}`);
|
|
},
|
|
}));
|
|
}
|
|
|
|
if (artists?.items?.length) {
|
|
musicGroups['Artists'] = artists.items.map((artist) => ({
|
|
id: `artist-${artist.id}`,
|
|
group: 'Artists',
|
|
icon: null,
|
|
image: api.getArtistPictureUrl(artist.picture, 80),
|
|
label: artist.name,
|
|
description: 'Artist',
|
|
action: () => {
|
|
navigate(`/artist/${artist.id}`);
|
|
},
|
|
}));
|
|
}
|
|
|
|
if (Object.keys(musicGroups).length > 0) {
|
|
this.appendMusicGroups(musicGroups);
|
|
}
|
|
|
|
this.removeMusicLoading();
|
|
} catch (e) {
|
|
if (e.name !== 'AbortError') {
|
|
this.removeMusicLoading();
|
|
}
|
|
}
|
|
}
|
|
|
|
cancelMusicSearch() {
|
|
if (this.musicSearchAbort) {
|
|
this.musicSearchAbort.abort();
|
|
this.musicSearchAbort = null;
|
|
}
|
|
}
|
|
|
|
showMusicLoading() {
|
|
this.removeMusicLoading();
|
|
const loading = document.createElement('div');
|
|
loading.className = 'cmdk-loading';
|
|
loading.setAttribute('data-music-loading', '');
|
|
loading.innerHTML = '<div class="cmdk-loading-spinner"></div>Searching music...';
|
|
this.resultsContainer.appendChild(loading);
|
|
}
|
|
|
|
removeMusicLoading() {
|
|
this.resultsContainer.querySelector('[data-music-loading]')?.remove();
|
|
}
|
|
|
|
appendMusicGroups(musicGroups) {
|
|
this.removeMusicLoading();
|
|
this.resultsContainer.querySelectorAll('[data-music-group]').forEach((el) => el.remove());
|
|
|
|
const startIndex = this.flatItems.length;
|
|
let index = startIndex;
|
|
|
|
for (const [heading, items] of Object.entries(musicGroups)) {
|
|
const groupEl = document.createElement('div');
|
|
groupEl.className = 'cmdk-group';
|
|
groupEl.setAttribute('data-music-group', '');
|
|
|
|
const headingEl = document.createElement('div');
|
|
headingEl.className = 'cmdk-group-heading';
|
|
headingEl.textContent = heading;
|
|
groupEl.appendChild(headingEl);
|
|
|
|
for (const item of items) {
|
|
const itemEl = this.createItemElement(item, index);
|
|
groupEl.appendChild(itemEl);
|
|
this.flatItems.push(item);
|
|
index++;
|
|
}
|
|
|
|
this.resultsContainer.appendChild(groupEl);
|
|
}
|
|
}
|
|
|
|
groupBy(items, key) {
|
|
const groups = {};
|
|
for (const item of items) {
|
|
const group = item[key] || 'Other';
|
|
if (!groups[group]) groups[group] = [];
|
|
groups[group].push(item);
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
renderGroups(groups) {
|
|
this.resultsContainer.innerHTML = '';
|
|
this.flatItems = [];
|
|
let index = 0;
|
|
|
|
const groupEntries = Object.entries(groups);
|
|
if (groupEntries.length === 0) {
|
|
const query = this.input.value.trim();
|
|
if (query) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'cmdk-empty';
|
|
empty.textContent = 'No commands found';
|
|
this.resultsContainer.appendChild(empty);
|
|
}
|
|
return;
|
|
}
|
|
|
|
for (const [heading, items] of groupEntries) {
|
|
const groupEl = document.createElement('div');
|
|
groupEl.className = 'cmdk-group';
|
|
|
|
const headingEl = document.createElement('div');
|
|
headingEl.className = 'cmdk-group-heading';
|
|
headingEl.textContent = heading;
|
|
groupEl.appendChild(headingEl);
|
|
|
|
for (const item of items) {
|
|
const itemEl = this.createItemElement(item, index);
|
|
groupEl.appendChild(itemEl);
|
|
this.flatItems.push(item);
|
|
index++;
|
|
}
|
|
|
|
this.resultsContainer.appendChild(groupEl);
|
|
}
|
|
|
|
this.updateSelection();
|
|
}
|
|
|
|
createItemElement(item, index) {
|
|
const el = document.createElement('div');
|
|
el.className = 'cmdk-item';
|
|
el.setAttribute('data-index', index);
|
|
if (index === this.selectedIndex) el.setAttribute('data-selected', 'true');
|
|
|
|
let iconHtml = '';
|
|
if (item.image) {
|
|
iconHtml = `<div class="cmdk-item-icon"><img src="${escapeHtml(item.image)}" crossorigin="anonymous" alt="" loading="lazy" /></div>`;
|
|
} else if (item.icon && ICONS[item.icon]) {
|
|
iconHtml = `<div class="cmdk-item-icon">${ICONS[item.icon]}</div>`;
|
|
}
|
|
|
|
let shortcutHtml = '';
|
|
if (item.shortcut) {
|
|
const keys = item.shortcut.split('+');
|
|
shortcutHtml = `<div class="cmdk-item-shortcut">${keys.map((k) => `<kbd>${escapeHtml(k)}</kbd>`).join('')}</div>`;
|
|
}
|
|
|
|
const descHtml = item.description
|
|
? `<span class="cmdk-item-description">${escapeHtml(item.description)}</span>`
|
|
: '';
|
|
|
|
el.innerHTML = `${iconHtml}<div class="cmdk-item-content"><span class="cmdk-item-label">${escapeHtml(item.label)}</span>${descHtml}</div>${shortcutHtml}`;
|
|
|
|
el.addEventListener('click', () => {
|
|
this.selectedIndex = index;
|
|
this.executeSelected();
|
|
});
|
|
|
|
el.addEventListener('mouseenter', () => {
|
|
this.selectedIndex = index;
|
|
this.updateSelection();
|
|
});
|
|
|
|
return el;
|
|
}
|
|
|
|
updateSelection() {
|
|
const items = this.resultsContainer.querySelectorAll('.cmdk-item');
|
|
items.forEach((item) => {
|
|
const idx = parseInt(item.getAttribute('data-index'));
|
|
if (idx === this.selectedIndex) {
|
|
item.setAttribute('data-selected', 'true');
|
|
item.scrollIntoView({ block: 'nearest' });
|
|
} else {
|
|
item.removeAttribute('data-selected');
|
|
}
|
|
});
|
|
}
|
|
|
|
executeSelected() {
|
|
const item = this.flatItems[this.selectedIndex];
|
|
if (!item || !item.action) return;
|
|
|
|
item.action();
|
|
this.close();
|
|
}
|
|
|
|
renderSettingsResults(query) {
|
|
if (this.allSettings.length === 0) this.cacheAllSettings();
|
|
|
|
let results = this.allSettings;
|
|
if (query) {
|
|
const fuse = new Fuse(this.allSettings, {
|
|
keys: ['label', 'description'],
|
|
includeScore: true,
|
|
threshold: 0.4,
|
|
ignoreLocation: true,
|
|
});
|
|
results = fuse.search(query).map((r) => r.item);
|
|
}
|
|
|
|
const items = results.map((setting) => ({
|
|
id: `setting-${setting.id}`,
|
|
group: `Settings \u2022 ${setting.tab}`,
|
|
icon: 'settings',
|
|
label: setting.label,
|
|
description: setting.description,
|
|
action: () => this.navigateToSetting(setting),
|
|
}));
|
|
|
|
const groups = this.groupBy(items, 'group');
|
|
this.renderGroups(groups);
|
|
}
|
|
|
|
cacheAllSettings() {
|
|
const settingItems = document.querySelectorAll('#page-settings .setting-item');
|
|
this.allSettings = Array.from(settingItems)
|
|
.map((item) => {
|
|
const labelEl = item.querySelector('.label');
|
|
const descEl = item.querySelector('.description');
|
|
const tabEl = item.closest('.settings-tab-content');
|
|
|
|
const label = labelEl ? labelEl.textContent.trim() : '';
|
|
const description = descEl ? descEl.textContent.trim() : '';
|
|
const tab = tabEl ? tabEl.id.replace('settings-tab-', '') : '';
|
|
|
|
if (!item.id) {
|
|
const inputEl = item.querySelector('input[id], select[id], button[id]');
|
|
item.id = inputEl
|
|
? `setting-item-for-${inputEl.id}`
|
|
: `setting-item-${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
return { id: item.id, label, description, tab };
|
|
})
|
|
.filter((s) => s.label);
|
|
}
|
|
|
|
async navigateToSetting(setting) {
|
|
navigate('/settings');
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
const tabButton = document.querySelector(`.settings-tab[data-tab="${setting.tab}"]`);
|
|
if (tabButton && !tabButton.classList.contains('active')) {
|
|
tabButton.click();
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
|
|
const settingElement = document.getElementById(setting.id);
|
|
if (settingElement) {
|
|
settingElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
settingElement.style.transition = 'background-color 0.3s ease-out, box-shadow 0.3s ease-out';
|
|
settingElement.style.backgroundColor = 'rgba(var(--highlight-rgb), 0.2)';
|
|
settingElement.style.boxShadow = '0 0 0 2px rgba(var(--highlight-rgb), 0.5)';
|
|
setTimeout(() => {
|
|
settingElement.style.backgroundColor = '';
|
|
settingElement.style.boxShadow = '';
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
setTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme', theme);
|
|
const themeOptions = document.querySelectorAll('.theme-option');
|
|
themeOptions.forEach((opt) => {
|
|
if (opt.dataset.theme === theme) opt.classList.add('active');
|
|
else opt.classList.remove('active');
|
|
});
|
|
this.notify(`Theme set to ${theme}`);
|
|
}
|
|
|
|
async toggleVisualizer() {
|
|
const { visualizerSettings } = await import('./storage.js');
|
|
const current = visualizerSettings.isEnabled();
|
|
visualizerSettings.setEnabled(!current);
|
|
this.notify(`Visualizer ${!current ? 'enabled' : 'disabled'}`);
|
|
|
|
const overlay = document.getElementById('fullscreen-cover-overlay');
|
|
if (overlay && getComputedStyle(overlay).display !== 'none') {
|
|
window.monochromeUi?.closeFullscreenCover();
|
|
}
|
|
}
|
|
|
|
async setVisualizerPreset(preset) {
|
|
const { visualizerSettings } = await import('./storage.js');
|
|
visualizerSettings.setPreset(preset);
|
|
if (window.monochromeUi?.visualizer) {
|
|
window.monochromeUi.visualizer.setPreset(preset);
|
|
}
|
|
this.notify(`Visualizer preset: ${preset}`);
|
|
}
|
|
|
|
async setQuality(quality) {
|
|
const qualityNames = { LOW: 'Low', HIGH: 'High', LOSSLESS: 'Lossless', HI_RES_LOSSLESS: 'Hi-Res' };
|
|
|
|
if (window.monochromePlayer) {
|
|
window.monochromePlayer.setQuality(quality);
|
|
localStorage.setItem('playback-quality', quality);
|
|
const streamingSelect = document.getElementById('streaming-quality-setting');
|
|
if (streamingSelect) streamingSelect.value = quality;
|
|
}
|
|
|
|
const { downloadQualitySettings } = await import('./storage.js');
|
|
downloadQualitySettings.setQuality(quality);
|
|
const downloadSelect = document.getElementById('download-quality-setting');
|
|
if (downloadSelect) downloadSelect.value = quality;
|
|
|
|
this.notify(`Quality set to ${qualityNames[quality] || quality}`);
|
|
}
|
|
|
|
setSleepTimer(minutes) {
|
|
if (window.monochromePlayer) {
|
|
window.monochromePlayer.setSleepTimer(minutes);
|
|
this.notify(`Sleep timer: ${minutes} minutes`);
|
|
}
|
|
}
|
|
|
|
async likeAllInQueue() {
|
|
const player = window.monochromePlayer;
|
|
const ui = window.monochromeUi;
|
|
if (!player || !ui) return;
|
|
|
|
const queue = player.getCurrentQueue();
|
|
if (queue.length === 0) {
|
|
this.notify('Queue is empty');
|
|
return;
|
|
}
|
|
|
|
const { handleTrackAction } = await import('./events.js');
|
|
const scrobbler = window.monochromeScrobbler;
|
|
|
|
let likedCount = 0;
|
|
this.notify('Liking all tracks in queue...');
|
|
for (const track of queue) {
|
|
const isLiked = await db.isFavorite('track', track.id);
|
|
if (!isLiked) {
|
|
await handleTrackAction('toggle-like', track, player, ui.api, ui.lyricsManager, 'track', ui, scrobbler);
|
|
likedCount++;
|
|
}
|
|
}
|
|
this.notify(`Liked ${likedCount} new track(s)`);
|
|
}
|
|
|
|
async downloadQueue() {
|
|
const player = window.monochromePlayer;
|
|
const ui = window.monochromeUi;
|
|
if (!player || !ui) return;
|
|
|
|
const queue = player.getCurrentQueue();
|
|
if (queue.length === 0) {
|
|
this.notify('Queue is empty');
|
|
return;
|
|
}
|
|
|
|
const { downloadTracks } = await import('./downloads.js');
|
|
const { downloadQualitySettings } = await import('./storage.js');
|
|
downloadTracks(queue, ui.api, downloadQualitySettings.getQuality(), ui.lyricsManager);
|
|
}
|
|
|
|
async createPlaylist() {
|
|
const name = `New Playlist ${new Date().toLocaleDateString()}`;
|
|
await db.createPlaylist(name);
|
|
navigate('/library');
|
|
this.notify('Playlist created');
|
|
}
|
|
|
|
async createFolder() {
|
|
const name = `New Folder ${new Date().toLocaleDateString()}`;
|
|
await db.createFolder(name);
|
|
navigate('/library');
|
|
this.notify('Folder created');
|
|
}
|
|
|
|
async clearCache() {
|
|
const api = window.monochromeUi?.api;
|
|
if (api) {
|
|
await api.clearCache();
|
|
this.notify('Cache cleared');
|
|
}
|
|
}
|
|
|
|
async notify(message) {
|
|
const { showNotification } = await import('./downloads.js');
|
|
showNotification(message);
|
|
}
|
|
}
|
|
|
|
new CommandPalette();
|