fix(svelte): resolve warnings in 26 components

Phase 1 (real warnings):
- SearchView: add $state() to searchInput and scrollContainer
- DiscoverBrowseView: use $derived() for genreContext
- FavoritesView: replace deprecated svelte:component
- CastPicker: replace deprecated svelte:component (2 instances)
- NowPlayingBar: remove unused .section-separator CSS
- SettingsView: remove unused nested .experimental-badge CSS
- PlaylistDetailView: remove unused .playlist-api-image CSS
- PurchaseAlbumDetailView: remove unused .download-done CSS
- DiscoverPlaylistsBrowseView: fix nested buttons (outer to div)
- AlbumCreditsModal: fix nested button (inner to div)

Phase 2 (a11y warnings):
- Add tabindex="-1" to dialog/menu elements (7 files)
- Add svelte-ignore for autofocus (3 files)
- Add onkeydown handlers to role="button" elements (5 files)
- Add role/svelte-ignore to onclick divs (6 files)
- Add role="menu" to dropdown menus (4 files)
- Add svelte-ignore for drag handlers (3 files)
- Fix label a11y: add aria-label, convert decorative labels to spans
This commit is contained in:
vicrodh committed 2026-03-04 19:55:32 -06:00
1 parent 94f26ce665
commit d50b2cf03a
26 files changed
+237 -187

No files matched your search

