feat(i18n): add translations to main UI components
- Sidebar: navigation labels, playlist section, sort options - SearchView: search placeholder, tabs, results messages - HomeView: section titles, loading states, empty states - QueuePanel: headers, search, empty state, actions - NowPlayingBar: player controls, tooltips, cast buttons - FocusMode: close button, lyrics loading - HomeSettingsModal: all settings labels and descriptions Added new translation keys to en.json and es.json locales.
This commit is contained in:
1 parent
5c869eeae9
commit
3cfe3c1333
9 files changed
+209
-102
No files matched your search
@@ -2,6 +2,7 @@
|
||||
import { SkipBack, Play, Pause, SkipForward, ChevronDown } from 'lucide-svelte';
|
||||
import LyricsLines from './lyrics/LyricsLines.svelte';
|
||||
import { startActiveLineUpdates, stopActiveLineUpdates } from '$lib/stores/lyricsStore';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
interface LyricsLine {
|
||||
text: string;
|
||||
@@ -135,7 +136,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Close Button -->
|
||||
<button class="close-btn" class:visible={showControls} onclick={onClose} title="Close (Esc)">
|
||||
<button class="close-btn" class:visible={showControls} onclick={onClose} title={$t('actions.close') + ' (Esc)'}>
|
||||
<ChevronDown size={28} />
|
||||
</button>
|
||||
|
||||
@@ -154,7 +155,7 @@
|
||||
{#if lyricsLoading}
|
||||
<div class="lyrics-state">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading lyrics...</span>
|
||||
<span>{$t('player.fetchingLyrics')}</span>
|
||||
</div>
|
||||
{:else if lyricsError}
|
||||
<div class="lyrics-state">
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
resetToDefaults,
|
||||
type HomeSettings
|
||||
} from '$lib/stores/homeSettingsStore';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
@@ -58,16 +59,16 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {isOpen} {onClose} title="Customize Home">
|
||||
<Modal {isOpen} {onClose} title={$t('home.customizeHome')}>
|
||||
{#snippet children()}
|
||||
<!-- Greeting Settings -->
|
||||
<div class="settings-section">
|
||||
<div class="section-title">Greeting</div>
|
||||
<div class="section-title">{$t('homeSettings.greeting')}</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Show greeting</span>
|
||||
<span class="setting-desc">Display personalized greeting at top</span>
|
||||
<span class="setting-label">{$t('homeSettings.showGreeting')}</span>
|
||||
<span class="setting-desc">{$t('homeSettings.showGreetingDesc')}</span>
|
||||
</div>
|
||||
<label class="toggle">
|
||||
<input
|
||||
@@ -82,13 +83,13 @@
|
||||
{#if settings.greeting.enabled}
|
||||
<div class="setting-row greeting-input-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Custom greeting</span>
|
||||
<span class="setting-desc">Use {'{name}'} for your name. Leave empty for time-based.</span>
|
||||
<span class="setting-label">{$t('homeSettings.customGreeting')}</span>
|
||||
<span class="setting-desc">{$t('homeSettings.customGreetingDesc')}</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
class="greeting-input"
|
||||
placeholder="Good vibes, {name}!"
|
||||
placeholder={$t('homeSettings.customGreetingPlaceholder')}
|
||||
bind:value={customGreetingInput}
|
||||
onblur={handleCustomGreetingChange}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleCustomGreetingChange()}
|
||||
@@ -99,8 +100,8 @@
|
||||
|
||||
<!-- Sections Order -->
|
||||
<div class="settings-section">
|
||||
<div class="section-title">Sections</div>
|
||||
<p class="section-desc">Reorder with arrows. Toggle visibility with switch.</p>
|
||||
<div class="section-title">{$t('homeSettings.sections')}</div>
|
||||
<p class="section-desc">{$t('homeSettings.sectionsDesc')}</p>
|
||||
|
||||
<div class="sections-list">
|
||||
{#each settings.sections as section, index (section.id)}
|
||||
@@ -110,7 +111,7 @@
|
||||
class="order-btn"
|
||||
onclick={() => handleMoveUp(section.id)}
|
||||
disabled={index === 0}
|
||||
title="Move up"
|
||||
title={$t('homeSettings.moveUp')}
|
||||
>
|
||||
<ChevronUp size={16} />
|
||||
</button>
|
||||
@@ -118,7 +119,7 @@
|
||||
class="order-btn"
|
||||
onclick={() => handleMoveDown(section.id)}
|
||||
disabled={index === settings.sections.length - 1}
|
||||
title="Move down"
|
||||
title={$t('homeSettings.moveDown')}
|
||||
>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
@@ -150,7 +151,7 @@
|
||||
{#snippet footer()}
|
||||
<button class="reset-btn" onclick={handleReset}>
|
||||
<RotateCcw size={14} />
|
||||
Reset to defaults
|
||||
{$t('homeSettings.resetToDefaults')}
|
||||
</button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
} from 'lucide-svelte';
|
||||
import QualityBadge from './QualityBadge.svelte';
|
||||
import AudioOutputBadges from './AudioOutputBadges.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
interface Props {
|
||||
artwork?: string;
|
||||
@@ -186,16 +187,16 @@
|
||||
class="control-btn"
|
||||
class:active={isShuffle}
|
||||
onclick={onToggleShuffle}
|
||||
title="Shuffle"
|
||||
title={$t('player.shuffle')}
|
||||
>
|
||||
<Shuffle size={16} />
|
||||
</button>
|
||||
|
||||
<button class="control-btn" onclick={onSkipBack} title="Previous">
|
||||
<button class="control-btn" onclick={onSkipBack} title={$t('player.previous')}>
|
||||
<SkipBack size={18} />
|
||||
</button>
|
||||
|
||||
<button class="control-btn play-btn" onclick={onTogglePlay} title={isPlaying ? 'Pause' : 'Play'}>
|
||||
<button class="control-btn play-btn" onclick={onTogglePlay} title={isPlaying ? $t('player.pause') : $t('player.play')}>
|
||||
{#if isPlaying}
|
||||
<Pause size={20} />
|
||||
{:else}
|
||||
@@ -203,7 +204,7 @@
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button class="control-btn" onclick={onSkipForward} title="Next">
|
||||
<button class="control-btn" onclick={onSkipForward} title={$t('player.next')}>
|
||||
<SkipForward size={18} />
|
||||
</button>
|
||||
|
||||
@@ -211,7 +212,7 @@
|
||||
class="control-btn"
|
||||
class:active={repeatMode !== 'off'}
|
||||
onclick={onToggleRepeat}
|
||||
title={repeatMode === 'off' ? 'Repeat' : repeatMode === 'all' ? 'Repeat All' : 'Repeat One'}
|
||||
title={repeatMode === 'off' ? $t('player.repeat') : repeatMode === 'all' ? $t('player.repeatAll') : $t('player.repeatOne')}
|
||||
>
|
||||
{#if repeatMode === 'one'}
|
||||
<Repeat1 size={16} />
|
||||
@@ -220,7 +221,7 @@
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button class="control-btn" title="Add to Playlist">
|
||||
<button class="control-btn" title={$t('actions.addToPlaylist')}>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
|
||||
@@ -228,7 +229,7 @@
|
||||
class="control-btn"
|
||||
class:active={isFavorite}
|
||||
onclick={onToggleFavorite}
|
||||
title={isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}
|
||||
title={isFavorite ? $t('actions.removeFromFavorites') : $t('actions.addToFavorites')}
|
||||
>
|
||||
<Heart size={16} fill={isFavorite ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
@@ -262,7 +263,7 @@
|
||||
<span class="song-title" title={trackTitle}>{trackTitle}</span>
|
||||
<div class="song-meta">
|
||||
{#if artist}
|
||||
<button class="meta-link" onclick={onArtistClick} title="Go to Artist">
|
||||
<button class="meta-link" onclick={onArtistClick} title={$t('actions.goToArtist')}>
|
||||
{artist}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -270,7 +271,7 @@
|
||||
<span class="meta-separator">·</span>
|
||||
{/if}
|
||||
{#if album}
|
||||
<button class="meta-link" onclick={onAlbumClick} title="Go to Album">
|
||||
<button class="meta-link" onclick={onAlbumClick} title={$t('actions.goToAlbum')}>
|
||||
{album}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -286,7 +287,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<span>No track playing</span>
|
||||
<span>{$t('player.noTrackPlaying')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -297,7 +298,7 @@
|
||||
class="control-btn"
|
||||
class:cast-active={isCastConnected}
|
||||
onclick={onCast}
|
||||
title={isCastConnected ? 'Casting - Click to manage' : 'Cast to device'}
|
||||
title={isCastConnected ? $t('player.castingManage') : $t('player.castToDevice')}
|
||||
>
|
||||
<Cast size={16} />
|
||||
</button>
|
||||
@@ -306,20 +307,20 @@
|
||||
class="control-btn"
|
||||
class:active={lyricsActive}
|
||||
onclick={onToggleLyrics}
|
||||
title="Lyrics"
|
||||
title={$t('player.lyrics')}
|
||||
>
|
||||
<Mic2 size={16} />
|
||||
</button>
|
||||
|
||||
<button class="control-btn" onclick={onOpenQueue} title="Queue">
|
||||
<button class="control-btn" onclick={onOpenQueue} title={$t('player.queue')}>
|
||||
<ListMusic size={16} />
|
||||
</button>
|
||||
|
||||
<button class="control-btn" onclick={onOpenMiniPlayer} title="Mini Player">
|
||||
<button class="control-btn" onclick={onOpenMiniPlayer} title={$t('player.miniPlayer')}>
|
||||
<PictureInPicture2 size={16} />
|
||||
</button>
|
||||
|
||||
<button class="control-btn" onclick={onOpenFullScreen} title="Full Screen">
|
||||
<button class="control-btn" onclick={onOpenFullScreen} title={$t('player.fullScreen')}>
|
||||
<Maximize2 size={16} />
|
||||
</button>
|
||||
|
||||
@@ -328,7 +329,7 @@
|
||||
<button
|
||||
class="control-btn volume-btn"
|
||||
onclick={() => onVolumeChange?.(volume === 0 ? 70 : 0)}
|
||||
title={volume === 0 ? 'Unmute' : 'Mute'}
|
||||
title={volume === 0 ? $t('player.unmute') : $t('player.mute')}
|
||||
>
|
||||
{#if volume === 0}
|
||||
<VolumeX size={16} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { X, GripVertical, Play, Search } from 'lucide-svelte';
|
||||
import GlassSurface from './glass/GlassSurface.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
interface QueueTrack {
|
||||
id: string;
|
||||
@@ -114,7 +115,7 @@
|
||||
<GlassSurface rootClassName="queue-panel" enableRipple={false} enableDistortion={false}>
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h2>Queue</h2>
|
||||
<h2>{$t('player.queue')}</h2>
|
||||
<button class="close-btn" onclick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
@@ -131,7 +132,7 @@
|
||||
<!-- Now Playing -->
|
||||
{#if currentTrack}
|
||||
<div class="section now-playing-section">
|
||||
<div class="section-header">Now Playing</div>
|
||||
<div class="section-header">{$t('player.nowPlaying')}</div>
|
||||
<div class="now-playing-card">
|
||||
<img src={currentTrack.artwork} alt={currentTrack.title} />
|
||||
<div class="track-info">
|
||||
@@ -148,7 +149,7 @@
|
||||
<Search size={14} class="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search in queue..."
|
||||
placeholder={$t('player.searchQueue')}
|
||||
bind:value={searchQuery}
|
||||
class="search-input"
|
||||
/>
|
||||
@@ -164,7 +165,7 @@
|
||||
{#if upcomingTracks.length > 0}
|
||||
<div class="section next-up-section">
|
||||
<div class="section-header">
|
||||
Next Up ({filteredTracks.length}{searchQuery ? ` / ${upcomingTracks.length}` : ''})
|
||||
{$t('player.upNext')} ({filteredTracks.length}{searchQuery ? ` / ${upcomingTracks.length}` : ''})
|
||||
</div>
|
||||
<div class="tracks">
|
||||
{#each filteredTracks as track, index}
|
||||
@@ -212,7 +213,7 @@
|
||||
</div>
|
||||
{/each}
|
||||
{#if searchQuery && filteredTracks.length === 0}
|
||||
<div class="no-results">No tracks match "{searchQuery}"</div>
|
||||
<div class="no-results">{$t('player.noTracksMatch', { values: { query: searchQuery } })}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,8 +223,8 @@
|
||||
{#if upcomingTracks.length === 0 && !currentTrack}
|
||||
<div class="empty-state">
|
||||
<div class="emoji">🎵</div>
|
||||
<div class="empty-title">Queue is empty</div>
|
||||
<div class="empty-text">Search for music or browse your library to add tracks.</div>
|
||||
<div class="empty-title">{$t('player.queueEmpty')}</div>
|
||||
<div class="empty-text">{$t('player.queueEmptyDescription')}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -231,8 +232,8 @@
|
||||
<!-- Footer Actions -->
|
||||
{#if upcomingTracks.length > 0 || currentTrack}
|
||||
<div class="footer">
|
||||
<button class="clear-btn" onclick={onClearQueue}>Clear Queue</button>
|
||||
<button class="save-btn" onclick={onSaveAsPlaylist}>Save as Playlist</button>
|
||||
<button class="clear-btn" onclick={onClearQueue}>{$t('player.clearQueue')}</button>
|
||||
<button class="save-btn" onclick={onSaveAsPlaylist}>{$t('player.saveQueue')}</button>
|
||||
</div>
|
||||
{/if}
|
||||
</GlassSurface>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import NavigationItem from './NavigationItem.svelte';
|
||||
import UserCard from './UserCard.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
interface Playlist {
|
||||
id: number;
|
||||
@@ -374,13 +375,13 @@
|
||||
}}
|
||||
>
|
||||
<Search class="search-icon" size={16} />
|
||||
<span class="search-placeholder">Search</span>
|
||||
<span class="search-placeholder">{$t('nav.search')}</span>
|
||||
</button>
|
||||
|
||||
<!-- Navigation Section -->
|
||||
<nav class="nav-section">
|
||||
<NavigationItem
|
||||
label="Home"
|
||||
label={$t('nav.home')}
|
||||
active={activeView === 'home'}
|
||||
onclick={() => handleViewChange('home')}
|
||||
>
|
||||
@@ -391,7 +392,7 @@
|
||||
<!-- Favorites Section (standalone) -->
|
||||
<nav class="nav-section">
|
||||
<NavigationItem
|
||||
label="Favorites"
|
||||
label={$t('nav.favorites')}
|
||||
active={activeView === 'favorites'}
|
||||
onclick={() => handleViewChange('favorites')}
|
||||
>
|
||||
@@ -402,20 +403,20 @@
|
||||
<!-- Playlists Section -->
|
||||
<div class="section playlists-section">
|
||||
<div class="playlists-header">
|
||||
<div class="section-header">Playlists</div>
|
||||
<div class="section-header">{$t('nav.playlists')}</div>
|
||||
<div class="header-actions" bind:this={menuRef}>
|
||||
<button class="icon-btn" onclick={onCreatePlaylist} title="New playlist">
|
||||
<button class="icon-btn" onclick={onCreatePlaylist} title={$t('playlist.createNew')}>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
<button
|
||||
class="icon-btn"
|
||||
bind:this={triggerRef}
|
||||
onclick={(e) => { e.stopPropagation(); toggleMenu(); }}
|
||||
title="Options"
|
||||
title={$t('actions.more')}
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
<button class="icon-btn" onclick={() => playlistsCollapsed = !playlistsCollapsed} title={playlistsCollapsed ? "Expand" : "Collapse"}>
|
||||
<button class="icon-btn" onclick={() => playlistsCollapsed = !playlistsCollapsed} title={playlistsCollapsed ? $t('actions.open') : $t('actions.close')}>
|
||||
{#if playlistsCollapsed}
|
||||
<ChevronDown size={14} />
|
||||
{:else}
|
||||
@@ -438,7 +439,7 @@
|
||||
onmouseleave={closeSubmenuDelayed}
|
||||
>
|
||||
<ArrowUpDown size={14} />
|
||||
<span class="menu-label">Sort by</span>
|
||||
<span class="menu-label">{$t('library.sortBy')}</span>
|
||||
<ChevronRight size={14} class="submenu-arrow" />
|
||||
</div>
|
||||
|
||||
@@ -452,38 +453,38 @@
|
||||
onmouseleave={closeSubmenuDelayed}
|
||||
>
|
||||
<button class="menu-item" class:selected={sortOption === 'name'} onclick={() => handleSortChange('name')}>
|
||||
Name (A-Z)
|
||||
{$t('sort.nameAZ')}
|
||||
</button>
|
||||
<button class="menu-item" class:selected={sortOption === 'recent'} onclick={() => handleSortChange('recent')}>
|
||||
Recent
|
||||
{$t('sort.recent')}
|
||||
</button>
|
||||
<button class="menu-item" class:selected={sortOption === 'tracks'} onclick={() => handleSortChange('tracks')}>
|
||||
# of Tracks
|
||||
{$t('sort.trackCount')}
|
||||
</button>
|
||||
<button class="menu-item" class:selected={sortOption === 'playcount'} onclick={() => handleSortChange('playcount')}>
|
||||
Play Count
|
||||
{$t('sort.playCount')}
|
||||
</button>
|
||||
<button class="menu-item" class:selected={sortOption === 'custom'} onclick={() => handleSortChange('custom')}>
|
||||
Custom
|
||||
{$t('sort.custom')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button class="menu-item" onclick={() => handleMenuAction(loadUserPlaylists)}>
|
||||
<RefreshCw size={14} />
|
||||
<span>Refresh</span>
|
||||
<span>{$t('actions.refresh')}</span>
|
||||
</button>
|
||||
|
||||
<button class="menu-item" onclick={() => handleMenuAction(onImportPlaylist ?? (() => {}))}>
|
||||
<Import size={14} />
|
||||
<span>Import</span>
|
||||
<span>{$t('playlist.import')}</span>
|
||||
</button>
|
||||
|
||||
<div class="menu-divider"></div>
|
||||
|
||||
<button class="menu-item" onclick={() => handleMenuAction(onPlaylistManagerClick ?? (() => {}))}>
|
||||
<Settings size={14} />
|
||||
<span>Manage playlists</span>
|
||||
<span>{$t('playlist.manage')}</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -491,7 +492,7 @@
|
||||
{#if !playlistsCollapsed}
|
||||
<div class="playlists-scroll">
|
||||
{#if playlistsLoading}
|
||||
<div class="playlists-loading">Loading...</div>
|
||||
<div class="playlists-loading">{$t('actions.loading')}</div>
|
||||
{:else if visiblePlaylists.length > 0}
|
||||
<nav class="playlists-nav">
|
||||
{#each visiblePlaylists as playlist (playlist.id)}
|
||||
@@ -507,9 +508,9 @@
|
||||
{/each}
|
||||
</nav>
|
||||
{:else if userPlaylists.length > 0}
|
||||
<div class="no-playlists">All playlists are hidden</div>
|
||||
<div class="no-playlists">{$t('playlist.allHidden')}</div>
|
||||
{:else}
|
||||
<div class="no-playlists">No playlists yet</div>
|
||||
<div class="no-playlists">{$t('empty.noPlaylists')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -518,7 +519,7 @@
|
||||
<!-- Local Library Section -->
|
||||
<div class="section local-library-section">
|
||||
<button class="section-header-btn" onclick={() => localLibraryCollapsed = !localLibraryCollapsed}>
|
||||
<span class="section-header">Local Library</span>
|
||||
<span class="section-header">{$t('library.title')}</span>
|
||||
{#if localLibraryCollapsed}
|
||||
<ChevronDown size={12} />
|
||||
{:else}
|
||||
@@ -527,7 +528,7 @@
|
||||
</button>
|
||||
{#if !localLibraryCollapsed}
|
||||
<NavigationItem
|
||||
label="Browse Library"
|
||||
label={$t('library.browse')}
|
||||
active={activeView === 'library'}
|
||||
onclick={() => handleViewChange('library')}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Music, User, Loader2 } from 'lucide-svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import HorizontalScrollRow from '../HorizontalScrollRow.svelte';
|
||||
import AlbumCard from '../AlbumCard.svelte';
|
||||
import TrackRow from '../TrackRow.svelte';
|
||||
@@ -456,8 +457,8 @@
|
||||
<div class="loading-icon">
|
||||
<Loader2 size={36} class="spinner" />
|
||||
</div>
|
||||
<h2>Loading your home</h2>
|
||||
<p>Building recommendations from your listening history.</p>
|
||||
<h2>{$t('home.loading')}</h2>
|
||||
<p>{$t('home.loadingDescription')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -469,7 +470,7 @@
|
||||
{:else}
|
||||
<div></div>
|
||||
{/if}
|
||||
<button class="settings-btn" onclick={() => isSettingsModalOpen = true} title="Customize Home">
|
||||
<button class="settings-btn" onclick={() => isSettingsModalOpen = true} title={$t('home.customizeHome')}>
|
||||
<img src="/home-gear.svg" alt="Settings" class="settings-icon" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -479,22 +480,22 @@
|
||||
<div class="state-icon loading">
|
||||
<Loader2 size={36} class="spinner" />
|
||||
</div>
|
||||
<h1>Loading your home</h1>
|
||||
<p>Building recommendations from your listening history.</p>
|
||||
<h1>{$t('home.loading')}</h1>
|
||||
<p>{$t('home.loadingDescription')}</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="home-state">
|
||||
<div class="state-icon">
|
||||
<Music size={36} />
|
||||
</div>
|
||||
<h1>Could not load Home</h1>
|
||||
<h1>{$t('home.loadError')}</h1>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{:else if hasContent}
|
||||
<!-- Render sections in user-defined order -->
|
||||
{#each visibleSections as sectionId (sectionId)}
|
||||
{#if sectionId === 'newReleases' && newReleases.length > 0}
|
||||
<HorizontalScrollRow title="New Releases">
|
||||
<HorizontalScrollRow title={$t('home.newReleases')}>
|
||||
{#snippet children()}
|
||||
{#each newReleases as album}
|
||||
<AlbumCard
|
||||
@@ -518,7 +519,7 @@
|
||||
{/if}
|
||||
|
||||
{#if sectionId === 'pressAwards' && pressAwards.length > 0}
|
||||
<HorizontalScrollRow title="Press Awards">
|
||||
<HorizontalScrollRow title={$t('home.pressAwards')}>
|
||||
{#snippet children()}
|
||||
{#each pressAwards as album}
|
||||
<AlbumCard
|
||||
@@ -542,7 +543,7 @@
|
||||
{/if}
|
||||
|
||||
{#if sectionId === 'mostStreamed' && mostStreamed.length > 0}
|
||||
<HorizontalScrollRow title="Popular Albums">
|
||||
<HorizontalScrollRow title={$t('home.popularAlbums')}>
|
||||
{#snippet children()}
|
||||
{#each mostStreamed as album}
|
||||
<AlbumCard
|
||||
@@ -566,7 +567,7 @@
|
||||
{/if}
|
||||
|
||||
{#if sectionId === 'qobuzissimes' && qobuzissimes.length > 0}
|
||||
<HorizontalScrollRow title="Qobuzissimes">
|
||||
<HorizontalScrollRow title={$t('home.qobuzissimes')}>
|
||||
{#snippet children()}
|
||||
{#each qobuzissimes as album}
|
||||
<AlbumCard
|
||||
@@ -590,7 +591,7 @@
|
||||
{/if}
|
||||
|
||||
{#if sectionId === 'editorPicks' && editorPicks.length > 0}
|
||||
<HorizontalScrollRow title="Editor's Picks">
|
||||
<HorizontalScrollRow title={$t('home.editorPicks')}>
|
||||
{#snippet children()}
|
||||
{#each editorPicks as album}
|
||||
<AlbumCard
|
||||
@@ -614,7 +615,7 @@
|
||||
{/if}
|
||||
|
||||
{#if sectionId === 'recentAlbums' && recentAlbums.length > 0}
|
||||
<HorizontalScrollRow title="Recently Played">
|
||||
<HorizontalScrollRow title={$t('home.recentlyPlayed')}>
|
||||
{#snippet children()}
|
||||
{#each recentAlbums as album}
|
||||
<AlbumCard
|
||||
@@ -640,7 +641,7 @@
|
||||
{#if sectionId === 'continueTracks' && continueTracks.length > 0}
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2>Continue Listening</h2>
|
||||
<h2>{$t('home.continueListening')}</h2>
|
||||
</div>
|
||||
<div class="track-list compact">
|
||||
{#each continueTracks as track, index}
|
||||
@@ -667,7 +668,7 @@
|
||||
{/if}
|
||||
|
||||
{#if sectionId === 'topArtists' && topArtists.length > 0}
|
||||
<HorizontalScrollRow title="Your Top Artists">
|
||||
<HorizontalScrollRow title={$t('home.yourTopArtists')}>
|
||||
{#snippet children()}
|
||||
{#each topArtists as artist}
|
||||
<button class="artist-card" onclick={() => onArtistClick?.(artist.id)}>
|
||||
@@ -685,7 +686,7 @@
|
||||
{/if}
|
||||
<div class="artist-name">{artist.name}</div>
|
||||
{#if artist.playCount}
|
||||
<div class="artist-meta">{artist.playCount} plays</div>
|
||||
<div class="artist-meta">{$t('home.artistPlays', { values: { count: artist.playCount } })}</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
@@ -695,7 +696,7 @@
|
||||
{/if}
|
||||
|
||||
{#if sectionId === 'favoriteAlbums' && favoriteAlbums.length > 0}
|
||||
<HorizontalScrollRow title="More From Favorites">
|
||||
<HorizontalScrollRow title={$t('home.moreFromFavorites')}>
|
||||
{#snippet children()}
|
||||
{#each favoriteAlbums as album}
|
||||
<AlbumCard
|
||||
@@ -723,8 +724,8 @@
|
||||
<div class="state-icon">
|
||||
<Music size={48} />
|
||||
</div>
|
||||
<h1>Start Listening</h1>
|
||||
<p>Play music or add favorites to activate recommendations.</p>
|
||||
<h1>{$t('home.startListening')}</h1>
|
||||
<p>{$t('home.startListeningDescription')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import AlbumCard from '../AlbumCard.svelte';
|
||||
import TrackMenu from '../TrackMenu.svelte';
|
||||
import { getSearchState, setSearchState, type SearchResults, type SearchTab } from '$lib/stores/searchState';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
let searchInput: HTMLInputElement | null = null;
|
||||
|
||||
@@ -278,19 +279,19 @@
|
||||
<div class="search-view">
|
||||
<!-- Search Header -->
|
||||
<div class="search-header">
|
||||
<h1>Search</h1>
|
||||
<h1>{$t('search.title')}</h1>
|
||||
<div class="search-input-container">
|
||||
<Search size={20} class="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for albums, tracks, or artists..."
|
||||
placeholder={$t('search.placeholder')}
|
||||
bind:value={query}
|
||||
oninput={debounceSearch}
|
||||
class="search-input"
|
||||
bind:this={searchInput}
|
||||
/>
|
||||
{#if query.trim()}
|
||||
<button class="search-clear" onclick={clearSearch} aria-label="Clear search">
|
||||
<button class="search-clear" onclick={clearSearch} aria-label={$t('actions.clear')}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
@@ -305,7 +306,7 @@
|
||||
onclick={() => handleTabChange('albums')}
|
||||
>
|
||||
<Disc3 size={18} />
|
||||
<span>Albums</span>
|
||||
<span>{$t('search.albums')}</span>
|
||||
{#if albumResults}
|
||||
<span class="count">{albumResults.total}</span>
|
||||
{/if}
|
||||
@@ -316,7 +317,7 @@
|
||||
onclick={() => handleTabChange('tracks')}
|
||||
>
|
||||
<Music size={18} />
|
||||
<span>Tracks</span>
|
||||
<span>{$t('search.tracks')}</span>
|
||||
{#if trackResults}
|
||||
<span class="count">{trackResults.total}</span>
|
||||
{/if}
|
||||
@@ -327,7 +328,7 @@
|
||||
onclick={() => handleTabChange('artists')}
|
||||
>
|
||||
<Mic2 size={18} />
|
||||
<span>Artists</span>
|
||||
<span>{$t('search.artists')}</span>
|
||||
{#if artistResults}
|
||||
<span class="count">{artistResults.total}</span>
|
||||
{/if}
|
||||
@@ -339,21 +340,21 @@
|
||||
{#if isSearching}
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Searching...</p>
|
||||
<p>{$t('search.searching')}</p>
|
||||
</div>
|
||||
{:else if searchError}
|
||||
<div class="error">
|
||||
<p>Search failed</p>
|
||||
<p>{$t('errors.loadFailed')}</p>
|
||||
<p class="error-detail">{searchError}</p>
|
||||
</div>
|
||||
{:else if !query.trim()}
|
||||
<div class="empty-state">
|
||||
<Search size={48} />
|
||||
<p>Start typing to search Qobuz</p>
|
||||
<p>{$t('search.startTyping')}</p>
|
||||
</div>
|
||||
{:else if activeTab === 'albums' && albumResults}
|
||||
{#if albumResults.items.length === 0}
|
||||
<div class="no-results">No albums found for "{query}"</div>
|
||||
<div class="no-results">{$t('search.noAlbumsFor', { values: { query } })}</div>
|
||||
{:else}
|
||||
<div class="albums-grid">
|
||||
{#each albumResults.items as album}
|
||||
@@ -376,14 +377,14 @@
|
||||
{#if hasMoreAlbums}
|
||||
<div class="load-more-container">
|
||||
<button class="load-more-btn" onclick={loadMore} disabled={isLoadingMore}>
|
||||
{isLoadingMore ? 'Loading...' : `Load More (${albumResults.items.length} of ${albumResults.total})`}
|
||||
{isLoadingMore ? $t('actions.loading') : $t('artist.loadMore') + ` (${albumResults.items.length} / ${albumResults.total})`}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if activeTab === 'tracks' && trackResults}
|
||||
{#if trackResults.items.length === 0}
|
||||
<div class="no-results">No tracks found for "{query}"</div>
|
||||
<div class="no-results">{$t('search.noTracksFor', { values: { query } })}</div>
|
||||
{:else}
|
||||
<div class="tracks-list">
|
||||
{#each trackResults.items as track, index}
|
||||
@@ -440,14 +441,14 @@
|
||||
{#if hasMoreTracks}
|
||||
<div class="load-more-container">
|
||||
<button class="load-more-btn" onclick={loadMore} disabled={isLoadingMore}>
|
||||
{isLoadingMore ? 'Loading...' : `Load More (${trackResults.items.length} of ${trackResults.total})`}
|
||||
{isLoadingMore ? $t('actions.loading') : $t('artist.loadMore') + ` (${trackResults.items.length} / ${trackResults.total})`}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if activeTab === 'artists' && artistResults}
|
||||
{#if artistResults.items.length === 0}
|
||||
<div class="no-results">No artists found for "{query}"</div>
|
||||
<div class="no-results">{$t('search.noArtistsFor', { values: { query } })}</div>
|
||||
{:else}
|
||||
<div class="artists-grid">
|
||||
{#each artistResults.items as artist}
|
||||
@@ -461,7 +462,7 @@
|
||||
{/if}
|
||||
<div class="artist-name">{artist.name}</div>
|
||||
{#if artist.albums_count}
|
||||
<div class="artist-albums">{artist.albums_count} albums</div>
|
||||
<div class="artist-albums">{$t('library.albumCount', { values: { count: artist.albums_count } })}</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
@@ -469,7 +470,7 @@
|
||||
{#if hasMoreArtists}
|
||||
<div class="load-more-container">
|
||||
<button class="load-more-btn" onclick={loadMore} disabled={isLoadingMore}>
|
||||
{isLoadingMore ? 'Loading...' : `Load More (${artistResults.items.length} of ${artistResults.total})`}
|
||||
{isLoadingMore ? $t('actions.loading') : $t('artist.loadMore') + ` (${artistResults.items.length} / ${artistResults.total})`}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -28,7 +28,20 @@
|
||||
"madeForYou": "Made for You",
|
||||
"topPicks": "Top Picks",
|
||||
"yourPlaylists": "Your Playlists",
|
||||
"seeAll": "See All"
|
||||
"seeAll": "See All",
|
||||
"loading": "Loading your home",
|
||||
"loadingDescription": "Building recommendations from your listening history.",
|
||||
"customizeHome": "Customize Home",
|
||||
"loadError": "Could not load Home",
|
||||
"popularAlbums": "Popular Albums",
|
||||
"qobuzissimes": "Qobuzissimes",
|
||||
"editorPicks": "Editor's Picks",
|
||||
"continueListening": "Continue Listening",
|
||||
"yourTopArtists": "Your Top Artists",
|
||||
"artistPlays": "{count} plays",
|
||||
"moreFromFavorites": "More From Favorites",
|
||||
"startListening": "Start Listening",
|
||||
"startListeningDescription": "Play music or add favorites to activate recommendations."
|
||||
},
|
||||
"search": {
|
||||
"title": "Search",
|
||||
@@ -40,10 +53,15 @@
|
||||
"noResults": "No results found",
|
||||
"searching": "Searching...",
|
||||
"recentSearches": "Recent Searches",
|
||||
"clearHistory": "Clear History"
|
||||
"clearHistory": "Clear History",
|
||||
"startTyping": "Start typing to search Qobuz",
|
||||
"noAlbumsFor": "No albums found for \"{query}\"",
|
||||
"noTracksFor": "No tracks found for \"{query}\"",
|
||||
"noArtistsFor": "No artists found for \"{query}\""
|
||||
},
|
||||
"library": {
|
||||
"title": "Local Library",
|
||||
"browse": "Browse Library",
|
||||
"albums": "Albums",
|
||||
"artists": "Artists",
|
||||
"tracks": "Tracks",
|
||||
@@ -127,7 +145,17 @@
|
||||
"playlistName": "Playlist Name",
|
||||
"description": "Description",
|
||||
"public": "Public",
|
||||
"private": "Private"
|
||||
"private": "Private",
|
||||
"import": "Import",
|
||||
"manage": "Manage playlists",
|
||||
"allHidden": "All playlists are hidden"
|
||||
},
|
||||
"sort": {
|
||||
"nameAZ": "Name (A-Z)",
|
||||
"recent": "Recent",
|
||||
"trackCount": "# of Tracks",
|
||||
"playCount": "Play Count",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favorites",
|
||||
@@ -148,6 +176,7 @@
|
||||
"fetchingLyrics": "Fetching lyrics...",
|
||||
"shuffle": "Shuffle",
|
||||
"repeat": "Repeat",
|
||||
"repeatAll": "Repeat All",
|
||||
"repeatOne": "Repeat One",
|
||||
"repeatOff": "Repeat Off",
|
||||
"previous": "Previous",
|
||||
@@ -159,10 +188,18 @@
|
||||
"unmute": "Unmute",
|
||||
"clearQueue": "Clear Queue",
|
||||
"queueEmpty": "Queue is empty",
|
||||
"queueEmptyDescription": "Search for music or browse your library to add tracks.",
|
||||
"playNext": "Play Next",
|
||||
"addToQueue": "Add to Queue",
|
||||
"removeFromQueue": "Remove from Queue",
|
||||
"saveQueue": "Save as Playlist"
|
||||
"saveQueue": "Save as Playlist",
|
||||
"searchQueue": "Search in queue...",
|
||||
"noTracksMatch": "No tracks match \"{query}\"",
|
||||
"noTrackPlaying": "No track playing",
|
||||
"castToDevice": "Cast to device",
|
||||
"castingManage": "Casting - Click to manage",
|
||||
"miniPlayer": "Mini Player",
|
||||
"fullScreen": "Full Screen"
|
||||
},
|
||||
"quality": {
|
||||
"mp3": "MP3 320kbps",
|
||||
@@ -370,5 +407,18 @@
|
||||
"clearQueue": "Clear all tracks from queue?",
|
||||
"clearCache": "Clear all cached data?",
|
||||
"logout": "Are you sure you want to log out?"
|
||||
},
|
||||
"homeSettings": {
|
||||
"greeting": "Greeting",
|
||||
"showGreeting": "Show greeting",
|
||||
"showGreetingDesc": "Display personalized greeting at top",
|
||||
"customGreeting": "Custom greeting",
|
||||
"customGreetingDesc": "Use {name} for your name. Leave empty for time-based.",
|
||||
"customGreetingPlaceholder": "Good vibes, {name}!",
|
||||
"sections": "Sections",
|
||||
"sectionsDesc": "Reorder with arrows. Toggle visibility with switch.",
|
||||
"moveUp": "Move up",
|
||||
"moveDown": "Move down",
|
||||
"resetToDefaults": "Reset to defaults"
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,20 @@
|
||||
"madeForYou": "Hecho para Ti",
|
||||
"topPicks": "Selecciones Destacadas",
|
||||
"yourPlaylists": "Tus Listas",
|
||||
"seeAll": "Ver Todo"
|
||||
"seeAll": "Ver Todo",
|
||||
"loading": "Cargando tu inicio",
|
||||
"loadingDescription": "Construyendo recomendaciones de tu historial de escucha.",
|
||||
"customizeHome": "Personalizar Inicio",
|
||||
"loadError": "No se pudo cargar el Inicio",
|
||||
"popularAlbums": "Álbumes Populares",
|
||||
"qobuzissimes": "Qobuzissimes",
|
||||
"editorPicks": "Selección del Editor",
|
||||
"continueListening": "Continuar Escuchando",
|
||||
"yourTopArtists": "Tus Artistas Favoritos",
|
||||
"artistPlays": "{count} reproducciones",
|
||||
"moreFromFavorites": "Más de Favoritos",
|
||||
"startListening": "Empieza a Escuchar",
|
||||
"startListeningDescription": "Reproduce música o agrega favoritos para activar recomendaciones."
|
||||
},
|
||||
"search": {
|
||||
"title": "Buscar",
|
||||
@@ -40,10 +53,15 @@
|
||||
"noResults": "Sin resultados",
|
||||
"searching": "Buscando...",
|
||||
"recentSearches": "Búsquedas Recientes",
|
||||
"clearHistory": "Limpiar Historial"
|
||||
"clearHistory": "Limpiar Historial",
|
||||
"startTyping": "Escribe para buscar en Qobuz",
|
||||
"noAlbumsFor": "No se encontraron álbumes para \"{query}\"",
|
||||
"noTracksFor": "No se encontraron pistas para \"{query}\"",
|
||||
"noArtistsFor": "No se encontraron artistas para \"{query}\""
|
||||
},
|
||||
"library": {
|
||||
"title": "Biblioteca Local",
|
||||
"browse": "Explorar Biblioteca",
|
||||
"albums": "Álbumes",
|
||||
"artists": "Artistas",
|
||||
"tracks": "Pistas",
|
||||
@@ -127,7 +145,17 @@
|
||||
"playlistName": "Nombre de la Lista",
|
||||
"description": "Descripción",
|
||||
"public": "Pública",
|
||||
"private": "Privada"
|
||||
"private": "Privada",
|
||||
"import": "Importar",
|
||||
"manage": "Administrar listas",
|
||||
"allHidden": "Todas las listas están ocultas"
|
||||
},
|
||||
"sort": {
|
||||
"nameAZ": "Nombre (A-Z)",
|
||||
"recent": "Reciente",
|
||||
"trackCount": "# de Pistas",
|
||||
"playCount": "Reproducciones",
|
||||
"custom": "Personalizado"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favoritos",
|
||||
@@ -148,6 +176,7 @@
|
||||
"fetchingLyrics": "Buscando letras...",
|
||||
"shuffle": "Aleatorio",
|
||||
"repeat": "Repetir",
|
||||
"repeatAll": "Repetir Todo",
|
||||
"repeatOne": "Repetir Una",
|
||||
"repeatOff": "No Repetir",
|
||||
"previous": "Anterior",
|
||||
@@ -159,10 +188,18 @@
|
||||
"unmute": "Activar Sonido",
|
||||
"clearQueue": "Limpiar Cola",
|
||||
"queueEmpty": "La cola está vacía",
|
||||
"queueEmptyDescription": "Busca música o explora tu biblioteca para agregar pistas.",
|
||||
"playNext": "Reproducir Siguiente",
|
||||
"addToQueue": "Agregar a la Cola",
|
||||
"removeFromQueue": "Quitar de la Cola",
|
||||
"saveQueue": "Guardar como Lista"
|
||||
"saveQueue": "Guardar como Lista",
|
||||
"searchQueue": "Buscar en la cola...",
|
||||
"noTracksMatch": "Ninguna pista coincide con \"{query}\"",
|
||||
"noTrackPlaying": "Sin pista en reproducción",
|
||||
"castToDevice": "Transmitir a dispositivo",
|
||||
"castingManage": "Transmitiendo - Clic para administrar",
|
||||
"miniPlayer": "Mini Reproductor",
|
||||
"fullScreen": "Pantalla Completa"
|
||||
},
|
||||
"quality": {
|
||||
"mp3": "MP3 320kbps",
|
||||
@@ -370,5 +407,18 @@
|
||||
"clearQueue": "¿Limpiar todas las pistas de la cola?",
|
||||
"clearCache": "¿Limpiar todos los datos en caché?",
|
||||
"logout": "¿Estás seguro de que quieres cerrar sesión?"
|
||||
},
|
||||
"homeSettings": {
|
||||
"greeting": "Saludo",
|
||||
"showGreeting": "Mostrar saludo",
|
||||
"showGreetingDesc": "Mostrar saludo personalizado arriba",
|
||||
"customGreeting": "Saludo personalizado",
|
||||
"customGreetingDesc": "Usa {name} para tu nombre. Deja vacío para basado en hora.",
|
||||
"customGreetingPlaceholder": "¡Buenas vibras, {name}!",
|
||||
"sections": "Secciones",
|
||||
"sectionsDesc": "Reordena con flechas. Activa/desactiva con interruptor.",
|
||||
"moveUp": "Subir",
|
||||
"moveDown": "Bajar",
|
||||
"resetToDefaults": "Restablecer valores"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user