multi-track selection
This commit is contained in:
parent
cc2f28a798
commit
ab11ff6a37
5 changed files with 499 additions and 6 deletions
403
js/events.js
403
js/events.js
|
|
@ -8,7 +8,13 @@ import {
|
|||
getShareUrl,
|
||||
escapeHtml,
|
||||
} from './utils.js';
|
||||
import { lastFMStorage, libreFmSettings, listenBrainzSettings, waveformSettings } from './storage.js';
|
||||
import {
|
||||
lastFMStorage,
|
||||
libreFmSettings,
|
||||
listenBrainzSettings,
|
||||
waveformSettings,
|
||||
keyboardShortcuts,
|
||||
} from './storage.js';
|
||||
import { showNotification, downloadTrackWithMetadata, downloadAlbumAsZip, downloadPlaylistAsZip } from './downloads.js';
|
||||
import { downloadQualitySettings } from './storage.js';
|
||||
import { updateTabTitle, navigate } from './router.js';
|
||||
|
|
@ -47,10 +53,191 @@ import {
|
|||
trackStartMix,
|
||||
trackEvent,
|
||||
} from './analytics.js';
|
||||
import { SVG_BIN, SVG_MUTE, SVG_PAUSE, SVG_PLAY, SVG_VOLUME } from './icons.js';
|
||||
import { SVG_BIN, SVG_MUTE, SVG_PAUSE, SVG_PLAY, SVG_VOLUME, SVG_CHECKBOX, SVG_CHECKBOX_CHECKED } from './icons.js';
|
||||
|
||||
let currentTrackIdForWaveform = null;
|
||||
|
||||
const trackSelection = {
|
||||
selectedIds: new Set(),
|
||||
lastClickedId: null,
|
||||
isSelecting: false,
|
||||
};
|
||||
|
||||
function isMultiSelectToggle(e) {
|
||||
const shortcut = keyboardShortcuts.getShortcutForAction('multiSelectToggle');
|
||||
if (!shortcut) return e.ctrlKey || e.metaKey;
|
||||
const key = e.key?.toLowerCase();
|
||||
const shortcutKey = shortcut.key?.toLowerCase();
|
||||
|
||||
if (['control', 'shift', 'alt', 'meta'].includes(shortcutKey)) {
|
||||
if (shortcut.ctrl && !(e.ctrlKey || e.metaKey)) return false;
|
||||
if (shortcut.shift && !e.shiftKey) return false;
|
||||
if (shortcut.alt && !e.altKey) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
(shortcut.ctrl ? e.ctrlKey || e.metaKey : !e.ctrlKey && !e.metaKey) &&
|
||||
(shortcut.shift ? e.shiftKey : !e.shiftKey) &&
|
||||
(shortcut.alt ? e.altKey : !e.altKey) &&
|
||||
key === shortcutKey
|
||||
);
|
||||
}
|
||||
|
||||
function isMultiSelectRange(e) {
|
||||
const shortcut = keyboardShortcuts.getShortcutForAction('multiSelectRange');
|
||||
if (!shortcut) return e.shiftKey;
|
||||
const key = e.key?.toLowerCase();
|
||||
const shortcutKey = shortcut.key?.toLowerCase();
|
||||
|
||||
if (['control', 'shift', 'alt', 'meta'].includes(shortcutKey)) {
|
||||
if (shortcut.ctrl && !(e.ctrlKey || e.metaKey)) return false;
|
||||
if (shortcut.shift && !e.shiftKey) return false;
|
||||
if (shortcut.alt && !e.altKey) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
(shortcut.ctrl ? e.ctrlKey || e.metaKey : !e.ctrlKey && !e.metaKey) &&
|
||||
(shortcut.shift ? e.shiftKey : !e.shiftKey) &&
|
||||
(shortcut.alt ? e.altKey : !e.altKey) &&
|
||||
key === shortcutKey
|
||||
);
|
||||
}
|
||||
|
||||
function getSelectedTracks() {
|
||||
return Array.from(trackSelection.selectedIds);
|
||||
}
|
||||
|
||||
function updateCheckbox(checkbox, checked) {
|
||||
if (checkbox) {
|
||||
checkbox.innerHTML = checked ? SVG_CHECKBOX_CHECKED(18) : SVG_CHECKBOX(18);
|
||||
checkbox.classList.toggle('checked', checked);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTrackSelection(trackItem, ctrlHeld, shiftHeld) {
|
||||
const trackId = trackItem.dataset.trackId;
|
||||
const isSelected = trackSelection.selectedIds.has(trackId);
|
||||
|
||||
if (ctrlHeld) {
|
||||
if (isSelected) {
|
||||
trackSelection.selectedIds.delete(trackId);
|
||||
trackItem.classList.remove('selected');
|
||||
updateCheckbox(trackItem.querySelector('.track-checkbox'), false);
|
||||
} else {
|
||||
trackSelection.selectedIds.add(trackId);
|
||||
trackItem.classList.add('selected');
|
||||
updateCheckbox(trackItem.querySelector('.track-checkbox'), true);
|
||||
}
|
||||
trackSelection.lastClickedId = trackId;
|
||||
} else if (shiftHeld && trackSelection.lastClickedId && trackSelection.lastClickedId !== trackId) {
|
||||
const parentList = trackItem.closest('.track-list') || trackItem.closest('#main-content');
|
||||
const allTrackElements = Array.from(parentList.querySelectorAll('.track-item'));
|
||||
const lastIndex = allTrackElements.findIndex((el) => el.dataset.trackId === trackSelection.lastClickedId);
|
||||
const currentIndex = allTrackElements.findIndex((el) => el.dataset.trackId === trackId);
|
||||
|
||||
if (lastIndex !== -1 && currentIndex !== -1) {
|
||||
const start = Math.min(lastIndex, currentIndex);
|
||||
const end = Math.max(lastIndex, currentIndex);
|
||||
for (let i = start; i <= end; i++) {
|
||||
const el = allTrackElements[i];
|
||||
trackSelection.selectedIds.add(el.dataset.trackId);
|
||||
el.classList.add('selected');
|
||||
updateCheckbox(el.querySelector('.track-checkbox'), true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!isSelected) {
|
||||
trackSelection.selectedIds.add(trackId);
|
||||
trackItem.classList.add('selected');
|
||||
updateCheckbox(trackItem.querySelector('.track-checkbox'), true);
|
||||
} else {
|
||||
trackSelection.selectedIds.delete(trackId);
|
||||
trackItem.classList.remove('selected');
|
||||
updateCheckbox(trackItem.querySelector('.track-checkbox'), false);
|
||||
}
|
||||
trackSelection.lastClickedId = trackId;
|
||||
}
|
||||
|
||||
trackSelection.isSelecting = trackSelection.selectedIds.size > 0;
|
||||
document.body.classList.toggle('multi-select-mode', trackSelection.isSelecting);
|
||||
}
|
||||
|
||||
function showMultiSelectPlaylistModal(tracks) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.style.cssText =
|
||||
'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000;';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="background: var(--card); border-radius: var(--radius); padding: 1.5rem; min-width: 350px; max-width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; border-bottom: 1px solid var(--border); padding-bottom: 0.75rem;">
|
||||
<h3 style="margin: 0;">Add to Playlist</h3>
|
||||
<button class="modal-close" style="background: none; border: none; color: var(--foreground); font-size: 1.5rem; cursor: pointer; padding: 0; line-height: 1;">×</button>
|
||||
</div>
|
||||
<div class="playlist-body" style="max-height: 300px; overflow-y: auto;">
|
||||
<div class="create-new-playlist" style="padding: 12px; cursor: pointer; border-bottom: 1px solid var(--border); color: var(--primary); font-weight: 500;">
|
||||
+ Create new playlist
|
||||
</div>
|
||||
<div class="playlist-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const closeModal = () => {
|
||||
modal.remove();
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
|
||||
modal.querySelector('.modal-close').addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
|
||||
document.body.appendChild(modal);
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
db.getPlaylists(true).then((playlists) => {
|
||||
const listEl = modal.querySelector('.playlist-list');
|
||||
if (playlists.length === 0) {
|
||||
listEl.innerHTML = '<div style="padding: 12px; color: var(--muted-foreground);">No playlists yet</div>';
|
||||
} else {
|
||||
listEl.innerHTML = playlists
|
||||
.map(
|
||||
(p) => `
|
||||
<div class="playlist-item" data-playlist-id="${p.id}" style="padding: 12px; cursor: pointer; border-bottom: 1px solid var(--border);">
|
||||
<span>${escapeHtml(p.name)}</span>
|
||||
<span style="color: var(--muted-foreground); font-size: 0.85rem; margin-left: 8px;">${p.tracks?.length || 0} tracks</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
listEl.querySelectorAll('.playlist-item').forEach((item) => {
|
||||
item.addEventListener('click', async () => {
|
||||
const playlistId = item.dataset.playlistId;
|
||||
for (const track of tracks) {
|
||||
await db.addTrackToPlaylist(playlistId, track);
|
||||
}
|
||||
syncManager.syncUserPlaylist(await db.getPlaylist(playlistId), 'update');
|
||||
showNotification(`Added ${tracks.length} tracks to playlist`);
|
||||
closeModal();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
modal.querySelector('.create-new-playlist').addEventListener('click', () => {
|
||||
const name = prompt('Playlist name:');
|
||||
if (name) {
|
||||
db.createPlaylist(name, tracks).then((playlist) => {
|
||||
showNotification(`Created playlist "${name}" with ${tracks.length} tracks`);
|
||||
closeModal();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
||||
const playPauseBtn = document.querySelector('.now-playing-bar .play-pause-btn');
|
||||
const nextBtn = document.getElementById('next-btn');
|
||||
|
|
@ -74,6 +261,104 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
|||
volumeFill.style.width = `${effectiveVolume}%`;
|
||||
};
|
||||
|
||||
function clearSelection() {
|
||||
trackSelection.selectedIds.clear();
|
||||
trackSelection.lastClickedId = null;
|
||||
trackSelection.isSelecting = false;
|
||||
document.body.classList.remove('multi-select-mode');
|
||||
document.querySelectorAll('.track-item.selected').forEach((el) => {
|
||||
el.classList.remove('selected');
|
||||
});
|
||||
document.querySelectorAll('.track-checkbox').forEach((checkbox) => {
|
||||
checkbox.innerHTML = SVG_CHECKBOX(18);
|
||||
checkbox.classList.remove('checked');
|
||||
});
|
||||
updateSelectionBar();
|
||||
}
|
||||
|
||||
function updateSelectionBar() {
|
||||
let bar = document.getElementById('selection-bar');
|
||||
if (!bar) {
|
||||
bar = document.createElement('div');
|
||||
bar.id = 'selection-bar';
|
||||
bar.className = 'selection-bar';
|
||||
bar.innerHTML = `
|
||||
<span class="selection-count">0 selected</span>
|
||||
<div class="selection-actions">
|
||||
<button data-action="play-selected">Play</button>
|
||||
<button data-action="add-to-queue-selected">Add to queue</button>
|
||||
<button data-action="add-to-playlist-selected">Add to playlist</button>
|
||||
<button data-action="download-selected">Download</button>
|
||||
<button data-action="like-selected">Like</button>
|
||||
</div>
|
||||
<button data-action="clear-selection" style="margin-left: 8px;">Clear</button>
|
||||
`;
|
||||
document.body.appendChild(bar);
|
||||
|
||||
bar.querySelectorAll('button').forEach((btn) => {
|
||||
btn.addEventListener('click', () => handleSelectionAction(btn.dataset.action));
|
||||
});
|
||||
}
|
||||
|
||||
const count = trackSelection.selectedIds.size;
|
||||
bar.querySelector('.selection-count').textContent = `${count} selected`;
|
||||
bar.classList.toggle('visible', count > 0);
|
||||
}
|
||||
|
||||
function handleSelectionAction(action) {
|
||||
const selectedIds = getSelectedTracks();
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
const mainContent = document.getElementById('main-content');
|
||||
const selectedTracks = [];
|
||||
mainContent.querySelectorAll('.track-item').forEach((item) => {
|
||||
if (trackSelection.selectedIds.has(item.dataset.trackId)) {
|
||||
const track = trackDataStore.get(item);
|
||||
if (track) selectedTracks.push(track);
|
||||
}
|
||||
});
|
||||
|
||||
switch (action) {
|
||||
case 'play-selected':
|
||||
if (selectedTracks.length > 0) {
|
||||
player.setQueue(selectedTracks, 0);
|
||||
document.getElementById('shuffle-btn').classList.remove('active');
|
||||
player.playTrackFromQueue();
|
||||
}
|
||||
break;
|
||||
case 'add-to-queue-selected':
|
||||
if (selectedTracks.length > 0) {
|
||||
player.addToQueue(selectedTracks);
|
||||
if (window.renderQueueFunction) window.renderQueueFunction();
|
||||
showNotification(`Added ${selectedTracks.length} tracks to queue`);
|
||||
}
|
||||
break;
|
||||
case 'add-to-playlist-selected':
|
||||
if (selectedTracks.length > 0) {
|
||||
showMultiSelectPlaylistModal(selectedTracks);
|
||||
}
|
||||
break;
|
||||
case 'download-selected':
|
||||
if (selectedTracks.length > 0) {
|
||||
selectedTracks.forEach((track) => {
|
||||
downloadTrackWithMetadata(track, downloadQualitySettings.getQuality(), api, lyricsManager);
|
||||
});
|
||||
showNotification(`Downloading ${selectedTracks.length} tracks`);
|
||||
}
|
||||
break;
|
||||
case 'like-selected':
|
||||
selectedTracks.forEach(async (track) => {
|
||||
const added = await db.toggleFavorite('track', track);
|
||||
syncManager.syncLibraryItem('track', track, added);
|
||||
});
|
||||
showNotification(`Liked ${selectedTracks.length} tracks`);
|
||||
break;
|
||||
case 'clear-selection':
|
||||
clearSelection();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (homeStartRadioBtn) {
|
||||
homeStartRadioBtn.addEventListener('click', async () => {
|
||||
await player.enableRadio();
|
||||
|
|
@ -1774,6 +2059,14 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
}
|
||||
contextMenu._contextTrack = contextTrack;
|
||||
contextMenu._contextType = menuBtn.dataset.type || trackItem.dataset.type || 'track';
|
||||
if (trackSelection.isSelecting && trackSelection.selectedIds.size > 0) {
|
||||
const selectedTracks = [];
|
||||
document.querySelectorAll('.track-item.selected').forEach((item) => {
|
||||
const track = trackDataStore.get(item);
|
||||
if (track) selectedTracks.push(track);
|
||||
});
|
||||
contextMenu._selectedTracks = selectedTracks;
|
||||
}
|
||||
await updateContextMenuLikeState(contextMenu, contextTrack);
|
||||
const rect = menuBtn.getBoundingClientRect();
|
||||
positionMenu(contextMenu, rect.left, rect.bottom + 5, rect);
|
||||
|
|
@ -1782,6 +2075,16 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
return;
|
||||
}
|
||||
|
||||
const checkbox = e.target.closest('.track-checkbox');
|
||||
if (checkbox) {
|
||||
e.stopPropagation();
|
||||
const trackItem = checkbox.closest('.track-item');
|
||||
if (trackItem) {
|
||||
toggleTrackSelection(trackItem, isMultiSelectToggle(e), isMultiSelectRange(e));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const trackItem = e.target.closest('.track-item');
|
||||
if (trackItem && (trackItem.classList.contains('unavailable') || trackItem.classList.contains('blocked'))) {
|
||||
return;
|
||||
|
|
@ -1795,6 +2098,22 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
const clickedTrackId = trackItem.dataset.trackId;
|
||||
const isSearch = window.location.pathname.startsWith('/search/');
|
||||
|
||||
if (isMultiSelectToggle(e)) {
|
||||
e.preventDefault();
|
||||
toggleTrackSelection(trackItem, true, isMultiSelectRange(e));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMultiSelectRange(e) && trackSelection.isSelecting) {
|
||||
e.preventDefault();
|
||||
toggleTrackSelection(trackItem, false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (trackSelection.isSelecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSearch) {
|
||||
const clickedTrack = trackDataStore.get(trackItem);
|
||||
if (clickedTrack) {
|
||||
|
|
@ -1886,6 +2205,15 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
contextMenu._originalHTML = null;
|
||||
}
|
||||
|
||||
// Store selected tracks for multi-select actions
|
||||
let selectedTracks = [];
|
||||
if (trackSelection.isSelecting && trackSelection.selectedIds.size > 0) {
|
||||
document.querySelectorAll('.track-item.selected').forEach((item) => {
|
||||
const track = trackDataStore.get(item);
|
||||
if (track) selectedTracks.push(track);
|
||||
});
|
||||
}
|
||||
|
||||
// Hide actions for unavailable tracks
|
||||
const unavailableActions = ['play-next', 'add-to-queue', 'download', 'track-mix'];
|
||||
contextMenu.querySelectorAll('[data-action]').forEach((btn) => {
|
||||
|
|
@ -1896,6 +2224,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
|
||||
contextMenu._contextTrack = contextTrack;
|
||||
contextMenu._contextType = contextTrack.type || 'track';
|
||||
contextMenu._selectedTracks = selectedTracks;
|
||||
await updateContextMenuLikeState(contextMenu, contextTrack);
|
||||
positionMenu(contextMenu, e.clientX, e.clientY);
|
||||
}
|
||||
|
|
@ -1933,7 +2262,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
document.addEventListener('click', (e) => {
|
||||
if (contextMenu.style.display === 'block') {
|
||||
if (contextMenu._originalHTML) {
|
||||
contextMenu.innerHTML = contextMenu._originalHTML;
|
||||
|
|
@ -1942,6 +2271,21 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
contextMenu._contextType = null;
|
||||
contextMenu._originalHTML = null;
|
||||
}
|
||||
|
||||
if (
|
||||
trackSelection.isSelecting &&
|
||||
!e.target.closest('.track-item') &&
|
||||
!e.target.closest('.selection-bar') &&
|
||||
!e.target.closest('.track-checkbox')
|
||||
) {
|
||||
clearSelection();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && trackSelection.isSelecting) {
|
||||
clearSelection();
|
||||
}
|
||||
});
|
||||
|
||||
contextMenu.addEventListener('click', async (e) => {
|
||||
|
|
@ -1983,9 +2327,55 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
}
|
||||
|
||||
if (action && track) {
|
||||
// Track context menu action
|
||||
trackContextMenuAction(action, type, track);
|
||||
await handleTrackAction(action, track, player, api, lyricsManager, type, ui, scrobbler, target.dataset);
|
||||
const selectedTracks = contextMenu._selectedTracks || [];
|
||||
const isMultiSelect = selectedTracks.length > 1;
|
||||
|
||||
if (isMultiSelect) {
|
||||
// Handle multi-select actions
|
||||
switch (action) {
|
||||
case 'play-next':
|
||||
selectedTracks.forEach((t) => {
|
||||
trackPlayNext(t);
|
||||
player.addNextToQueue(t);
|
||||
});
|
||||
if (window.renderQueueFunction) window.renderQueueFunction();
|
||||
showNotification(`Playing next: ${selectedTracks.length} tracks`);
|
||||
clearSelection();
|
||||
break;
|
||||
case 'add-to-queue':
|
||||
player.addToQueue(selectedTracks);
|
||||
if (window.renderQueueFunction) window.renderQueueFunction();
|
||||
showNotification(`Added ${selectedTracks.length} tracks to queue`);
|
||||
clearSelection();
|
||||
break;
|
||||
case 'toggle-like':
|
||||
selectedTracks.forEach(async (t) => {
|
||||
const added = await db.toggleFavorite('track', t);
|
||||
syncManager.syncLibraryItem('track', t, added);
|
||||
});
|
||||
showNotification(`Liked ${selectedTracks.length} tracks`);
|
||||
clearSelection();
|
||||
break;
|
||||
case 'add-to-playlist':
|
||||
showMultiSelectPlaylistModal(selectedTracks);
|
||||
clearSelection();
|
||||
break;
|
||||
case 'download':
|
||||
selectedTracks.forEach((t) => {
|
||||
downloadTrackWithMetadata(t, downloadQualitySettings.getQuality(), api, lyricsManager);
|
||||
});
|
||||
showNotification(`Downloading ${selectedTracks.length} tracks`);
|
||||
clearSelection();
|
||||
break;
|
||||
default:
|
||||
clearSelection();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Track context menu action
|
||||
trackContextMenuAction(action, type, track);
|
||||
await handleTrackAction(action, track, player, api, lyricsManager, type, ui, scrobbler, target.dataset);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset menu state before closing
|
||||
|
|
@ -1995,6 +2385,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
}
|
||||
contextMenu.style.display = 'none';
|
||||
contextMenu._contextType = null;
|
||||
contextMenu._selectedTracks = null;
|
||||
});
|
||||
|
||||
// Now playing bar interactions
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
export { default as SVG_ANIMATE_SPIN } from '../images/animate-spin.svg?svg&icon';
|
||||
export { default as SVG_APPLE } from '../images/apple.svg?svg&icon';
|
||||
export { default as SVG_BIN } from '!lucide/trash-2.svg?svg&icon';
|
||||
export { default as SVG_CHECK } from '!lucide/check.svg?svg&icon';
|
||||
export { default as SVG_CHECKBOX } from '!lucide/square.svg?svg&icon';
|
||||
export { default as SVG_CHECKBOX_CHECKED } from '!lucide/check-square.svg?svg&icon';
|
||||
export { default as SVG_CLOCK } from '!lucide/clock.svg?svg&icon';
|
||||
export { default as SVG_CLOSE } from '!lucide/x.svg?svg&icon';
|
||||
export { default as SVG_DOWNLOAD } from '!lucide/download.svg?svg&icon';
|
||||
|
|
|
|||
|
|
@ -2747,6 +2747,14 @@ export const keyboardShortcuts = {
|
|||
alt: false,
|
||||
description: 'Toggle visualizer auto-cycle',
|
||||
},
|
||||
multiSelectToggle: {
|
||||
key: 'control',
|
||||
shift: false,
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
description: 'Toggle track selection (individual)',
|
||||
},
|
||||
multiSelectRange: { key: 'shift', shift: true, ctrl: false, alt: false, description: 'Select track range' },
|
||||
},
|
||||
|
||||
getShortcuts() {
|
||||
|
|
|
|||
3
js/ui.js
3
js/ui.js
|
|
@ -82,6 +82,7 @@ import {
|
|||
SVG_CLOCK,
|
||||
SVG_MOVE_UP,
|
||||
SVG_MOVE_DOWN,
|
||||
SVG_CHECKBOX,
|
||||
} from './icons.js';
|
||||
|
||||
function sortTracks(tracks, sortType) {
|
||||
|
|
@ -397,6 +398,7 @@ export class UIRenderer {
|
|||
? `<span class="video-item-icon" title="Music Video" style="display: inline-flex; align-items: center; margin-right: 4px; color: var(--muted-foreground);">${SVG_VIDEO(14)}</span>`
|
||||
: '';
|
||||
const trackNumberHTML = `<div class="track-number">${showCover ? trackImageHTML : displayIndex}</div>`;
|
||||
const checkboxHTML = `<div class="track-checkbox" data-action="toggle-select">${SVG_CHECKBOX(18)}</div>`;
|
||||
const explicitBadge = hasExplicitContent(track) ? this.createExplicitBadge() : '';
|
||||
const qualityBadge = createQualityBadgeHTML(track);
|
||||
const trackTitle = getTrackTitle(track);
|
||||
|
|
@ -437,6 +439,7 @@ export class UIRenderer {
|
|||
${track.isLocal ? 'data-is-local="true"' : ''}
|
||||
${isUnavailable ? 'title="This track is currently unavailable"' : ''}
|
||||
${blockedTitle}>
|
||||
${checkboxHTML}
|
||||
${trackNumberHTML}
|
||||
<div class="track-item-info">
|
||||
<div class="track-item-details">
|
||||
|
|
|
|||
88
styles.css
88
styles.css
|
|
@ -2156,6 +2156,7 @@ input[type='search']::-webkit-search-cancel-button {
|
|||
transform var(--transition-fast);
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.track-item:hover {
|
||||
|
|
@ -2200,6 +2201,93 @@ input[type='search']::-webkit-search-cancel-button {
|
|||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.track-checkbox {
|
||||
display: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
color: var(--muted-foreground);
|
||||
flex-shrink: 0;
|
||||
transition: color var(--transition-fast);
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.multi-select-mode .track-checkbox,
|
||||
body.multi-select-mode .track-checkbox {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.track-checkbox:hover {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.track-checkbox.checked {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.track-item.selected {
|
||||
background-color: rgb(var(--highlight-rgb), 0.1);
|
||||
}
|
||||
|
||||
body.multi-select-mode .track-item {
|
||||
cursor: default;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
body.multi-select-mode .track-item:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.selection-bar {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
padding: 12px 20px;
|
||||
border-radius: 24px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selection-bar.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selection-bar .selection-count {
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.selection-bar .selection-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selection-bar button {
|
||||
background: transparent;
|
||||
border: 1px solid var(--primary-foreground);
|
||||
color: var(--primary-foreground);
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.selection-bar button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.track-number {
|
||||
color: var(--muted-foreground);
|
||||
text-align: center;
|
||||
|
|
|
|||
Loading…
Reference in a new issue