diff --git a/js/commandPalette.js b/js/commandPalette.js
index 4415f5e..b9bae51 100644
--- a/js/commandPalette.js
+++ b/js/commandPalette.js
@@ -18,48 +18,48 @@ class CommandPalette {
{
name: 'theme',
description: 'Change theme (white, dark, ocean, purple, forest, etc.)',
- action: (args) => this.handleTheme(args)
+ action: (args) => this.handleTheme(args),
},
{
name: 'play',
description: 'Search and play a track',
- action: (args, autoPick) => this.handlePlay(args, autoPick)
+ action: (args, autoPick) => this.handlePlay(args, autoPick),
},
{
name: 'shuffle',
description: 'Shuffle a playlist, artist, or album',
- action: (args, autoPick) => this.handleShuffle(args, autoPick)
+ action: (args, autoPick) => this.handleShuffle(args, autoPick),
},
{
name: 'queue',
description: 'Manage the queue (wipe, like all, download)',
- action: (args) => this.handleQueue(args)
+ action: (args) => this.handleQueue(args),
},
{
name: 'setting',
description: 'Search for a specific setting',
- action: (args) => this.handleSettingSearch(args)
+ action: (args) => this.handleSettingSearch(args),
},
{
name: 'sleep',
description: 'Set sleep timer in minutes',
- action: (args) => this.handleSleepTimer(args)
+ action: (args) => this.handleSleepTimer(args),
},
{
name: 'quality',
description: 'Set streaming & download quality',
- action: (args) => this.handleQuality(args)
+ action: (args) => this.handleQuality(args),
},
{
name: 'visualizer',
description: 'Control visualizer (toggle, preset)',
- action: (args) => this.handleVisualizer(args)
+ action: (args) => this.handleVisualizer(args),
},
{
name: 'cache',
description: 'Clear application cache',
- action: () => this.handleClearCache()
- }
+ action: () => this.handleClearCache(),
+ },
];
this.init();
@@ -75,7 +75,7 @@ class CommandPalette {
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();
});
@@ -106,18 +106,23 @@ class CommandPalette {
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'
- }]);
+ 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;
@@ -126,7 +131,7 @@ class CommandPalette {
const cmdName = match[1].toLowerCase();
const args = match[2] || '';
- const command = this.commands.find(c => c.name === cmdName);
+ const command = this.commands.find((c) => c.name === cmdName);
if (command) {
const commandsWithSubmenus = ['queue', 'go', 'visualizer', 'quality', 'sleep', 'setting'];
@@ -135,12 +140,14 @@ class CommandPalette {
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'
- }]);
+ 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());
@@ -172,38 +179,43 @@ class CommandPalette {
if (filter) {
if (Fuse) {
const fuse = new Fuse(this.commands, { keys: ['name', 'description'] });
- cmds = fuse.search(filter).map(r => r.item);
+ cmds = fuse.search(filter).map((r) => r.item);
} else {
- cmds = this.commands.filter(c => c.name.includes(filter));
+ 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'
- })));
+
+ 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 = '
No results found
';
+ this.resultsContainer.innerHTML =
+ '
No results found
';
return;
}
results.forEach((result, index) => {
const div = document.createElement('div');
div.className = `command-result-item ${index === this.selectedIndex ? 'selected' : ''}`;
-
- const imgHtml = result.image ? `

` : '';
-
+
+ const imgHtml = result.image
+ ? `

`
+ : '';
+
div.innerHTML = `
${imgHtml}
@@ -248,9 +260,9 @@ class CommandPalette {
const theme = args.trim().toLowerCase();
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
-
+
const themeOptions = document.querySelectorAll('.theme-option');
- themeOptions.forEach(opt => {
+ themeOptions.forEach((opt) => {
if (opt.dataset.theme === theme) opt.classList.add('active');
else opt.classList.remove('active');
});
@@ -271,18 +283,20 @@ class CommandPalette {
}
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();
- }
- })));
+ 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;
}
@@ -315,7 +329,7 @@ class CommandPalette {
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) {
@@ -335,11 +349,11 @@ class CommandPalette {
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();
}
@@ -348,23 +362,25 @@ class CommandPalette {
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'
- })));
+ 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}`));
+ import('./router.js').then((m) => m.navigate(page === 'home' ? '/' : `/${page}`));
} else {
this.showNotification(`Unknown page: ${page}`);
}
@@ -372,24 +388,26 @@ class CommandPalette {
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'
- })));
+ 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();
+ this.setSleepTimer(minutes);
+ this.close();
} else {
- this.showNotification("Invalid duration");
+ this.showNotification('Invalid duration');
}
}
@@ -402,45 +420,45 @@ class CommandPalette {
handleQuality(args) {
const qualityMap = {
- 'low': 'LOW',
- 'high': 'HIGH',
- 'lossless': 'LOSSLESS',
- 'hires': 'HI_RES_LOSSLESS',
+ low: 'LOW',
+ high: 'HIGH',
+ lossless: 'LOSSLESS',
+ hires: 'HI_RES_LOSSLESS',
'hi-res': 'HI_RES_LOSSLESS',
- 'master': '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' }
+ { id: 'hi-res', name: 'Hi-Res', code: 'HI_RES_LOSSLESS' },
];
if (!args || !args.trim()) {
- const results = displayQualities.map(q => ({
+ 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'
+ type: 'command',
}));
results.push({
name: 'Usage: >quality [level] [-S] [-D]',
description: '-S for Streaming only, -D for Download only',
action: () => {},
- type: 'hint'
+ 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());
+ 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');
@@ -456,7 +474,7 @@ class CommandPalette {
} else if (flags.includes('-s') && !flags.includes('-d')) {
setDownload = false;
}
-
+
this.setQuality(qualityCode, setStreaming, setDownload);
this.close();
}
@@ -496,35 +514,41 @@ class CommandPalette {
getQualityName(code) {
const names = {
- 'LOW': 'Low',
- 'HIGH': 'High',
- 'LOSSLESS': 'Lossless',
- 'HI_RES_LOSSLESS': 'Hi-Res'
+ 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'
- })));
+ 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;
}
@@ -548,10 +572,10 @@ class CommandPalette {
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();
+ window.monochromeUi?.closeFullscreenCover();
}
}
@@ -575,42 +599,48 @@ class CommandPalette {
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)}`;
- }
+ this.allSettings = Array.from(settingItems)
+ .map((item) => {
+ const labelEl = item.querySelector('.label');
+ const descEl = item.querySelector('.description');
+ const tabEl = item.closest('.settings-tab-content');
- return {
- id: item.id,
- label,
- description,
- tab,
- };
- }).filter(s => s.label);
+ 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'
- })));
+ this.renderResults(
+ this.allSettings.map((setting) => ({
+ name: setting.label,
+ description: `[${setting.tab}] ${setting.description}`,
+ action: () => {
+ this.navigateToSetting(setting);
+ this.close();
+ },
+ type: 'setting',
+ }))
+ );
return;
}
@@ -621,7 +651,7 @@ class CommandPalette {
ignoreLocation: true,
});
- const results = fuse.search(query).map(r => r.item);
+ const results = fuse.search(query).map((r) => r.item);
if (autoPick && results.length > 0) {
this.navigateToSetting(results[0]);
@@ -629,47 +659,48 @@ class CommandPalette {
return;
}
- this.renderResults(results.map(setting => ({
- name: setting.label,
- description: `[${setting.tab}] ${setting.description}`,
- action: () => {
- this.navigateToSetting(setting);
- this.close();
- },
- type: 'setting'
- })));
+ 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));
+ 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();
- }
+ 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));
+ 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);
- }
+ 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;
@@ -678,7 +709,7 @@ class CommandPalette {
try {
if (cmdName === 'play') {
const data = await api.searchTracks(query);
- results = data.items.map(track => ({
+ results = data.items.map((track) => ({
name: track.title,
description: `${track.artist?.name || 'Unknown'} • ${track.album?.title || 'Unknown'}`,
image: api.getCoverUrl(track.album?.cover, 80),
@@ -687,22 +718,24 @@ class CommandPalette {
window.monochromePlayer.playTrackFromQueue();
this.close();
},
- type: 'result'
+ 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)
+ db.getPlaylists(true),
]);
let matchedUserPlaylists = [];
if (Fuse) {
const fuse = new Fuse(userPlaylists, { keys: ['name'] });
- matchedUserPlaylists = fuse.search(query).map(r => r.item);
+ matchedUserPlaylists = fuse.search(query).map((r) => r.item);
} else {
- matchedUserPlaylists = userPlaylists.filter(p => p.name.toLowerCase().includes(query.toLowerCase()));
+ matchedUserPlaylists = userPlaylists.filter((p) =>
+ p.name.toLowerCase().includes(query.toLowerCase())
+ );
}
const formatResult = (item, type, subtitle, image) => ({
@@ -710,14 +743,25 @@ class CommandPalette {
description: `${type} • ${subtitle}`,
image: image,
action: () => this.playCollection(item, type, true),
- type: 'result'
+ 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)))
+ ...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) {
@@ -731,7 +775,7 @@ class CommandPalette {
async handlePlay(args, autoPick) {
if (!args) return;
-
+
if (autoPick) {
const api = window.monochromeUi?.api;
const results = await api.searchTracks(args);
@@ -746,7 +790,7 @@ class CommandPalette {
async handleShuffle(args, autoPick) {
if (!args) return;
-
+
if (autoPick) {
this.performSearch('shuffle', args).then(() => {
if (this.results.length > 0 && this.results[0].action) {
@@ -789,7 +833,7 @@ class CommandPalette {
})
);
}
-
+
if (allTracks.length > 0) {
tracks = allTracks;
} else {
@@ -817,4 +861,4 @@ class CommandPalette {
}
}
-new CommandPalette();
\ No newline at end of file
+new CommandPalette();
diff --git a/styles.css b/styles.css
index c71d74c..49ae84b 100644
--- a/styles.css
+++ b/styles.css
@@ -7868,11 +7868,10 @@ textarea:focus {
animation: fade-in 0.3s ease;
}
-
#command-palette-overlay {
position: fixed;
inset: 0;
- background: rgba(0, 0, 0, 0.5);
+ background: rgb(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 10000;
display: flex;
@@ -7925,7 +7924,8 @@ textarea:focus {
color: var(--foreground);
}
-.command-result-item:hover, .command-result-item.selected {
+.command-result-item:hover,
+.command-result-item.selected {
background: var(--secondary);
}