+3 -1
View File
@@ -45,7 +45,9 @@
</script>
{#if isOpen}
<div class="modal-backdrop" onclick={onClose}>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="modal-backdrop" onclick={onClose} role="presentation">
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="modal" onclick={(e) => e.stopPropagation()}>
<!-- Header -->
<div class="modal-header">
+104 -99
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { Play, ChevronDown, ChevronUp, Loader2, X } from 'lucide-svelte';
import type { AlbumCredits, QobuzAlbum, Performer, TrackCredits } from '$lib/types';
import type { AlbumCredits, QobuzAlbum, Performer, TrackCredits } from '$lib/types';
interface Props {
isOpen: boolean;
@@ -50,15 +50,15 @@
}
});
async function loadAlbumCredits(id: string) {
loading = true;
error = null;
try {
const album = await invoke<QobuzAlbum>('v2_get_album', { albumId: id });
credits = mapAlbumToCredits(album);
} catch (e) {
error = e instanceof Error ? e.message : String(e);
credits = null;
async function loadAlbumCredits(id: string) {
loading = true;
error = null;
try {
const album = await invoke<QobuzAlbum>('v2_get_album', { albumId: id });
credits = mapAlbumToCredits(album);
} catch (e) {
error = e instanceof Error ? e.message : String(e);
credits = null;
} finally {
loading = false;
}
@@ -79,97 +79,99 @@
onTrackPlay?.(track);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose();
}
}
function parsePerformers(performersStr?: string): Performer[] {
if (!performersStr) return [];
return performersStr
.split(' - ')
.map((performerChunk) => {
const parts = performerChunk.split(',').map((part) => part.trim()).filter(Boolean);
if (parts.length === 0) return null;
const [name, ...roles] = parts;
return { name, roles };
})
.filter((performer): performer is Performer => performer !== null);
}
function formatTrackDuration(seconds: number): string {
const totalSeconds = Math.max(0, Math.floor(seconds || 0));
const minutes = Math.floor(totalSeconds / 60);
const secs = totalSeconds % 60;
return `${minutes}:${String(secs).padStart(2, '0')}`;
}
function formatAlbumDuration(seconds: number): string {
const totalSeconds = Math.max(0, Math.floor(seconds || 0));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
function formatQuality(bitDepth?: number, samplingRate?: number): string {
if (bitDepth && samplingRate) return `${bitDepth}-bit / ${samplingRate} kHz`;
if (bitDepth) return `${bitDepth}-bit`;
if (samplingRate) return `${samplingRate} kHz`;
return '-';
}
function mapAlbumToCredits(album: QobuzAlbum): AlbumCredits {
const artwork = album.image?.large || album.image?.thumbnail || album.image?.small || '';
const tracks = album.tracks?.items ?? [];
const releaseDate = album.release_date_original || '';
const releaseYear = releaseDate ? String(new Date(releaseDate).getFullYear()) : '';
const mappedTracks: TrackCredits[] = tracks.map((trackItem, index) => ({
id: trackItem.id,
number: trackItem.track_number || index + 1,
title: trackItem.title,
artist: trackItem.performer?.name || album.artist?.name || 'Unknown Artist',
duration: formatTrackDuration(trackItem.duration || 0),
duration_seconds: trackItem.duration || 0,
performers: parsePerformers(trackItem.performers),
copyright: trackItem.copyright,
album_id: album.id,
artist_id: trackItem.performer?.id
}));
return {
album: {
id: album.id,
artwork,
title: album.title,
artist: album.artist?.name || 'Unknown Artist',
artist_id: album.artist?.id,
year: releaseYear,
release_date: releaseDate || undefined,
label: album.label?.name || '',
label_id: album.label?.id,
genre: album.genre?.name || '',
quality: formatQuality(album.maximum_bit_depth, album.maximum_sampling_rate),
track_count: album.tracks_count ?? mappedTracks.length,
duration: formatAlbumDuration(album.duration || 0),
bit_depth: album.maximum_bit_depth,
sampling_rate: album.maximum_sampling_rate,
description: album.description
},
tracks: mappedTracks
};
}
</script>
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose();
}
}
function parsePerformers(performersStr?: string): Performer[] {
if (!performersStr) return [];
return performersStr
.split(' - ')
.map((performerChunk) => {
const parts = performerChunk.split(',').map((part) => part.trim()).filter(Boolean);
if (parts.length === 0) return null;
const [name, ...roles] = parts;
return { name, roles };
})
.filter((performer): performer is Performer => performer !== null);
}
function formatTrackDuration(seconds: number): string {
const totalSeconds = Math.max(0, Math.floor(seconds || 0));
const minutes = Math.floor(totalSeconds / 60);
const secs = totalSeconds % 60;
return `${minutes}:${String(secs).padStart(2, '0')}`;
}
function formatAlbumDuration(seconds: number): string {
const totalSeconds = Math.max(0, Math.floor(seconds || 0));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
function formatQuality(bitDepth?: number, samplingRate?: number): string {
if (bitDepth && samplingRate) return `${bitDepth}-bit / ${samplingRate} kHz`;
if (bitDepth) return `${bitDepth}-bit`;
if (samplingRate) return `${samplingRate} kHz`;
return '-';
}
function mapAlbumToCredits(album: QobuzAlbum): AlbumCredits {
const artwork = album.image?.large || album.image?.thumbnail || album.image?.small || '';
const tracks = album.tracks?.items ?? [];
const releaseDate = album.release_date_original || '';
const releaseYear = releaseDate ? String(new Date(releaseDate).getFullYear()) : '';
const mappedTracks: TrackCredits[] = tracks.map((trackItem, index) => ({
id: trackItem.id,
number: trackItem.track_number || index + 1,
title: trackItem.title,
artist: trackItem.performer?.name || album.artist?.name || 'Unknown Artist',
duration: formatTrackDuration(trackItem.duration || 0),
duration_seconds: trackItem.duration || 0,
performers: parsePerformers(trackItem.performers),
copyright: trackItem.copyright,
album_id: album.id,
artist_id: trackItem.performer?.id
}));
return {
album: {
id: album.id,
artwork,
title: album.title,
artist: album.artist?.name || 'Unknown Artist',
artist_id: album.artist?.id,
year: releaseYear,
release_date: releaseDate || undefined,
label: album.label?.name || '',
label_id: album.label?.id,
genre: album.genre?.name || '',
quality: formatQuality(album.maximum_bit_depth, album.maximum_sampling_rate),
track_count: album.tracks_count ?? mappedTracks.length,
duration: formatAlbumDuration(album.duration || 0),
bit_depth: album.maximum_bit_depth,
sampling_rate: album.maximum_sampling_rate,
description: album.description
},
tracks: mappedTracks
};
}
</script>
<svelte:window on:keydown={handleKeydown} />
{#if isOpen}
<div class="modal-overlay" onclick={onClose} role="dialog" aria-modal="true">
<div class="credits-modal" onclick={(e) => e.stopPropagation()}>
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="modal-overlay" onclick={onClose} role="presentation">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="credits-modal" role="dialog" aria-modal="true" tabindex="-1" onclick={(e) => e.stopPropagation()}>
{#if loading}
<div class="loading-state">
<Loader2 size={32} class="spinner" />
@@ -296,13 +298,16 @@
>
<div class="track-number">
{#if isHovered && onTrackPlay}
<button
<div
class="play-btn"
role="button"
tabindex="0"
onclick={(e) => handleTrackPlay(track, e)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleTrackPlay(track, e); } }}
aria-label="Play track"
>
<Play size={14} fill="currentColor" />
</button>
</div>
{:else}
<span>{track.number}</span>
{/if}
+4 -2
View File
@@ -183,9 +183,10 @@
<!-- Connected Device Banner -->
{#if castState.isConnected && castState.device}
{@const ProtocolIcon = getProtocolIcon(castState.protocol!)}
<div class="connected-banner">
<div class="connected-info">
<svelte:component this={getProtocolIcon(castState.protocol!)} size={20} />
<ProtocolIcon size={20} />
<div class="connected-text">
<span class="connected-label">Connected to</span>
<span class="connected-name">{castState.device.name}</span>
@@ -249,13 +250,14 @@
{:else}
<div class="devices">
{#each devices() as device}
{@const CastIcon = getProtocolIcon(activeProtocol)}
<button class="device" onclick={() => handleConnect(device)}>
<Monitor size={24} />
<div class="device-info">
<span class="device-name">{device.name}</span>
<span class="device-ip">{device.ip}</span>
</div>
<svelte:component this={getProtocolIcon(activeProtocol)} size={20} class="cast-icon" />
<CastIcon size={20} class="cast-icon" />
</button>
{/each}
</div>
@@ -80,11 +80,13 @@
{#if isOpen}
<div class="modal-backdrop" onclick={handleBackdropClick} role="presentation">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="keybindings-settings-title"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
>
<header class="modal-header">
@@ -88,11 +88,13 @@
onclick={handleBackdropClick}
role="presentation"
>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="shortcuts-modal-title"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
>
<header class="modal-header">
+2
View File
@@ -61,12 +61,14 @@
<svelte:window onkeydown={handleKeydown} />
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="modal-backdrop"
onclick={handleBackdropClick}
role="dialog"
aria-modal="true"
aria-labelledby="musician-modal-title"
tabindex="-1"
>
<div class="modal">
<!-- Header -->
-9
View File
@@ -673,15 +673,6 @@
50% { opacity: 0.6; }
}
/* Section Separator */
.section-separator {
width: 1px;
height: 20px;
background: var(--border-subtle);
margin: 0 8px;
flex-shrink: 0;
}
/* Queue Button & Icon */
.queue-btn {
width: 32px;
@@ -328,6 +328,7 @@
aria-modal="true"
tabindex="-1"
>
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="modal" onclick={(e) => e.stopPropagation()}>
<div class="modal-header">
<div class="header-title">
+4 -2
View File
@@ -660,6 +660,7 @@
aria-modal="true"
tabindex="-1"
>
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="modal" onclick={(e) => e.stopPropagation()}>
<div class="modal-header">
<h2>
@@ -782,7 +783,7 @@
{#if mode === 'edit'}
<div class="form-group">
<label>{$t('playlist.customArtwork')}</label>
<span class="form-label">{$t('playlist.customArtwork')}</span>
<div class="playlist-artwork-editor">
<div class="playlist-artwork-preview">
{#if customArtworkPath}
@@ -1022,7 +1023,8 @@
margin-bottom: 16px;
}
.form-group label {
.form-group label,
.form-group .form-label {
display: block;
font-size: 13px;
font-weight: 500;
+3
View File
@@ -269,6 +269,7 @@
</script>
<!-- Queue Panel -->
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
<aside class="queue-panel" onclick={handlePanelClick}>
<!-- Header with Tabs -->
<div class="header">
@@ -354,6 +355,7 @@
ondragend={handleDragEnd}
role="button"
tabindex={isUnavailable ? -1 : 0}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleTrackClick(queueTrack); } }}
>
<span class="track-number">{originalIndex + 1}</span>
<div class="track-info">
@@ -415,6 +417,7 @@
onmouseleave={() => hoveredHistoryTrack = null}
role="button"
tabindex="0"
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleHistoryTrackClick(track); } }}
>
<img src={track.artwork} alt={track.title} class="history-artwork" />
<div class="track-info">
+14 -1
View File
@@ -1618,6 +1618,8 @@
class="dropdown-menu"
bind:this={menuEl}
style={menuStyle}
role="menu"
tabindex="-1"
onmouseenter={() => isHoveringDropdown = true}
onmouseleave={() => isHoveringDropdown = false}
>
@@ -1629,6 +1631,7 @@
tabindex="0"
onmouseenter={openSubmenu}
onmouseleave={closeSubmenuDelayed}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openSubmenu(); } }}
>
<ArrowUpDown size={14} />
<span class="menu-label">{$t('library.sortBy')}</span>
@@ -1641,6 +1644,8 @@
class="submenu"
bind:this={submenuEl}
style={submenuStyle}
role="menu"
tabindex="-1"
onmouseenter={openSubmenu}
onmouseleave={closeSubmenuDelayed}
>
@@ -1723,6 +1728,7 @@
</span>
</button>
{:else if item.type === 'folder-playlist'}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="playlist-drag-wrapper"
class:dragging={draggedPlaylistId === item.playlist.id}
@@ -1744,6 +1750,7 @@
</NavigationItem>
</div>
{:else if item.type === 'root-playlist'}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="playlist-drag-wrapper"
class:dragging={draggedPlaylistId === item.playlist.id}
@@ -1880,6 +1887,7 @@
{@const availableFolders = folders.filter(f => f.id !== contextMenu.currentFolderId)}
{@const showSearch = availableFolders.length >= FOLDER_SEARCH_THRESHOLD}
{@const contextPlaylistHidden = contextMenu.playlist ? (playlistSettings.get(contextMenu.playlist.id)?.hidden ?? false) : false}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="context-menu"
class:has-search={showSearch}
@@ -1888,6 +1896,7 @@
onmouseenter={() => isHoveringContextMenu = true}
onmouseleave={() => isHoveringContextMenu = false}
role="menu"
tabindex="-1"
>
{#if contextMenu.folder}
{@const contextFolderHidden = contextMenu.folder.is_hidden ?? false}
@@ -1974,6 +1983,7 @@
<!-- Collapsed Folder Popover -->
{#if folderPopover.visible}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="folder-popover"
style="left: {folderPopover.x}px; top: {folderPopover.y}px;"
@@ -1981,6 +1991,7 @@
onmouseenter={() => isHoveringFolderPopover = true}
onmouseleave={() => isHoveringFolderPopover = false}
role="menu"
tabindex="-1"
>
<div class="folder-popover-header">
<Folder size={14} />
@@ -2011,10 +2022,12 @@
<!-- Create Folder Modal -->
{#if showCreateFolderModal}
<div class="modal-overlay" onclick={cancelCreateFolder} role="presentation">
<div class="modal-content create-folder-modal" onclick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="modal-content create-folder-modal" onclick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" tabindex="-1">
<h2 class="modal-title">{$t('playlist.newFolder', { default: 'New Folder' })}</h2>
<div class="form-group">
<label for="folder-name">{$t('common.name', { default: 'Name' })}</label>
<!-- svelte-ignore a11y_autofocus -->
<input
id="folder-name"
type="text"
+37 -35
View File
@@ -56,8 +56,8 @@
return role.replace(/([A-Z])/g, ' $1').trim().toUpperCase();
}
// Group performers by role and return ordered entries
function getGroupedCredits(performers: Performer[]): { role: string; names: string[] }[] {
// Group performers by role and return ordered entries
function getGroupedCredits(performers: Performer[]): { role: string; names: string[] }[] {
const grouped: Record<string, string[]> = {};
performers.forEach(performer => {
@@ -98,25 +98,25 @@
return a.localeCompare(b);
});
return entries.map(([role, names]) => ({ role, names }));
}
// Parse Qobuz performers string: "Name, Role - Name, Role"
function parsePerformers(performersStr?: string): Performer[] {
if (!performersStr) return [];
return performersStr
.split(' - ')
.map(segment => segment.trim())
.filter(Boolean)
.map(segment => {
const parts = segment.split(', ').map(p => p.trim()).filter(Boolean);
return {
name: parts[0] || '',
roles: parts.slice(1)
};
})
.filter(p => p.name.length > 0);
}
return entries.map(([role, names]) => ({ role, names }));
}
// Parse Qobuz performers string: "Name, Role - Name, Role"
function parsePerformers(performersStr?: string): Performer[] {
if (!performersStr) return [];
return performersStr
.split(' - ')
.map(segment => segment.trim())
.filter(Boolean)
.map(segment => {
const parts = segment.split(', ').map(p => p.trim()).filter(Boolean);
return {
name: parts[0] || '',
roles: parts.slice(1)
};
})
.filter(p => p.name.length > 0);
}
// Load track info when modal opens
$effect(() => {
@@ -128,18 +128,18 @@
}
});
async function loadTrackInfo(id: number) {
loading = true;
error = null;
try {
const track = await invoke<TrackInfo['track']>('v2_get_track', { trackId: id });
trackInfo = {
track,
performers: parsePerformers(track.performers)
};
} catch (e) {
error = e instanceof Error ? e.message : String(e);
trackInfo = null;
async function loadTrackInfo(id: number) {
loading = true;
error = null;
try {
const track = await invoke<TrackInfo['track']>('v2_get_track', { trackId: id });
trackInfo = {
track,
performers: parsePerformers(track.performers)
};
} catch (e) {
error = e instanceof Error ? e.message : String(e);
trackInfo = null;
} finally {
loading = false;
}
@@ -160,8 +160,10 @@
<svelte:window on:keydown={handleKeydown} />
{#if isOpen}
<div class="modal-overlay" onclick={onClose} role="dialog" aria-modal="true">
<div class="track-modal" onclick={(e) => e.stopPropagation()}>
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="modal-overlay" onclick={onClose} role="presentation">
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="track-modal" onclick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" tabindex="-1">
{#if loading}
<div class="loading-state">
<Loader2 size={32} class="spinner" />
@@ -1632,7 +1632,7 @@
Qobuz Radio
</button>
</div>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="radio-dropdown-backdrop" onclick={() => radioDropdownOpen = false}></div>
{/if}
{#if isRadioLoading && radioLoadingMessage}
@@ -1692,7 +1692,7 @@
</p>
</button>
</div>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="hide-dropdown-backdrop" onclick={() => showHideDropdown = false}></div>
{/if}
</div>
@@ -1858,11 +1858,13 @@
}}
>
{#if multiSelectMode}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
<label class="track-checkbox-wrap" onclick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={multiSelectedIds.has(track.id)}
onchange={() => toggleMultiSelect(track.id)}
aria-label={$t('actions.select')}
style="width:15px;height:15px;cursor:pointer;accent-color:var(--accent-primary);"
/>
</label>
@@ -239,10 +239,12 @@
onclick={() => confirmClearOpen = false}
onkeydown={(e) => e.key === 'Escape' && (confirmClearOpen = false)}
>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="modal-content"
role="dialog"
aria-modal="true"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
>
<h2 class="modal-title">{$t('settings.blacklist.confirmClearTitle')}</h2>
@@ -92,7 +92,7 @@
albumOfTheWeek: 'discover-albums-of-the-week',
pressAward: 'discover-press-accolades',
};
const genreContext = GENRE_CONTEXT_MAP[endpointType];
const genreContext = $derived(GENRE_CONTEXT_MAP[endpointType]);
// State
let albums = $state<FavoriteAlbum[]>([]);
@@ -291,9 +291,12 @@
<!-- List View -->
<div class="playlist-list">
{#each getFilteredPlaylists() as playlist (playlist.id)}
<button
<div
class="list-row"
role="button"
tabindex="0"
onclick={() => onPlaylistClick?.(playlist.id)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPlaylistClick?.(playlist.id); } }}
>
<div class="list-artwork">
{#if playlist.image?.rectangle || playlist.image?.covers?.[0]}
@@ -344,7 +347,7 @@
</button>
{/if}
</div>
</button>
</div>
{/each}
</div>
{/if}
@@ -1326,6 +1326,7 @@
{:else}
<div class="search-expanded">
<Search size={16} class="search-icon-inline" />
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
placeholder={$t('placeholders.search')}
@@ -1346,12 +1347,13 @@
<div class="favorites-nav">
<div class="nav-left">
{#each favoritesPreferences.tab_order as tab}
{@const Icon = getTabIcon(tab as TabType)}
<button
class="nav-link"
class:active={activeTab === tab}
onclick={() => handleTabChange(tab as TabType)}
>
<svelte:component this={getTabIcon(tab as TabType)} size={16} />
<Icon size={16} />
<span>{getTabLabel(tab as TabType)}</span>
</button>
{/each}
@@ -263,6 +263,7 @@
{#if searchExpanded}
<div class="search-expanded">
<Search size={16} class="search-icon-inline" />
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
class="search-input-inline"
@@ -880,11 +880,13 @@
}}
>
{#if multiSelectMode}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
<label class="track-checkbox-wrap" onclick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={multiSelectedIds.has(track.id)}
onchange={() => toggleMultiSelect(track.id)}
aria-label={$t('actions.select')}
style="width:15px;height:15px;cursor:pointer;accent-color:var(--accent-primary);"
/>
</label>
@@ -3579,8 +3579,8 @@
{:else}
<div class="album-list">
{#each hiddenAlbums as album (album.id)}
<div class="album-row" role="button" tabindex="0">
<div class="album-row-art" onclick={() => handleAlbumClick(album)}>
<div class="album-row" role="button" tabindex="0" onclick={() => handleAlbumClick(album)} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleAlbumClick(album); } }}>
<div class="album-row-art">
{#if album.artwork_path}
<img src={getArtworkUrl(album.artwork_path)} alt={album.title} loading="lazy" decoding="async" />
{:else}
@@ -3589,7 +3589,7 @@
</div>
{/if}
</div>
<div class="album-row-info" onclick={() => handleAlbumClick(album)}>
<div class="album-row-info">
<div class="album-row-title">{album.title}</div>
<div class="album-row-artist">{album.artist}</div>
<div class="album-row-meta">
@@ -3677,8 +3677,10 @@
{/if}
</button>
{#if showFilterPanel}
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
<div
class="filter-panel"
role="region"
onmouseenter={clearFilterPanelTimer}
onmouseleave={startFilterPanelTimer}
onclick={handleFilterPanelActivity}
@@ -4108,8 +4110,9 @@
<!-- Album Settings Modal -->
{#if showAlbumEditModal && selectedAlbum}
<div class="modal-overlay" onclick={() => showAlbumEditModal = false}>
<div class="modal" onclick={(e: MouseEvent) => e.stopPropagation()}>
<div class="modal-overlay" onclick={() => showAlbumEditModal = false} role="presentation">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="modal" role="dialog" aria-modal="true" tabindex="-1" onclick={(e: MouseEvent) => e.stopPropagation()}>
<div class="modal-header">
<h2>Album Settings</h2>
<button class="close-btn" onclick={() => showAlbumEditModal = false}>
@@ -4150,7 +4153,7 @@
<div class="form-group">
<div class="artwork-layout-header" class:discogs-active={discogsFetchSuccessful}>
<label>Album Artwork</label>
<span class="form-label">Album Artwork</span>
{#if discogsFetchSuccessful}
<div class="discogs-layout-label">Select Artwork from Discogs</div>
{/if}
@@ -5825,7 +5828,7 @@
margin-bottom: 20px;
}
.form-group label {
.form-group .form-label {
display: block;
font-size: 13px;
font-weight: 500;
@@ -5845,7 +5848,7 @@
grid-template-columns: 1fr 1fr;
}
.artwork-layout-header label {
.artwork-layout-header .form-label {
margin-bottom: 0;
}
@@ -2268,6 +2268,7 @@
{@const available = isTrackAvailable(track)}
{@const removedFromQobuz = isTrackRemovedFromQobuz(track)}
{@const trackBlacklisted = !track.isLocal && track.artistId ? isArtistBlacklisted(track.artistId) : false}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="track-row-wrapper virtual-track-item"
class:unavailable={!available}
@@ -2288,21 +2289,25 @@
>
{#if multiSelectMode && !isCustomOrderMode}
{@const trackKey = getTrackKey(track)}
<label class="track-checkbox" onclick={(e) => e.stopPropagation()}>
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
<label class="track-checkbox" onclick={(e: MouseEvent) => e.stopPropagation()}>
<input
type="checkbox"
checked={multiSelectedKeys.has(trackKey)}
onchange={() => toggleMultiSelect(track)}
aria-label={$t('actions.select')}
/>
</label>
{/if}
{#if isCustomOrderMode}
{@const trackKey = getTrackKey(track)}
<label class="track-checkbox" onclick={(e) => e.stopPropagation()}>
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
<label class="track-checkbox" onclick={(e: MouseEvent) => e.stopPropagation()}>
<input
type="checkbox"
checked={selectedTrackKeys.has(trackKey)}
onchange={() => toggleTrackSelection(track)}
aria-label={$t('actions.select')}
/>
</label>
<div class="reorder-controls">
@@ -3208,13 +3213,6 @@
border-color: var(--alpha-20);
}
/* Playlist API image (pre-made collage from Qobuz) */
.playlist-api-image {
border-radius: 8px;
object-fit: cover;
display: block;
}
.track-list-bottom-spacer {
height: 64px;
}
@@ -950,6 +950,8 @@
{#if showFilterMenu}
<div
class="dropdown-menu"
role="menu"
tabindex="-1"
onmouseenter={() => isHoveringFilterMenu = true}
onmouseleave={() => isHoveringFilterMenu = false}
>
@@ -998,6 +1000,8 @@
{#if showSortMenu}
<div
class="dropdown-menu"
role="menu"
tabindex="-1"
onmouseenter={() => isHoveringSortMenu = true}
onmouseleave={() => isHoveringSortMenu = false}
>
@@ -1104,6 +1108,7 @@
{#if viewMode === 'grid'}
<div class="folders-grid">
{#each folders as folder (folder.id)}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="folder-card"
class:drag-over={dragOverFolderId === folder.id}
@@ -1219,6 +1224,7 @@
{@const isFavorite = playlistSettings.get(playlist.id)?.is_favorite}
{@const localStatus = getLocalContentStatus(playlist.id)}
{@const isUnavailable = offlineStatus.isOffline && !isPlaylistAvailableOffline(playlist.id)}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="grid-item"
class:hidden={isHidden}
@@ -1267,6 +1273,7 @@
role="button"
tabindex="0"
onclick={() => onPlaylistSelect?.(playlist.id)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPlaylistSelect?.(playlist.id); } }}
title={isUnavailable ? $t('offline.viewOnly') : undefined}
>
<div class="artwork">
@@ -1373,6 +1380,7 @@
role="button"
tabindex="0"
onclick={() => onPlaylistSelect?.(playlist.id)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPlaylistSelect?.(playlist.id); } }}
>
<div class="tree-item-artwork">
<PlaylistCollage artworks={playlist.images ?? []} size={32} />
@@ -1433,6 +1441,7 @@
role="button"
tabindex="0"
onclick={() => onPlaylistSelect?.(playlist.id)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPlaylistSelect?.(playlist.id); } }}
>
<div class="tree-item-artwork">
<PlaylistCollage artworks={playlist.images ?? []} size={32} />
@@ -1501,6 +1510,7 @@
role="button"
tabindex="0"
onclick={() => onPlaylistSelect?.(playlist.id)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPlaylistSelect?.(playlist.id); } }}
title={isUnavailable ? $t('offline.viewOnly') : undefined}
>
{#if sort === 'custom' && !isUnavailable}
@@ -389,6 +389,7 @@
{@const isActive = activeTrackId === track.id}
{@const isDownloadedForFormat = selectedFormatId !== null && (track.downloaded_format_ids ?? []).includes(selectedFormatId)}
{@const isDownloaded = isDownloadedForFormat || status === 'complete'}
<!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div
class="track-row"
class:downloading={status === 'downloading'}
@@ -399,6 +400,7 @@
onclick={() => track.streamable && onTrackPlay?.(toDisplayTrack(track))}
role={track.streamable && onTrackPlay ? 'button' : undefined}
tabindex={track.streamable && onTrackPlay ? 0 : undefined}
onkeydown={track.streamable && onTrackPlay ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onTrackPlay?.(toDisplayTrack(track)); } } : undefined}
>
<div class="col-number">
{#if isActive && isPlaybackActive}
@@ -900,12 +902,6 @@
justify-content: center;
}
.download-done {
color: var(--success, #4caf50);
display: flex;
align-items: center;
}
.download-active {
color: var(--accent-primary);
display: flex;
@@ -907,6 +907,7 @@
{#each group.items as track (track.id)}
{@const dlStatus = getTrackDownloadStatus(track.id)}
{@const isDownloaded = track.downloaded || dlStatus === 'complete'}
<!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div
class="track-row"
class:active={activeTrackId === track.id}
@@ -916,6 +917,7 @@
onclick={() => track.streamable && onTrackPlay?.(toDisplayTrack(track))}
role={track.streamable && onTrackPlay ? 'button' : undefined}
tabindex={track.streamable && onTrackPlay ? 0 : undefined}
onkeydown={track.streamable && onTrackPlay ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onTrackPlay?.(toDisplayTrack(track)); } } : undefined}
>
<div class="track-artwork">
{#if track.album?.image}
+9 -2
View File
@@ -14,10 +14,10 @@
import { saveScrollPosition, getSavedScrollPosition } from '$lib/stores/navigationStore';
import { t } from '$lib/i18n';
let searchInput: HTMLInputElement | null = null;
let searchInput = $state<HTMLInputElement | null>(null);
let albumsCarouselContainer = $state<HTMLDivElement | null>(null);
let artistsCarouselContainer = $state<HTMLDivElement | null>(null);
let scrollContainer: HTMLDivElement | null = null;
let scrollContainer = $state<HTMLDivElement | null>(null);
let isScrolled = $state(false);
let currentAlbumPage = $state(0);
let currentArtistPage = $state(0);
@@ -1338,11 +1338,14 @@
<MoreHorizontal size={16} />
</button>
{#if mostPopularMenuOpen}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="popular-menu"
bind:this={popularMenuEl}
style={popularMenuStyle}
use:portal
role="menu"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
>
<button class="menu-item" onclick={() => { onAlbumPlay?.(album.id); mostPopularMenuOpen = false; }}>
@@ -1395,6 +1398,7 @@
</div>
{:else if allResults.most_popular?.type === 'tracks'}
{@const track = allResults.most_popular.content}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="popular-card most-popular-card" class:menu-open={mostPopularMenuOpen} onmouseenter={measurePopularOverflow}>
<div class="popular-card-artwork">
{#if track.album?.image?.large || track.album?.image?.small}
@@ -1436,11 +1440,14 @@
<MoreHorizontal size={16} />
</button>
{#if mostPopularMenuOpen}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="popular-menu"
bind:this={popularMenuEl}
style={popularMenuStyle}
use:portal
role="menu"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
>
<button class="menu-item" onclick={() => { handleSearchTrackPlay(track, 0); mostPopularMenuOpen = false; }}>
+2 -7
View File
@@ -3997,8 +3997,8 @@
<!-- Auto-Theme failure modal -->
{#if autoThemeFailedModal}
<div class="auto-theme-modal-backdrop" role="presentation" onclick={dismissAutoThemeFailedModal}>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="auto-theme-modal" onclick={(e) => e.stopPropagation()}>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="auto-theme-modal" role="dialog" aria-modal="true" tabindex="-1" onclick={(e) => e.stopPropagation()}>
<h3>{$t('settings.appearance.autoThemeError')}</h3>
<p class="auto-theme-modal-message">{autoThemeFailedMessage}</p>
<p class="auto-theme-modal-hint">{$t('settings.appearance.autoThemeFailedHint')}</p>
@@ -5947,11 +5947,6 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz</pre>
color: var(--text-primary);
}
.collapsible-section .section-title-btn .experimental-badge {
flex-shrink: 0;
margin-left: -4px;
}
/* Composition subsection (inside Appearance) */
.composition-subsection {
padding-top: 16px;