864 lines
30 KiB
JavaScript
864 lines
30 KiB
JavaScript
import { debounce } from './utils.js';
|
|
import { db } from './db.js';
|
|
import Fuse from 'fuse.js';
|
|
|
|
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.results = [];
|
|
|
|
this.allSettings = [];
|
|
this.debouncedSearch = debounce(this.performSearch.bind(this), 300);
|
|
|
|
this.commands = [
|
|
{
|
|
name: 'theme',
|
|
description: 'Change theme (white, dark, ocean, purple, forest, etc.)',
|
|
action: (args) => this.handleTheme(args),
|
|
},
|
|
{
|
|
name: 'play',
|
|
description: 'Search and play a track',
|
|
action: (args, autoPick) => this.handlePlay(args, autoPick),
|
|
},
|
|
{
|
|
name: 'shuffle',
|
|
description: 'Shuffle a playlist, artist, or album',
|
|
action: (args, autoPick) => this.handleShuffle(args, autoPick),
|
|
},
|
|
{
|
|
name: 'queue',
|
|
description: 'Manage the queue (wipe, like all, download)',
|
|
action: (args) => this.handleQueue(args),
|
|
},
|
|
{
|
|
name: 'setting',
|
|
description: 'Search for a specific setting',
|
|
action: (args) => this.handleSettingSearch(args),
|
|
},
|
|
{
|
|
name: 'sleep',
|
|
description: 'Set sleep timer in minutes',
|
|
action: (args) => this.handleSleepTimer(args),
|
|
},
|
|
{
|
|
name: 'quality',
|
|
description: 'Set streaming & download quality',
|
|
action: (args) => this.handleQuality(args),
|
|
},
|
|
{
|
|
name: 'visualizer',
|
|
description: 'Control visualizer (toggle, preset)',
|
|
action: (args) => this.handleVisualizer(args),
|
|
},
|
|
{
|
|
name: 'cache',
|
|
description: 'Clear application cache',
|
|
action: () => this.handleClearCache(),
|
|
},
|
|
];
|
|
|
|
this.init();
|
|
}
|
|
|
|
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.overlay.style.display = 'flex';
|
|
this.input.value = '>';
|
|
this.input.focus();
|
|
this.handleInput();
|
|
}
|
|
|
|
close() {
|
|
this.isOpen = false;
|
|
this.overlay.style.display = 'none';
|
|
}
|
|
|
|
handleInput() {
|
|
const value = this.input.value;
|
|
this.selectedIndex = 0;
|
|
|
|
if (!value.startsWith('>')) {
|
|
this.renderResults([
|
|
{
|
|
name: 'Type > to use commands',
|
|
description: 'e.g. >theme White, >play The Whole World Is Free',
|
|
action: () => {
|
|
this.input.value = '>';
|
|
this.handleInput();
|
|
},
|
|
type: 'hint',
|
|
},
|
|
]);
|
|
return;
|
|
}
|
|
|
|
const fullQuery = value.slice(1);
|
|
const match = fullQuery.match(/^(\S+)(?:\s+(.*))?$/);
|
|
|
|
if (!match) {
|
|
this.renderDefaultCommands();
|
|
return;
|
|
}
|
|
|
|
const cmdName = match[1].toLowerCase();
|
|
const args = match[2] || '';
|
|
|
|
const command = this.commands.find((c) => c.name === cmdName);
|
|
|
|
if (command) {
|
|
const commandsWithSubmenus = ['queue', 'go', 'visualizer', 'quality', 'sleep', 'setting'];
|
|
if (commandsWithSubmenus.includes(command.name) && !args.trim()) {
|
|
command.action(args);
|
|
return;
|
|
}
|
|
|
|
this.renderResults([
|
|
{
|
|
name: `Execute: ${command.name} ${args}`,
|
|
description: args ? `Run ${command.name} for "${args}"` : command.description,
|
|
action: () => command.action(args, ['play', 'shuffle', 'setting'].includes(command.name)),
|
|
type: 'execution',
|
|
},
|
|
]);
|
|
|
|
if (args.trim().length > 0 && (cmdName === 'play' || cmdName === 'shuffle')) {
|
|
this.debouncedSearch(cmdName, args.trim());
|
|
}
|
|
} else {
|
|
this.renderDefaultCommands(cmdName);
|
|
}
|
|
}
|
|
|
|
handleKeydown(e) {
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.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') {
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
renderDefaultCommands(filter = '') {
|
|
let cmds = this.commands;
|
|
if (filter) {
|
|
if (Fuse) {
|
|
const fuse = new Fuse(this.commands, { keys: ['name', 'description'] });
|
|
cmds = fuse.search(filter).map((r) => r.item);
|
|
} else {
|
|
cmds = this.commands.filter((c) => c.name.includes(filter));
|
|
}
|
|
}
|
|
|
|
this.renderResults(
|
|
cmds.map((c) => ({
|
|
name: c.name,
|
|
description: c.description,
|
|
action: () => {
|
|
this.input.value = `>${c.name} `;
|
|
this.handleInput();
|
|
},
|
|
type: 'command',
|
|
}))
|
|
);
|
|
}
|
|
|
|
renderResults(results) {
|
|
this.results = results;
|
|
this.resultsContainer.innerHTML = '';
|
|
|
|
if (results.length === 0) {
|
|
this.resultsContainer.innerHTML =
|
|
'<div style="padding: 1rem; color: var(--muted-foreground); text-align: center;">No results found</div>';
|
|
return;
|
|
}
|
|
|
|
results.forEach((result, index) => {
|
|
const div = document.createElement('div');
|
|
div.className = `command-result-item ${index === this.selectedIndex ? 'selected' : ''}`;
|
|
|
|
const imgHtml = result.image
|
|
? `<img src="${result.image}" crossorigin="anonymous" style="width: 32px; height: 32px; border-radius: 4px; margin-right: 10px; object-fit: cover;">`
|
|
: '';
|
|
|
|
div.innerHTML = `
|
|
<div style="display: flex; align-items: center;">
|
|
${imgHtml}
|
|
<div style="display: flex; flex-direction: column;"><span class="command-result-name" style="font-weight: 500;">${result.name}</span><span class="command-result-desc" style="font-size: 0.8rem; opacity: 0.7;">${result.description || ''}</span></div>
|
|
</div>
|
|
`;
|
|
div.addEventListener('click', () => {
|
|
this.selectedIndex = index;
|
|
this.executeSelected();
|
|
});
|
|
this.resultsContainer.appendChild(div);
|
|
});
|
|
}
|
|
|
|
updateSelection() {
|
|
const items = this.resultsContainer.querySelectorAll('.command-result-item');
|
|
items.forEach((item, index) => {
|
|
if (index === this.selectedIndex) {
|
|
item.classList.add('selected');
|
|
item.scrollIntoView({ block: 'nearest' });
|
|
} else {
|
|
item.classList.remove('selected');
|
|
}
|
|
});
|
|
}
|
|
|
|
executeSelected() {
|
|
const result = this.results[this.selectedIndex];
|
|
if (result && result.action) {
|
|
result.action();
|
|
if (result.type !== 'hint') {
|
|
this.close();
|
|
}
|
|
} else if (result && result.type === 'command') {
|
|
this.input.value = `>${result.name} `;
|
|
this.handleInput();
|
|
}
|
|
}
|
|
|
|
handleTheme(args) {
|
|
if (!args) return;
|
|
const theme = args.trim().toLowerCase();
|
|
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');
|
|
});
|
|
}
|
|
|
|
async showNotification(message) {
|
|
const { showNotification } = await import('./downloads.js');
|
|
showNotification(message);
|
|
}
|
|
|
|
handleQueue(args) {
|
|
const player = window.monochromePlayer;
|
|
const ui = window.monochromeUi;
|
|
|
|
if (!player || !ui) {
|
|
console.error('Player or UI not available for queue command');
|
|
return;
|
|
}
|
|
|
|
if (!args || !args.trim()) {
|
|
this.renderResults(
|
|
[
|
|
{ name: '>queue wipe', description: 'Clear the queue and stop playback' },
|
|
{ name: '>queue like all', description: 'Like all tracks in the current queue' },
|
|
{ name: '>queue download', description: 'Download all tracks in the current queue' },
|
|
].map((c) => ({
|
|
...c,
|
|
type: 'command',
|
|
action: () => {
|
|
this.input.value = c.name;
|
|
this.handleInput();
|
|
},
|
|
}))
|
|
);
|
|
return;
|
|
}
|
|
|
|
const subCommand = args.trim().toLowerCase();
|
|
|
|
switch (subCommand) {
|
|
case 'wipe':
|
|
player.wipeQueue();
|
|
this.showNotification('Queue wiped.');
|
|
this.close();
|
|
break;
|
|
case 'like all':
|
|
this.likeAllInQueue(player, ui);
|
|
break;
|
|
case 'download':
|
|
this.downloadQueue(player, ui);
|
|
break;
|
|
default:
|
|
this.showNotification(`Unknown queue command: ${subCommand}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
async likeAllInQueue(player, ui) {
|
|
const queue = player.getCurrentQueue();
|
|
if (queue.length === 0) {
|
|
this.showNotification('Queue is empty.');
|
|
return;
|
|
}
|
|
|
|
const { handleTrackAction } = await import('./events.js');
|
|
const scrobbler = window.monochromeScrobbler;
|
|
|
|
let likedCount = 0;
|
|
this.showNotification('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.showNotification(`Liked ${likedCount} new track(s) in the queue.`);
|
|
this.close();
|
|
}
|
|
|
|
async downloadQueue(player, ui) {
|
|
const queue = player.getCurrentQueue();
|
|
if (queue.length === 0) {
|
|
this.showNotification('Queue is empty.');
|
|
return;
|
|
}
|
|
|
|
const { downloadTracks } = await import('./downloads.js');
|
|
const { downloadQualitySettings } = await import('./storage.js');
|
|
const lyricsManager = ui.lyricsManager;
|
|
|
|
downloadTracks(queue, ui.api, downloadQualitySettings.getQuality(), lyricsManager);
|
|
this.close();
|
|
}
|
|
|
|
handleNavigation(args) {
|
|
const validPages = ['home', 'library', 'recent', 'settings', 'unreleased', 'about', 'download'];
|
|
|
|
if (!args || !args.trim()) {
|
|
this.renderResults(
|
|
validPages.map((p) => ({
|
|
name: `>go ${p}`,
|
|
description: `Navigate to ${p}`,
|
|
action: () => {
|
|
this.close();
|
|
import('./router.js').then((m) => m.navigate(p === 'home' ? '/' : `/${p}`));
|
|
},
|
|
type: 'command',
|
|
}))
|
|
);
|
|
return;
|
|
}
|
|
|
|
const page = args.trim().toLowerCase();
|
|
|
|
if (validPages.includes(page)) {
|
|
this.close();
|
|
import('./router.js').then((m) => m.navigate(page === 'home' ? '/' : `/${page}`));
|
|
} else {
|
|
this.showNotification(`Unknown page: ${page}`);
|
|
}
|
|
}
|
|
|
|
handleSleepTimer(args) {
|
|
if (!args || !args.trim()) {
|
|
this.renderResults(
|
|
[15, 30, 45, 60, 120].map((m) => ({
|
|
name: `>sleep ${m}`,
|
|
description: `Set sleep timer for ${m} minutes`,
|
|
action: () => {
|
|
this.setSleepTimer(m);
|
|
this.close();
|
|
},
|
|
type: 'command',
|
|
}))
|
|
);
|
|
return;
|
|
}
|
|
|
|
const minutes = parseInt(args.trim());
|
|
if (!isNaN(minutes) && minutes > 0) {
|
|
this.setSleepTimer(minutes);
|
|
this.close();
|
|
} else {
|
|
this.showNotification('Invalid duration');
|
|
}
|
|
}
|
|
|
|
setSleepTimer(minutes) {
|
|
if (window.monochromePlayer) {
|
|
window.monochromePlayer.setSleepTimer(minutes);
|
|
this.showNotification(`Sleep timer set for ${minutes} minutes`);
|
|
}
|
|
}
|
|
|
|
handleQuality(args) {
|
|
const qualityMap = {
|
|
low: 'LOW',
|
|
high: 'HIGH',
|
|
lossless: 'LOSSLESS',
|
|
hires: 'HI_RES_LOSSLESS',
|
|
'hi-res': 'HI_RES_LOSSLESS',
|
|
master: 'HI_RES_LOSSLESS',
|
|
};
|
|
|
|
const displayQualities = [
|
|
{ id: 'low', name: 'Low', code: 'LOW' },
|
|
{ id: 'high', name: 'High', code: 'HIGH' },
|
|
{ id: 'lossless', name: 'Lossless', code: 'LOSSLESS' },
|
|
{ id: 'hi-res', name: 'Hi-Res', code: 'HI_RES_LOSSLESS' },
|
|
];
|
|
|
|
if (!args || !args.trim()) {
|
|
const results = displayQualities.map((q) => ({
|
|
name: `>quality ${q.id}`,
|
|
description: `Set quality to ${q.name}`,
|
|
action: () => {
|
|
this.setQuality(q.code, true, true);
|
|
this.close();
|
|
},
|
|
type: 'command',
|
|
}));
|
|
|
|
results.push({
|
|
name: 'Usage: >quality [level] [-S] [-D]',
|
|
description: '-S for Streaming only, -D for Download only',
|
|
action: () => {},
|
|
type: 'hint',
|
|
});
|
|
this.renderResults(results);
|
|
return;
|
|
}
|
|
|
|
const parts = args.trim().split(/\s+/);
|
|
const qualityKey = parts.find((p) => !p.startsWith('-'))?.toLowerCase();
|
|
const flags = parts.filter((p) => p.startsWith('-')).map((f) => f.toLowerCase());
|
|
|
|
if (!qualityKey || !qualityMap[qualityKey]) {
|
|
this.showNotification('Invalid quality setting');
|
|
return;
|
|
}
|
|
|
|
const qualityCode = qualityMap[qualityKey];
|
|
let setStreaming = true;
|
|
let setDownload = true;
|
|
|
|
if (flags.includes('-d') && !flags.includes('-s')) {
|
|
setStreaming = false;
|
|
} else if (flags.includes('-s') && !flags.includes('-d')) {
|
|
setDownload = false;
|
|
}
|
|
|
|
this.setQuality(qualityCode, setStreaming, setDownload);
|
|
this.close();
|
|
}
|
|
|
|
async setQuality(quality, setStreaming, setDownload) {
|
|
const messages = [];
|
|
const qualityName = this.getQualityName(quality);
|
|
|
|
if (setStreaming) {
|
|
if (window.monochromePlayer) {
|
|
window.monochromePlayer.setQuality(quality);
|
|
localStorage.setItem('playback-quality', quality);
|
|
messages.push('Streaming');
|
|
|
|
const streamingSelect = document.getElementById('streaming-quality-setting');
|
|
if (streamingSelect) {
|
|
streamingSelect.value = quality;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (setDownload) {
|
|
const { downloadQualitySettings } = await import('./storage.js');
|
|
downloadQualitySettings.setQuality(quality);
|
|
messages.push('Download');
|
|
|
|
const downloadSelect = document.getElementById('download-quality-setting');
|
|
if (downloadSelect) {
|
|
downloadSelect.value = quality;
|
|
}
|
|
}
|
|
|
|
if (messages.length > 0) {
|
|
this.showNotification(`${messages.join(' & ')} quality set to ${qualityName}`);
|
|
}
|
|
}
|
|
|
|
getQualityName(code) {
|
|
const names = {
|
|
LOW: 'Low',
|
|
HIGH: 'High',
|
|
LOSSLESS: 'Lossless',
|
|
HI_RES_LOSSLESS: 'Hi-Res',
|
|
};
|
|
return names[code] || code;
|
|
}
|
|
|
|
async handleVisualizer(args) {
|
|
if (!args || !args.trim()) {
|
|
this.renderResults(
|
|
[
|
|
{ name: '>visualizer toggle', description: 'Toggle visualizer on/off', cmd: 'toggle' },
|
|
{ name: '>visualizer butterchurn', description: 'Set preset to Butterchurn', cmd: 'butterchurn' },
|
|
{ name: '>visualizer kawarp', description: 'Set preset to Kawarp', cmd: 'kawarp' },
|
|
{ name: '>visualizer lcd', description: 'Set preset to LCD', cmd: 'lcd' },
|
|
{ name: '>visualizer particles', description: 'Set preset to Particles', cmd: 'particles' },
|
|
{
|
|
name: '>visualizer unknown-pleasures',
|
|
description: 'Set preset to Unknown Pleasures',
|
|
cmd: 'unknown-pleasures',
|
|
},
|
|
].map((c) => ({
|
|
...c,
|
|
action: () => {
|
|
if (c.cmd === 'toggle') {
|
|
this.toggleVisualizer();
|
|
} else {
|
|
this.setVisualizerPreset(c.cmd);
|
|
}
|
|
this.close();
|
|
},
|
|
type: 'command',
|
|
}))
|
|
);
|
|
return;
|
|
}
|
|
|
|
const subCmd = args.trim().toLowerCase();
|
|
if (subCmd === 'toggle') {
|
|
this.toggleVisualizer();
|
|
this.close();
|
|
} else {
|
|
const presets = ['butterchurn', 'kawarp', 'lcd', 'particles', 'unknown-pleasures'];
|
|
if (presets.includes(subCmd)) {
|
|
this.setVisualizerPreset(subCmd);
|
|
this.close();
|
|
} else {
|
|
this.showNotification('Unknown visualizer command');
|
|
}
|
|
}
|
|
}
|
|
|
|
async toggleVisualizer() {
|
|
const { visualizerSettings } = await import('./storage.js');
|
|
const current = visualizerSettings.isEnabled();
|
|
visualizerSettings.setEnabled(!current);
|
|
this.showNotification(`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.showNotification(`Visualizer preset set to ${preset}`);
|
|
}
|
|
|
|
async handleClearCache() {
|
|
const api = window.monochromeUi?.api;
|
|
if (api) {
|
|
await api.clearCache();
|
|
this.showNotification('Cache cleared');
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
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 handleSettingSearch(args, autoPick = false) {
|
|
const query = args.trim().toLowerCase();
|
|
|
|
if (!query) {
|
|
this.renderResults(
|
|
this.allSettings.map((setting) => ({
|
|
name: setting.label,
|
|
description: `[${setting.tab}] ${setting.description}`,
|
|
action: () => {
|
|
this.navigateToSetting(setting);
|
|
this.close();
|
|
},
|
|
type: 'setting',
|
|
}))
|
|
);
|
|
return;
|
|
}
|
|
|
|
const fuse = new Fuse(this.allSettings, {
|
|
keys: ['label', 'description'],
|
|
includeScore: true,
|
|
threshold: 0.4,
|
|
ignoreLocation: true,
|
|
});
|
|
|
|
const results = fuse.search(query).map((r) => r.item);
|
|
|
|
if (autoPick && results.length > 0) {
|
|
this.navigateToSetting(results[0]);
|
|
this.close();
|
|
return;
|
|
}
|
|
|
|
this.renderResults(
|
|
results.map((setting) => ({
|
|
name: setting.label,
|
|
description: `[${setting.tab}] ${setting.description}`,
|
|
action: () => {
|
|
this.navigateToSetting(setting);
|
|
this.close();
|
|
},
|
|
type: 'setting',
|
|
}))
|
|
);
|
|
}
|
|
|
|
async navigateToSetting(setting) {
|
|
const router = await import('./router.js');
|
|
router.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);
|
|
}
|
|
}
|
|
|
|
async performSearch(cmdName, query) {
|
|
if (!this.isOpen) return;
|
|
|
|
const api = window.monochromeUi?.api;
|
|
if (!api) return;
|
|
|
|
let results = [];
|
|
|
|
try {
|
|
if (cmdName === 'play') {
|
|
const data = await api.searchTracks(query);
|
|
results = data.items.map((track) => ({
|
|
name: track.title,
|
|
description: `${track.artist?.name || 'Unknown'} • ${track.album?.title || 'Unknown'}`,
|
|
image: api.getCoverUrl(track.album?.cover, 80),
|
|
action: () => {
|
|
window.monochromePlayer.setQueue([track], 0);
|
|
window.monochromePlayer.playTrackFromQueue();
|
|
this.close();
|
|
},
|
|
type: 'result',
|
|
}));
|
|
} else if (cmdName === 'shuffle') {
|
|
const [albums, artists, playlists, userPlaylists] = await Promise.all([
|
|
api.searchAlbums(query),
|
|
api.searchArtists(query),
|
|
api.searchPlaylists(query),
|
|
db.getPlaylists(true),
|
|
]);
|
|
|
|
let matchedUserPlaylists = [];
|
|
if (Fuse) {
|
|
const fuse = new Fuse(userPlaylists, { keys: ['name'] });
|
|
matchedUserPlaylists = fuse.search(query).map((r) => r.item);
|
|
} else {
|
|
matchedUserPlaylists = userPlaylists.filter((p) =>
|
|
p.name.toLowerCase().includes(query.toLowerCase())
|
|
);
|
|
}
|
|
|
|
const formatResult = (item, type, subtitle, image) => ({
|
|
name: item.title || item.name,
|
|
description: `${type} • ${subtitle}`,
|
|
image: image,
|
|
action: () => this.playCollection(item, type, true),
|
|
type: 'result',
|
|
});
|
|
|
|
results = [
|
|
...matchedUserPlaylists.map((p) =>
|
|
formatResult(
|
|
p,
|
|
'User Playlist',
|
|
`${p.tracks?.length || 0} tracks`,
|
|
p.cover || (p.images && p.images[0])
|
|
)
|
|
),
|
|
...artists.items.map((a) =>
|
|
formatResult(a, 'Artist', 'Artist', api.getArtistPictureUrl(a.picture, 80))
|
|
),
|
|
...albums.items.map((a) => formatResult(a, 'Album', a.artist?.name, api.getCoverUrl(a.cover, 80))),
|
|
...playlists.items.map((p) =>
|
|
formatResult(p, 'Playlist', p.creator?.name || 'Tidal', api.getCoverUrl(p.image, 80))
|
|
),
|
|
];
|
|
}
|
|
} catch (e) {
|
|
console.error('Command palette search error:', e);
|
|
}
|
|
|
|
if (this.isOpen && results.length > 0) {
|
|
this.renderResults(results);
|
|
}
|
|
}
|
|
|
|
async handlePlay(args, autoPick) {
|
|
if (!args) return;
|
|
|
|
if (autoPick) {
|
|
const api = window.monochromeUi?.api;
|
|
const results = await api.searchTracks(args);
|
|
if (results.items.length > 0) {
|
|
const track = results.items[0];
|
|
window.monochromePlayer.setQueue([track], 0);
|
|
window.monochromePlayer.playTrackFromQueue();
|
|
this.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
async handleShuffle(args, autoPick) {
|
|
if (!args) return;
|
|
|
|
if (autoPick) {
|
|
this.performSearch('shuffle', args).then(() => {
|
|
if (this.results.length > 0 && this.results[0].action) {
|
|
this.results[0].action();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async playCollection(item, type, shuffle) {
|
|
const player = window.monochromePlayer;
|
|
const api = window.monochromeUi.api;
|
|
let tracks = [];
|
|
|
|
try {
|
|
if (type === 'User Playlist') {
|
|
tracks = item.tracks;
|
|
} else if (type === 'Artist') {
|
|
const artist = await api.getArtist(item.id);
|
|
const allReleases = [...(artist.albums || []), ...(artist.eps || [])];
|
|
const trackSet = new Set();
|
|
const allTracks = [];
|
|
|
|
const chunkSize = 8;
|
|
for (let i = 0; i < allReleases.length; i += chunkSize) {
|
|
const chunk = allReleases.slice(i, i + chunkSize);
|
|
await Promise.all(
|
|
chunk.map(async (album) => {
|
|
try {
|
|
const { tracks: albumTracks } = await api.getAlbum(album.id);
|
|
albumTracks.forEach((track) => {
|
|
if (!trackSet.has(track.id)) {
|
|
trackSet.add(track.id);
|
|
allTracks.push(track);
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.warn(`Failed to fetch tracks for album ${album.title}:`, err);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
if (allTracks.length > 0) {
|
|
tracks = allTracks;
|
|
} else {
|
|
tracks = artist.tracks || [];
|
|
}
|
|
} else if (type === 'Album') {
|
|
tracks = (await api.getAlbum(item.id)).tracks;
|
|
} else if (type === 'Playlist') {
|
|
tracks = (await api.getPlaylist(item.uuid)).tracks;
|
|
}
|
|
|
|
if (tracks && tracks.length > 0) {
|
|
if (shuffle) {
|
|
tracks = [...tracks].sort(() => Math.random() - 0.5);
|
|
player.shuffleActive = true;
|
|
document.getElementById('shuffle-btn')?.classList.add('active');
|
|
}
|
|
player.setQueue(tracks, 0);
|
|
player.playTrackFromQueue();
|
|
this.close();
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to play collection:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
new CommandPalette();
|