merge: fix/general-debug-1.1.7 into pre-release (resolve i18n conflicts)
This commit is contained in:
commit
ca11c43a4b
12 files changed
+625
-185
No files matched your search
@@ -47,8 +47,8 @@
|
||||
let accessible = $state(true);
|
||||
let checkingAccessibility = $state(false);
|
||||
|
||||
// Network filesystem options
|
||||
const networkFsOptions = [
|
||||
// Network filesystem options (derived to support i18n reactivity)
|
||||
const networkFsOptions = $derived([
|
||||
{ value: '', label: $t('library.networkFs.auto') },
|
||||
{ value: 'cifs', label: 'SMB/CIFS (Samba)' },
|
||||
{ value: 'nfs', label: 'NFS' },
|
||||
@@ -58,7 +58,7 @@
|
||||
{ value: 'glusterfs', label: 'GlusterFS' },
|
||||
{ value: 'ceph', label: 'CephFS' },
|
||||
{ value: 'other', label: $t('library.networkFs.other') }
|
||||
];
|
||||
]);
|
||||
|
||||
// Reset form when modal opens
|
||||
$effect(() => {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { ask } from '@tauri-apps/plugin-dialog';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { showToast } from '$lib/stores/toastStore';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
interface LocalTrack {
|
||||
id: number;
|
||||
@@ -153,7 +154,7 @@
|
||||
// Remote metadata functions
|
||||
async function searchRemoteMetadata() {
|
||||
if (!albumTitle.trim() && !albumArtist.trim()) {
|
||||
showToast('Enter album title or artist to search', 'error');
|
||||
showToast($t('toast.enterSearchTerms'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -173,7 +174,7 @@
|
||||
showRemotePanel = results.length > 0;
|
||||
} catch (err) {
|
||||
console.error('Remote search failed:', err);
|
||||
showToast(`Search failed: ${err}`, 'error');
|
||||
showToast($t('toast.searchFailed', { values: { error: String(err) } }), 'error');
|
||||
hasSearched = true;
|
||||
} finally {
|
||||
remoteSearching = false;
|
||||
@@ -224,7 +225,7 @@
|
||||
'warning'
|
||||
);
|
||||
} else {
|
||||
showToast(`Applied metadata from ${providerName}`, 'success');
|
||||
showToast($t('toast.appliedMetadata', { values: { provider: providerName } }), 'success');
|
||||
}
|
||||
showRemotePanel = false;
|
||||
} catch (err) {
|
||||
@@ -232,9 +233,9 @@
|
||||
// Check for rate limiting
|
||||
const errStr = String(err);
|
||||
if (errStr.includes('429') || errStr.toLowerCase().includes('rate')) {
|
||||
showToast('Rate limited. Please wait a moment and try again.', 'warning');
|
||||
showToast($t('toast.rateLimited'), 'warning');
|
||||
} else {
|
||||
showToast(`Failed to fetch metadata: ${err}`, 'error');
|
||||
showToast($t('toast.failedFetchMetadata', { values: { error: String(err) } }), 'error');
|
||||
}
|
||||
} finally {
|
||||
remoteLoading = false;
|
||||
@@ -256,7 +257,7 @@
|
||||
await openUrl(url);
|
||||
} catch (err) {
|
||||
console.error('Failed to open URL:', err);
|
||||
showToast('Failed to open browser', 'error');
|
||||
showToast($t('toast.failedOpenBrowser'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +361,7 @@
|
||||
});
|
||||
await invoke('library_write_album_metadata_to_files', { request: payload });
|
||||
}
|
||||
showToast('Album metadata saved', 'success');
|
||||
showToast($t('toast.albumMetadataSaved'), 'success');
|
||||
await onSaved();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
|
||||
@@ -155,12 +155,12 @@
|
||||
|
||||
onSuccess?.(result);
|
||||
if (result.matched_tracks > 0) {
|
||||
showToast('Playlist imported', 'success');
|
||||
showToast($t('toast.playlistImported'), 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
error = String(err);
|
||||
pushLog(`Import failed: ${error}`, 'error');
|
||||
showToast('Playlist import failed', 'error');
|
||||
showToast($t('toast.playlistImportFailed'), 'error');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { logPlaylistAdd } from '$lib/services/recoService';
|
||||
import { subscribe as subscribeOffline, getStatus, createPendingPlaylist } from '$lib/stores/offlineStore';
|
||||
import { showToast } from '$lib/stores/toastStore';
|
||||
import { t } from '$lib/i18n';
|
||||
import {
|
||||
subscribe as subscribeFolders,
|
||||
getVisibleFolders,
|
||||
@@ -241,7 +242,7 @@
|
||||
[], // No Qobuz tracks for empty playlist
|
||||
[] // No local tracks for empty playlist
|
||||
);
|
||||
showToast('Playlist created offline - will sync when back online', 'info');
|
||||
showToast($t('toast.playlistCreatedOffline'), 'info');
|
||||
// Create a temporary playlist object for UI
|
||||
const tempPlaylist: Playlist = {
|
||||
id: -pendingId, // Negative ID to distinguish from real playlists
|
||||
@@ -429,7 +430,7 @@
|
||||
qobuzTrackIds,
|
||||
localTrackPaths
|
||||
);
|
||||
showToast(`Playlist "${name.trim()}" created offline - will sync when back online`, 'info');
|
||||
showToast($t('toast.playlistCreatedOffline'), 'info');
|
||||
|
||||
// Create a temporary playlist object for UI
|
||||
const tempPlaylist: Playlist = {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { ArrowLeft, Search, X, Trash2, Ban, ToggleLeft, ToggleRight, AlertCircle } from 'lucide-svelte';
|
||||
import ViewTransition from '../ViewTransition.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import { showToast } from '$lib/stores/toastStore';
|
||||
import {
|
||||
subscribe,
|
||||
@@ -108,17 +109,17 @@
|
||||
<div class="header">
|
||||
<button class="back-btn" onclick={onBack}>
|
||||
<ArrowLeft size={16} />
|
||||
<span>Back</span>
|
||||
<span>{$t('actions.back')}</span>
|
||||
</button>
|
||||
<div class="title-section">
|
||||
<Ban size={24} class="title-icon" />
|
||||
<h1>Artist Blacklist</h1>
|
||||
<h1>{$t('settings.blacklist.title')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="description">
|
||||
Blacklisted artists are hidden from search results, radio, suggestions, and similar artists.
|
||||
{$t('settings.blacklist.description')}
|
||||
</p>
|
||||
|
||||
<!-- Controls -->
|
||||
@@ -131,10 +132,10 @@
|
||||
>
|
||||
{#if enabled}
|
||||
<ToggleRight size={20} />
|
||||
<span>Enabled</span>
|
||||
<span>{$t('settings.blacklist.enabled')}</span>
|
||||
{:else}
|
||||
<ToggleLeft size={20} />
|
||||
<span>Disabled</span>
|
||||
<span>{$t('settings.blacklist.disabled')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -143,7 +144,7 @@
|
||||
<Search size={16} class="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search blacklisted artists..."
|
||||
placeholder={$t('settings.blacklist.searchPlaceholder')}
|
||||
bind:value={searchQuery}
|
||||
class="search-input"
|
||||
/>
|
||||
@@ -161,18 +162,18 @@
|
||||
onclick={() => confirmClearOpen = true}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
<span>Clear All</span>
|
||||
<span>{$t('settings.blacklist.clearAll')}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<span class="count">{getCount()} artists</span>
|
||||
<span class="count">{$t('settings.blacklist.artistCount', { values: { count: getCount() } })}</span>
|
||||
</div>
|
||||
|
||||
<!-- Disabled Warning -->
|
||||
{#if !enabled}
|
||||
<div class="warning-banner">
|
||||
<AlertCircle size={16} />
|
||||
<span>Blacklist is currently disabled. Artists below will appear in results.</span>
|
||||
<span>{$t('settings.blacklist.disabledWarning')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -180,18 +181,18 @@
|
||||
{#if loading}
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading blacklist...</p>
|
||||
<p>{$t('settings.blacklist.loadingBlacklist')}</p>
|
||||
</div>
|
||||
{:else if artists.length === 0}
|
||||
<div class="empty">
|
||||
<Ban size={48} class="empty-icon" />
|
||||
<p>No blacklisted artists</p>
|
||||
<span class="empty-hint">To blacklist an artist, go to their page and click the ban icon.</span>
|
||||
<p>{$t('settings.blacklist.noArtists')}</p>
|
||||
<span class="empty-hint">{$t('settings.blacklist.noArtistsHint')}</span>
|
||||
</div>
|
||||
{:else if filteredArtists.length === 0}
|
||||
<div class="empty">
|
||||
<Search size={48} class="empty-icon" />
|
||||
<p>No results for "{searchQuery}"</p>
|
||||
<p>{$t('settings.blacklist.noResults', { values: { query: searchQuery } })}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="list">
|
||||
@@ -209,7 +210,7 @@
|
||||
</div>
|
||||
<div class="artist-details">
|
||||
<span class="artist-name">{artist.artist_name}</span>
|
||||
<span class="artist-meta">Added {formatDate(artist.added_at)}</span>
|
||||
<span class="artist-meta">{$t('settings.blacklist.addedOn', { values: { date: formatDate(artist.added_at) } })}</span>
|
||||
{#if artist.notes}
|
||||
<span class="artist-notes">{artist.notes}</span>
|
||||
{/if}
|
||||
@@ -218,7 +219,7 @@
|
||||
<button
|
||||
class="remove-btn"
|
||||
onclick={() => handleRemove(artist.artist_id)}
|
||||
title="Remove from blacklist"
|
||||
title={$t('settings.blacklist.remove')}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -244,16 +245,16 @@
|
||||
aria-modal="true"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 class="modal-title">Clear Blacklist?</h2>
|
||||
<h2 class="modal-title">{$t('settings.blacklist.confirmClearTitle')}</h2>
|
||||
<p class="modal-text">
|
||||
This will remove all {artists.length} artists from your blacklist. This action cannot be undone.
|
||||
{$t('settings.blacklist.confirmClearText', { values: { count: artists.length } })}
|
||||
</p>
|
||||
<div class="modal-actions">
|
||||
<button class="modal-btn cancel" onclick={() => confirmClearOpen = false}>
|
||||
Cancel
|
||||
{$t('actions.cancel')}
|
||||
</button>
|
||||
<button class="modal-btn danger" onclick={handleClearAll}>
|
||||
Clear All
|
||||
{$t('settings.blacklist.clearAll')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import {
|
||||
subscribe as subscribeHomeSettings,
|
||||
getSettings,
|
||||
getGreetingText,
|
||||
getGreetingInfo,
|
||||
type HomeSettings,
|
||||
type HomeSectionId
|
||||
} from '$lib/stores/homeSettingsStore';
|
||||
@@ -128,8 +128,14 @@
|
||||
let homeSettings = $state<HomeSettings>(getSettings());
|
||||
let isSettingsModalOpen = $state(false);
|
||||
|
||||
// Computed greeting
|
||||
const greetingText = $derived(getGreetingText(userName));
|
||||
// Computed greeting with i18n support
|
||||
const greetingText = $derived.by(() => {
|
||||
const info = getGreetingInfo(userName);
|
||||
if (info.type === 'custom') {
|
||||
return info.text;
|
||||
}
|
||||
return $t(info.key, { values: { name: info.name } });
|
||||
});
|
||||
|
||||
|
||||
// Check if a section is visible
|
||||
|
||||
@@ -1357,19 +1357,19 @@
|
||||
|
||||
if (result.removed > 0) {
|
||||
cleanupStatus = `Removed ${result.removed} of ${result.checked} tracks`;
|
||||
showToast(`Removed ${result.removed} tracks with missing files`, 'success');
|
||||
showToast($t('toast.removedMissingFiles', { values: { count: result.removed } }), 'success');
|
||||
// Reload library data
|
||||
cleanupStatus = 'Refreshing library...';
|
||||
await loadAlbums();
|
||||
await loadArtists();
|
||||
} else {
|
||||
cleanupStatus = `Checked ${result.checked} tracks - all OK`;
|
||||
showToast('No missing files found', 'info');
|
||||
showToast($t('toast.noMissingFilesFound'), 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to cleanup missing files:', err);
|
||||
cleanupStatus = 'Error during cleanup';
|
||||
showToast('Failed to cleanup missing files', 'error');
|
||||
showToast($t('toast.failedCleanupMissing'), 'error');
|
||||
} finally {
|
||||
cleaningUpMissingFiles = false;
|
||||
// Clear status after a delay
|
||||
@@ -1761,7 +1761,7 @@
|
||||
refreshingAlbumMetadata = true;
|
||||
albumMetadataRefreshed = false;
|
||||
await invoke('library_refresh_album_metadata_from_files', { albumGroupKey: selectedAlbum.id });
|
||||
showToast('Metadata refreshed from files', 'success');
|
||||
showToast($t('toast.metadataRefreshed'), 'success');
|
||||
albumMetadataRefreshed = true;
|
||||
await handleTagEditorSaved();
|
||||
} catch (err) {
|
||||
|
||||
@@ -1616,7 +1616,7 @@
|
||||
showToast($t('settings.appearance.tray.enableTrayDesc'), 'info');
|
||||
} catch (err) {
|
||||
console.error('Failed to set enable tray:', err);
|
||||
showToast('Failed to save tray setting', 'error');
|
||||
showToast($t('toast.failedSaveTray'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1626,7 +1626,7 @@
|
||||
minimizeToTray = value;
|
||||
} catch (err) {
|
||||
console.error('Failed to set minimize to tray:', err);
|
||||
showToast('Failed to save tray setting', 'error');
|
||||
showToast($t('toast.failedSaveTray'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1636,7 +1636,7 @@
|
||||
closeToTray = value;
|
||||
} catch (err) {
|
||||
console.error('Failed to set close to tray:', err);
|
||||
showToast('Failed to save tray setting', 'error');
|
||||
showToast($t('toast.failedSaveTray'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1648,7 +1648,7 @@
|
||||
console.log('[Settings] Autoplay mode saved successfully');
|
||||
} catch (err) {
|
||||
console.error('[Settings] Failed to set autoplay mode:', err);
|
||||
showToast('Failed to save autoplay preference', 'error');
|
||||
showToast($t('toast.failedSaveAutoplay'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1660,7 +1660,7 @@
|
||||
console.log('[Settings] Show context icon saved successfully');
|
||||
} catch (err) {
|
||||
console.error('[Settings] Failed to set show context icon:', err);
|
||||
showToast('Failed to save icon visibility preference', 'error');
|
||||
showToast($t('toast.failedSaveIconVisibility'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1702,7 +1702,7 @@
|
||||
notifyDownloadSettingsChanged();
|
||||
} catch (err) {
|
||||
console.error('Failed to repair downloads:', err);
|
||||
showToast('Failed to repair offline library: ' + String(err), 'error');
|
||||
showToast($t('toast.failedRepairOffline', { values: { error: String(err) } }), 'error');
|
||||
} finally {
|
||||
isRepairingDownloads = false;
|
||||
}
|
||||
@@ -1726,7 +1726,7 @@
|
||||
await invoke('open_offline_cache_folder');
|
||||
} catch (err) {
|
||||
console.error('Failed to open cache folder:', err);
|
||||
showToast('Failed to open cache folder', 'error');
|
||||
showToast($t('toast.failedOpenCacheFolder'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1915,8 +1915,8 @@
|
||||
<div class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<Loader2 size={48} class="spinner" />
|
||||
<p>Loading audio devices...</p>
|
||||
<p class="loading-subtitle">Parsing hardware information</p>
|
||||
<p>{$t('settings.audio.loadingAudioDevices')}</p>
|
||||
<p class="loading-subtitle">{$t('settings.audio.parsingHardware')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1974,15 +1974,15 @@
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Limit Quality to Device</span>
|
||||
<span class="setting-desc">When enabled, overrides the quality setting above if your audio device doesn't support the selected sample rate. Prevents resampling to guarantee bit-perfect playback.</span>
|
||||
<span class="setting-label">{$t('settings.audio.limitQualityToDevice')}</span>
|
||||
<span class="setting-desc">{$t('settings.audio.limitQualityToDeviceDesc')}</span>
|
||||
</div>
|
||||
<Toggle enabled={limitQualityToDevice} onchange={handleLimitQualityToDeviceChange} />
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Audio Backend</span>
|
||||
<span class="setting-desc">Choose audio system: Auto (recommended), PipeWire (modern), ALSA Direct (bit-perfect, exclusive), or PulseAudio (legacy).</span>
|
||||
<span class="setting-label">{$t('settings.audio.audioBackend')}</span>
|
||||
<span class="setting-desc">{$t('settings.audio.audioBackendDesc')}</span>
|
||||
</div>
|
||||
<Dropdown
|
||||
value={selectedBackend}
|
||||
@@ -1996,10 +1996,10 @@
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.audio.outputDevice')}</span>
|
||||
<span class="setting-desc">Select your preferred audio output device. Devices shown are from the selected backend.</span>
|
||||
<span class="setting-desc">{$t('settings.audio.outputDeviceDesc')}</span>
|
||||
</div>
|
||||
{#if isLoadingDevices}
|
||||
<span class="loading-text">Loading devices...</span>
|
||||
<span class="loading-text">{$t('settings.audio.loadingDevices')}</span>
|
||||
{:else if selectedBackend === 'ALSA Direct'}
|
||||
<div class="dropdown-with-help">
|
||||
<DeviceDropdown
|
||||
@@ -2013,7 +2013,7 @@
|
||||
<button
|
||||
class="help-icon-btn"
|
||||
onclick={() => showAlsaUtilsHelpModal = true}
|
||||
title="Help with bit-perfect device detection"
|
||||
title={$t('settings.audio.helpBitPerfect')}
|
||||
>
|
||||
<HelpCircle size={16} />
|
||||
</button>
|
||||
@@ -2041,8 +2041,8 @@
|
||||
{#if showAlsaPluginSelector}
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">ALSA Plugin</span>
|
||||
<span class="setting-desc">hw: Bit-perfect, exclusive. plughw: Auto-convert. pcm: Most compatible.</span>
|
||||
<span class="setting-label">{$t('settings.audio.alsaPlugin')}</span>
|
||||
<span class="setting-desc">{$t('settings.audio.alsaPluginDesc')}</span>
|
||||
</div>
|
||||
<Dropdown
|
||||
value={selectedAlsaPlugin}
|
||||
@@ -2057,8 +2057,8 @@
|
||||
{#if showAlsaHardwareVolume}
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Enable Hardware Volume Control</span>
|
||||
<span class="setting-desc">Experimental: Controls DAC volume via ALSA mixer. Some DACs don't support this - disable for maximum compatibility. If it fails, playback continues normally.</span>
|
||||
<span class="setting-label">{$t('settings.audio.hardwareVolume')}</span>
|
||||
<span class="setting-desc">{$t('settings.audio.hardwareVolumeDesc')}</span>
|
||||
</div>
|
||||
<Toggle enabled={alsaHardwareVolume} onchange={handleAlsaHardwareVolumeChange} />
|
||||
</div>
|
||||
@@ -2081,24 +2081,24 @@
|
||||
<div class="flatpak-warning">
|
||||
<div class="warning-icon">⚠️</div>
|
||||
<div class="warning-content">
|
||||
<strong>Flatpak Limitation:</strong> PipeWire cannot guarantee bit-perfect playback in sandboxed environments due to daemon access restrictions.
|
||||
<strong>{$t('settings.audio.flatpakWarningTitle')}</strong> {$t('settings.audio.flatpakWarningDesc')}
|
||||
<br />
|
||||
<strong>Recommended:</strong> Switch to ALSA Direct backend for true bit-perfect audio.
|
||||
<strong>{$t('settings.audio.flatpakRecommended')}</strong> {$t('settings.audio.flatpakRecommendedDesc')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Stream Uncached Tracks</span>
|
||||
<span class="setting-desc">Start playback faster when track is not in cache. Seeking may be limited during initial buffering.</span>
|
||||
<span class="setting-label">{$t('settings.audio.streamUncached')}</span>
|
||||
<span class="setting-desc">{$t('settings.audio.streamUncachedDesc')}</span>
|
||||
</div>
|
||||
<Toggle enabled={streamFirstTrack} onchange={handleStreamFirstTrackChange} />
|
||||
</div>
|
||||
{#if streamFirstTrack}
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Initial Buffer Size</span>
|
||||
<span class="setting-desc">Seconds to buffer before starting playback ({streamBufferSeconds}s)</span>
|
||||
<span class="setting-label">{$t('settings.audio.initialBuffer')}</span>
|
||||
<span class="setting-desc">{$t('settings.audio.initialBufferDesc', { values: { seconds: streamBufferSeconds } })}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
@@ -2113,8 +2113,8 @@
|
||||
{/if}
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Streaming Only</span>
|
||||
<span class="setting-desc">Disables temporary cache. Not recommended without a fast connection. Offline library tracks always play first.</span>
|
||||
<span class="setting-label">{$t('settings.audio.streamingOnly')}</span>
|
||||
<span class="setting-desc">{$t('settings.audio.streamingOnlyDesc')}</span>
|
||||
</div>
|
||||
<Toggle enabled={streamingOnly} onchange={handleStreamingOnlyChange} />
|
||||
</div>
|
||||
@@ -2333,7 +2333,7 @@
|
||||
<section class="section collapsible-section" bind:this={downloadsSection}>
|
||||
<button class="section-title-btn" onclick={() => offlineLibraryCollapsed = !offlineLibraryCollapsed}>
|
||||
<h3 class="section-title">{$t('settings.offlineLibrary.title')}</h3>
|
||||
<span class="section-summary">Cached tracks for offline playback</span>
|
||||
<span class="section-summary">{$t('settings.offlineLibrary.sectionSummary')}</span>
|
||||
{#if offlineLibraryCollapsed}
|
||||
<ChevronDown size={16} />
|
||||
{:else}
|
||||
@@ -2354,37 +2354,37 @@
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-with-description">
|
||||
<span class="setting-label">Repair Offline Library</span>
|
||||
<span class="setting-description">Fix offline markers lost during library scans</span>
|
||||
<span class="setting-label">{$t('settings.offlineLibrary.repair')}</span>
|
||||
<span class="setting-description">{$t('settings.offlineLibrary.repairDesc')}</span>
|
||||
</div>
|
||||
<button
|
||||
class="clear-btn"
|
||||
onclick={handleRepairDownloads}
|
||||
disabled={isRepairingDownloads || !downloadStats || downloadStats.readyTracks === 0}
|
||||
>
|
||||
{isRepairingDownloads ? 'Repairing...' : 'Repair'}
|
||||
{isRepairingDownloads ? $t('settings.offlineLibrary.repairing') : $t('actions.repair')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">Clear Offline Library</span>
|
||||
<span class="setting-label">{$t('settings.offlineLibrary.clearCache')}</span>
|
||||
<button
|
||||
class="clear-btn"
|
||||
onclick={handleClearDownloads}
|
||||
disabled={isClearingDownloads || !downloadStats || downloadStats.readyTracks === 0}
|
||||
>
|
||||
{isClearingDownloads ? 'Clearing...' : 'Clear All'}
|
||||
{isClearingDownloads ? $t('settings.storage.clearing') : $t('settings.offlineLibrary.clearCache')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-row last">
|
||||
<div class="setting-with-description">
|
||||
<span class="setting-label">Manage Offline Cache</span>
|
||||
<span class="setting-description">Open the cache folder in your file manager</span>
|
||||
<span class="setting-label">{$t('settings.offlineLibrary.manageCache')}</span>
|
||||
<span class="setting-description">{$t('settings.offlineLibrary.manageCacheDesc')}</span>
|
||||
</div>
|
||||
<button
|
||||
class="clear-btn"
|
||||
onclick={handleOpenCacheFolder}
|
||||
>
|
||||
Open Folder
|
||||
{$t('settings.offlineLibrary.openFolder')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -2582,9 +2582,9 @@
|
||||
<!-- MusicBrainz Integration -->
|
||||
<div class="setting-row last">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">MusicBrainz</span>
|
||||
<span class="setting-label">{$t('settings.integrations.musicbrainz')}</span>
|
||||
<small class="setting-note">
|
||||
Enable artist relationships and enhanced metadata from MusicBrainz.
|
||||
{$t('settings.integrations.musicbrainzDesc')}
|
||||
</small>
|
||||
</div>
|
||||
<Toggle enabled={musicbrainzEnabled} onchange={handleMusicBrainzChange} />
|
||||
@@ -2593,11 +2593,11 @@
|
||||
|
||||
<!-- Updates Section -->
|
||||
<section class="section" bind:this={updatesSection}>
|
||||
<h3 class="section-title">Updates</h3>
|
||||
<h3 class="section-title">{$t('settings.updates.title')}</h3>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Check for new releases on launch</span>
|
||||
<span class="setting-label">{$t('settings.updates.checkOnLaunch')}</span>
|
||||
</div>
|
||||
<Toggle
|
||||
enabled={updatePreferences.checkOnLaunch}
|
||||
@@ -2607,7 +2607,7 @@
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Check for updates now</span>
|
||||
<span class="setting-label">{$t('settings.updates.checkNow')}</span>
|
||||
</div>
|
||||
<button
|
||||
class="connect-btn updates-check-btn"
|
||||
@@ -2617,16 +2617,16 @@
|
||||
>
|
||||
{#if isCheckingUpdates}
|
||||
<Loader2 size={14} class="spin" />
|
||||
<span>Checking...</span>
|
||||
<span>{$t('settings.updates.checking')}</span>
|
||||
{:else}
|
||||
<span>Check</span>
|
||||
<span>{$t('settings.updates.check')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Show what's new on new version launch</span>
|
||||
<span class="setting-label">{$t('settings.updates.showWhatsNew')}</span>
|
||||
</div>
|
||||
<Toggle
|
||||
enabled={updatePreferences.showWhatsNewOnLaunch}
|
||||
@@ -2636,9 +2636,9 @@
|
||||
|
||||
<div class="setting-row last">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Show changelog for current version</span>
|
||||
<span class="setting-label">{$t('settings.updates.showChangelog')}</span>
|
||||
{#if updatesCurrentVersion}
|
||||
<small class="setting-note">Current version: v{updatesCurrentVersion}</small>
|
||||
<small class="setting-note">{$t('settings.updates.currentVersion', { values: { version: updatesCurrentVersion } })}</small>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
@@ -2646,7 +2646,7 @@
|
||||
onclick={handleShowCurrentChangelog}
|
||||
type="button"
|
||||
>
|
||||
Show
|
||||
{$t('actions.show')}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -2674,7 +2674,7 @@
|
||||
<section class="section collapsible-section" bind:this={storageSection}>
|
||||
<button class="section-title-btn" onclick={() => storageCollapsed = !storageCollapsed}>
|
||||
<h3 class="section-title">{$t('settings.storage.title')}</h3>
|
||||
<span class="section-summary">Queue, lyrics, artwork, and metadata caches</span>
|
||||
<span class="section-summary">{$t('settings.storage.sectionSummary')}</span>
|
||||
{#if storageCollapsed}
|
||||
<ChevronDown size={16} />
|
||||
{:else}
|
||||
@@ -2734,7 +2734,7 @@
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">MusicBrainz Cache</span>
|
||||
<span class="setting-label">{$t('settings.integrations.musicbrainzCache')}</span>
|
||||
<small class="setting-note">
|
||||
{#if musicBrainzCacheStats}
|
||||
{musicBrainzCacheStats.artists} artists, {musicBrainzCacheStats.relations} relations, {musicBrainzCacheStats.recordings} recordings
|
||||
@@ -2809,7 +2809,7 @@
|
||||
onclick={handleClearAllCaches}
|
||||
disabled={isClearingAllCaches}
|
||||
>
|
||||
{isClearingAllCaches ? $t('settings.storage.clearing') : 'Clear All'}
|
||||
{isClearingAllCaches ? $t('settings.storage.clearing') : $t('actions.clearAll')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -139,7 +139,29 @@
|
||||
"releaseYear": "Release Year",
|
||||
"artistName": "Artist Name",
|
||||
"alphabetical": "Alphabetical",
|
||||
"group": "Group:"
|
||||
"group": "Group:",
|
||||
"backToLibrary": "Back to Library",
|
||||
"yourCollection": "Your local music collection",
|
||||
"playlistOfflineMode": "Playlist management is disabled in offline mode. You can still play your local tracks.",
|
||||
"artworkOffline": "Artwork fetching unavailable offline",
|
||||
"loadingLibrary": "Loading library...",
|
||||
"failedLoadLibrary": "Failed to load library",
|
||||
"backToActiveAlbums": "Back to Active Albums",
|
||||
"noHiddenAlbums": "No hidden albums",
|
||||
"noAlbumsInLibrary": "No albums in library",
|
||||
"addFoldersHint": "Add folders and scan to build your library",
|
||||
"clearAllFilters": "Clear all",
|
||||
"offlineCache": "Offline cache",
|
||||
"noAlbumsMatch": "No albums match your search",
|
||||
"tryDifferentSearch": "Try a different artist or album name",
|
||||
"noArtistsInLibrary": "No artists in library",
|
||||
"noArtistsMatch": "No artists match your search",
|
||||
"noAlbumsFound": "No albums found",
|
||||
"selectArtist": "Select an artist to view their albums",
|
||||
"noTracksInLibrary": "No tracks in library",
|
||||
"editAlbumInfo": "Edit album info",
|
||||
"hideAlbum": "Hide this album from library",
|
||||
"hideAlbumHint": "Hidden albums can be viewed from Settings"
|
||||
},
|
||||
"album": {
|
||||
"tracks": "tracks",
|
||||
@@ -359,8 +381,10 @@
|
||||
"edit": "Edit",
|
||||
"close": "Close",
|
||||
"open": "Open",
|
||||
"show": "Show",
|
||||
"refresh": "Refresh",
|
||||
"retry": "Retry",
|
||||
"repair": "Repair",
|
||||
"clear": "Clear",
|
||||
"clearAll": "Clear All",
|
||||
"confirm": "Confirm",
|
||||
@@ -388,6 +412,7 @@
|
||||
"streamingQualityDesc": "Maximum quality for streaming and playback cache",
|
||||
"qualityChangedCacheCleared": "Quality changed. Playback cache cleared.",
|
||||
"outputDevice": "Output Device",
|
||||
"outputDeviceDesc": "Select your preferred audio output device. Devices shown are from the selected backend.",
|
||||
"audioDevice": "Audio Device",
|
||||
"audioDeviceDesc": "Select your preferred audio output device. Devices shown are from the selected backend.",
|
||||
"exclusiveMode": "Exclusive Mode",
|
||||
@@ -400,6 +425,28 @@
|
||||
"audioBackendDesc": "Choose audio system: Auto (recommended), PipeWire (modern), ALSA Direct (bit-perfect, exclusive), or PulseAudio (legacy).",
|
||||
"currentSampleRate": "Current Sample Rate",
|
||||
"noActivePlayback": "No active playback",
|
||||
"loadingDevices": "Loading devices...",
|
||||
"loadingAudioDevices": "Loading audio devices...",
|
||||
"parsingHardware": "Parsing hardware information",
|
||||
"limitQualityToDevice": "Limit Quality to Device",
|
||||
"limitQualityToDeviceDesc": "When enabled, overrides the quality setting above if your audio device doesn't support the selected sample rate. Prevents resampling to guarantee bit-perfect playback.",
|
||||
"audioBackend": "Audio Backend",
|
||||
"audioBackendDesc": "Choose audio system: Auto (recommended), PipeWire (modern), ALSA Direct (bit-perfect, exclusive), or PulseAudio (legacy).",
|
||||
"alsaPlugin": "ALSA Plugin",
|
||||
"alsaPluginDesc": "hw: Bit-perfect, exclusive. plughw: Auto-convert. pcm: Most compatible.",
|
||||
"hardwareVolume": "Enable Hardware Volume Control",
|
||||
"hardwareVolumeDesc": "Experimental: Controls DAC volume via ALSA mixer. Some DACs don't support this - disable for maximum compatibility. If it fails, playback continues normally.",
|
||||
"helpBitPerfect": "Help with bit-perfect device detection",
|
||||
"flatpakWarningTitle": "Flatpak Limitation:",
|
||||
"flatpakWarningDesc": "PipeWire cannot guarantee bit-perfect playback in sandboxed environments due to daemon access restrictions.",
|
||||
"flatpakRecommended": "Recommended:",
|
||||
"flatpakRecommendedDesc": "Switch to ALSA Direct backend for true bit-perfect audio.",
|
||||
"streamUncached": "Stream Uncached Tracks",
|
||||
"streamUncachedDesc": "Start playback faster when track is not in cache. Seeking may be limited during initial buffering.",
|
||||
"initialBuffer": "Initial Buffer Size",
|
||||
"initialBufferDesc": "Seconds to buffer before starting playback ({seconds}s)",
|
||||
"streamingOnly": "Streaming Only",
|
||||
"streamingOnlyDesc": "Disables temporary cache. Not recommended without a fast connection. Offline library tracks always play first.",
|
||||
"streamBuffer": "Stream Buffering",
|
||||
"streamBufferDesc": "Start playback faster when track is not in cache. Seeking may be limited during initial buffering.",
|
||||
"streamBufferSeconds": "Seconds to buffer before starting playback ({seconds}s)",
|
||||
@@ -489,10 +536,15 @@
|
||||
"connect": "Connect",
|
||||
"disconnect": "Disconnect",
|
||||
"scrobbling": "Scrobbling",
|
||||
"configureApi": "Configure API"
|
||||
"configureApi": "Configure API",
|
||||
"musicbrainz": "MusicBrainz",
|
||||
"musicbrainzDesc": "Enable artist relationships and enhanced metadata from MusicBrainz.",
|
||||
"musicbrainzCache": "MusicBrainz Cache",
|
||||
"listenbrainz": "ListenBrainz"
|
||||
},
|
||||
"storage": {
|
||||
"title": "Storage",
|
||||
"sectionSummary": "Queue, lyrics, artwork, and metadata caches",
|
||||
"queueCacheNote": "Queue list storage. Clear if you have playback issues. (This is not the Offline Library cache.)",
|
||||
"queueCacheStats": "{tracks} tracks using {used} / {max}",
|
||||
"cachedTracks": "Cached Queue Tracks",
|
||||
@@ -500,6 +552,25 @@
|
||||
"clearCache": "Clear queue cache",
|
||||
"clearing": "Clearing..."
|
||||
},
|
||||
"blacklist": {
|
||||
"title": "Artist Blacklist",
|
||||
"description": "Blacklisted artists are hidden from search results, radio, suggestions, and similar artists.",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"clearAll": "Clear All",
|
||||
"noArtists": "No blacklisted artists",
|
||||
"noArtistsHint": "To blacklist an artist, go to their page and click the ban icon.",
|
||||
"artistCount": "{count} artists",
|
||||
"loadingBlacklist": "Loading blacklist...",
|
||||
"confirmClear": "Remove all artists from blacklist?",
|
||||
"disabledWarning": "Blacklist is currently disabled. Artists below will appear in results.",
|
||||
"searchPlaceholder": "Search blacklisted artists...",
|
||||
"noResults": "No results for \"{query}\"",
|
||||
"addedOn": "Added {date}",
|
||||
"remove": "Remove from blacklist",
|
||||
"confirmClearTitle": "Clear Blacklist?",
|
||||
"confirmClearText": "This will remove all {count} artists from your blacklist. This action cannot be undone."
|
||||
},
|
||||
"offlineLibrary": {
|
||||
"title": "Offline Library",
|
||||
"disclaimer": "The offline library is a temporary playback cache for offline listening while you have a valid Qobuz subscription. You do not receive a license to keep or redistribute the content. If your subscription becomes invalid, QBZ will remove all cached content after 3 days.",
|
||||
@@ -507,9 +578,23 @@
|
||||
"cachedTracksDesc": "Cached tracks for offline playback",
|
||||
"clearCache": "Clear All",
|
||||
"openFolder": "Open Folder",
|
||||
"repair": "Repair",
|
||||
"repair": "Repair Offline Library",
|
||||
"repairDesc": "Fix offline markers lost during library scans",
|
||||
"openFolderDesc": "Open the cache folder in your file manager"
|
||||
"manageCache": "Manage Offline Cache",
|
||||
"manageCacheDesc": "Open the cache folder in your file manager",
|
||||
"openFolderDesc": "Open the cache folder in your file manager",
|
||||
"sectionSummary": "Cached tracks for offline playback",
|
||||
"repairing": "Repairing..."
|
||||
},
|
||||
"updates": {
|
||||
"title": "Updates",
|
||||
"checkOnLaunch": "Check for new releases on launch",
|
||||
"checkNow": "Check for updates now",
|
||||
"checking": "Checking...",
|
||||
"check": "Check",
|
||||
"showWhatsNew": "Show what's new on new version launch",
|
||||
"showChangelog": "Show changelog for current version",
|
||||
"currentVersion": "Current version: v{version}"
|
||||
},
|
||||
"networkCacheWarning": "The selected folder is on a network drive.",
|
||||
"networkCacheWarningDetail": "Cached tracks may not be available when the network is disconnected or in offline mode. Are you sure you want to use this location?",
|
||||
@@ -623,12 +708,120 @@
|
||||
"customGreeting": "Custom greeting",
|
||||
"customGreetingDesc": "Use {name} for your name. Leave empty for time-based.",
|
||||
"customGreetingPlaceholder": "Good vibes, {name}!",
|
||||
"goodMorning": "Good morning, {name}",
|
||||
"goodAfternoon": "Good afternoon, {name}",
|
||||
"goodEvening": "Good evening, {name}",
|
||||
"goodNight": "Good night, {name}",
|
||||
"sections": "Sections",
|
||||
"sectionsDesc": "Reorder with arrows. Toggle visibility with switch.",
|
||||
"moveUp": "Move up",
|
||||
"moveDown": "Move down",
|
||||
"resetToDefaults": "Reset to defaults"
|
||||
},
|
||||
"toast": {
|
||||
"loadingAlbum": "Loading album...",
|
||||
"loadingArtist": "Loading artist...",
|
||||
"loadingPlaylist": "Loading playlist...",
|
||||
"lookingUp": "Looking up {name}...",
|
||||
"failedLoadAlbum": "Failed to load album",
|
||||
"failedLoadArtist": "Failed to load artist",
|
||||
"failedLoadPlaylist": "Failed to load playlist",
|
||||
"failedLoadMoreAlbums": "Failed to load more albums",
|
||||
"failedAddToQueue": "Failed to add to queue",
|
||||
"failedPlayTrack": "Failed to play track",
|
||||
"queueCleared": "Queue cleared",
|
||||
"queueEmpty": "Queue is empty",
|
||||
"queueEnded": "Queue ended",
|
||||
"failedClearQueue": "Failed to clear queue",
|
||||
"shuffleEnabled": "Shuffle play enabled",
|
||||
"shuffleDisabled": "Shuffle disabled",
|
||||
"repeatOff": "Repeat: Off",
|
||||
"repeatOne": "Repeat: One",
|
||||
"repeatAll": "Repeat: All",
|
||||
"infinitePlayEnabled": "Infinite play enabled",
|
||||
"infinitePlayDisabled": "Infinite play disabled",
|
||||
"linkCopied": "Link copied to clipboard",
|
||||
"albumLinkCopied": "Album link copied to clipboard",
|
||||
"playlistLinkCopied": "Playlist link copied to clipboard",
|
||||
"trackLinkCopied": "Track link copied to clipboard",
|
||||
"fetchingSonglink": "Fetching song.link...",
|
||||
"fetchingAlbumLink": "Fetching Album.link...",
|
||||
"songlinkCopied": "song.link copied to clipboard",
|
||||
"albumLinkCopiedSonglink": "Album.link copied to clipboard",
|
||||
"songlinkError": "song.link error: {error}",
|
||||
"albumLinkError": "Album.link error: {error}",
|
||||
"allTracksOffline": "All tracks already available offline",
|
||||
"removedFromOffline": "Removed from offline library",
|
||||
"failedRemoveOffline": "Failed to remove from offline library",
|
||||
"failedOpenFolder": "Failed to open folder",
|
||||
"failedOpenAlbumFolder": "Failed to open album folder",
|
||||
"playlistNoTracks": "Playlist has no tracks",
|
||||
"copiedToLibrary": "Copied to library",
|
||||
"copyingToLibrary": "Copying to library...",
|
||||
"playlistCopied": "Playlist copied to library",
|
||||
"failedCopyPlaylist": "Failed to copy playlist: {error}",
|
||||
"removedFromFavorites": "Removed from favorites",
|
||||
"playlistRemovedFavorites": "Playlist removed from favorites",
|
||||
"failedRemoveFavorites": "Failed to remove from favorites: {error}",
|
||||
"failedUpdateFavorites": "Failed to update favorites",
|
||||
"addedToFavorites": "Added to favorites",
|
||||
"noAvailableTracks": "No available tracks in queue (offline mode)",
|
||||
"failedReorderQueue": "Failed to reorder queue",
|
||||
"trackNotInHistory": "Track not found in history",
|
||||
"failedNavigateSource": "Failed to navigate to source",
|
||||
"failedLookupMusician": "Could not look up musician info",
|
||||
"couldNotFetchDetails": "Could not fetch details",
|
||||
"failedPrepareOffline": "Failed to prepare for offline",
|
||||
"failedRefreshOffline": "Failed to refresh offline copy",
|
||||
"preparingTrackOffline": "Preparing \"{title}\" for offline",
|
||||
"preparingTracksOffline": "Preparing {count} tracks from \"{album}\" for offline",
|
||||
"refreshingTrackOffline": "Refreshing \"{title}\" for offline",
|
||||
"refreshingAlbumOffline": "Refreshing all tracks from \"{album}\" for offline",
|
||||
"failedLoadAlbumRefresh": "Failed to load album for offline refresh",
|
||||
"playingTracksNext": "Playing {count} tracks next",
|
||||
"addedTracksToQueue": "Added {count} tracks to queue",
|
||||
"failedPreviousTrack": "Failed to go to previous track",
|
||||
"failedNextTrack": "Failed to go to next track",
|
||||
"tracksAddedToPlaylist": "Track(s) added to playlist",
|
||||
"playlistCreated": "Playlist created",
|
||||
"offlineModeStarted": "Started in offline mode",
|
||||
"welcomeUser": "Welcome, {name}!",
|
||||
"restoredTrack": "Restored: {title}",
|
||||
"logoutSuccess": "Logged out successfully",
|
||||
"failedLogout": "Failed to logout",
|
||||
"localTrackSearch": "Local track - search for artist in Search",
|
||||
"queuedPlayNext": "Queued to play next",
|
||||
"failedQueueTrack": "Failed to queue track",
|
||||
"addedToQueue": "Added to queue",
|
||||
"offlineModeActivated": "Offline mode activated",
|
||||
"backOnline": "Back online",
|
||||
"syncedPlaylist": "Synced playlist \"{name}\"",
|
||||
"failedSyncPlaylist": "Failed to sync playlist \"{name}\": {error}",
|
||||
"playbackError": "Playback error: {error}",
|
||||
"trackUnavailable": "\"{title}\" is no longer available",
|
||||
"playing": "Playing: {title}",
|
||||
"failedCopyLink": "Failed to copy link",
|
||||
"failedShareQobuz": "Failed to share Qobuz link: {error}",
|
||||
"playlistCreatedOffline": "Playlist created offline - will sync when back online",
|
||||
"playlistImported": "Playlist imported",
|
||||
"playlistImportFailed": "Playlist import failed",
|
||||
"enterSearchTerms": "Enter album title or artist to search",
|
||||
"searchFailed": "Search failed: {error}",
|
||||
"appliedMetadata": "Applied metadata from {provider}",
|
||||
"rateLimited": "Rate limited. Please wait a moment and try again.",
|
||||
"failedFetchMetadata": "Failed to fetch metadata: {error}",
|
||||
"failedOpenBrowser": "Failed to open browser",
|
||||
"albumMetadataSaved": "Album metadata saved",
|
||||
"removedMissingFiles": "Removed {count} tracks with missing files",
|
||||
"noMissingFilesFound": "No missing files found",
|
||||
"failedCleanupMissing": "Failed to cleanup missing files",
|
||||
"metadataRefreshed": "Metadata refreshed from files",
|
||||
"failedSaveTray": "Failed to save tray setting",
|
||||
"failedSaveAutoplay": "Failed to save autoplay preference",
|
||||
"failedSaveIconVisibility": "Failed to save icon visibility preference",
|
||||
"failedRepairOffline": "Failed to repair offline library: {error}",
|
||||
"failedOpenCacheFolder": "Failed to open cache folder"
|
||||
},
|
||||
"offline": {
|
||||
"title": "Offline Mode",
|
||||
"message": "Connect to the internet to access Qobuz™ content.",
|
||||
|
||||
@@ -139,7 +139,29 @@
|
||||
"releaseYear": "Año de Lanzamiento",
|
||||
"artistName": "Nombre del Artista",
|
||||
"alphabetical": "Alfabético",
|
||||
"group": "Agrupar:"
|
||||
"group": "Agrupar:",
|
||||
"backToLibrary": "Volver a la Biblioteca",
|
||||
"yourCollection": "Tu colección de música local",
|
||||
"playlistOfflineMode": "La gestión de playlists está deshabilitada en modo offline. Aún puedes reproducir tus pistas locales.",
|
||||
"artworkOffline": "Obtención de carátulas no disponible offline",
|
||||
"loadingLibrary": "Cargando biblioteca...",
|
||||
"failedLoadLibrary": "Error al cargar la biblioteca",
|
||||
"backToActiveAlbums": "Volver a Álbumes Activos",
|
||||
"noHiddenAlbums": "No hay álbumes ocultos",
|
||||
"noAlbumsInLibrary": "No hay álbumes en la biblioteca",
|
||||
"addFoldersHint": "Agrega carpetas y escanea para construir tu biblioteca",
|
||||
"clearAllFilters": "Limpiar todo",
|
||||
"offlineCache": "Caché offline",
|
||||
"noAlbumsMatch": "Ningún álbum coincide con tu búsqueda",
|
||||
"tryDifferentSearch": "Prueba con un artista o álbum diferente",
|
||||
"noArtistsInLibrary": "No hay artistas en la biblioteca",
|
||||
"noArtistsMatch": "Ningún artista coincide con tu búsqueda",
|
||||
"noAlbumsFound": "No se encontraron álbumes",
|
||||
"selectArtist": "Selecciona un artista para ver sus álbumes",
|
||||
"noTracksInLibrary": "No hay pistas en la biblioteca",
|
||||
"editAlbumInfo": "Editar información del álbum",
|
||||
"hideAlbum": "Ocultar este álbum de la biblioteca",
|
||||
"hideAlbumHint": "Los álbumes ocultos se pueden ver desde Configuración"
|
||||
},
|
||||
"album": {
|
||||
"tracks": "pistas",
|
||||
@@ -359,8 +381,10 @@
|
||||
"edit": "Editar",
|
||||
"close": "Cerrar",
|
||||
"open": "Abrir",
|
||||
"show": "Mostrar",
|
||||
"refresh": "Actualizar",
|
||||
"retry": "Reintentar",
|
||||
"repair": "Reparar",
|
||||
"clear": "Limpiar",
|
||||
"clearAll": "Limpiar Todo",
|
||||
"confirm": "Confirmar",
|
||||
@@ -388,6 +412,7 @@
|
||||
"streamingQualityDesc": "Calidad máxima para streaming y caché de reproducción",
|
||||
"qualityChangedCacheCleared": "Calidad cambiada. Caché de reproducción limpiado.",
|
||||
"outputDevice": "Dispositivo de Salida",
|
||||
"outputDeviceDesc": "Selecciona tu dispositivo de audio preferido. Se muestran los dispositivos del backend seleccionado.",
|
||||
"audioDevice": "Dispositivo de Audio",
|
||||
"audioDeviceDesc": "Selecciona tu dispositivo de salida de audio preferido. Los dispositivos mostrados son del backend seleccionado.",
|
||||
"exclusiveMode": "Modo Exclusivo",
|
||||
@@ -400,6 +425,28 @@
|
||||
"audioBackendDesc": "Elige el sistema de audio: Auto (recomendado), PipeWire (moderno), ALSA Direct (bit-perfect, exclusivo), o PulseAudio (legado).",
|
||||
"currentSampleRate": "Tasa de Muestreo Actual",
|
||||
"noActivePlayback": "Sin reproducción activa",
|
||||
"loadingDevices": "Cargando dispositivos...",
|
||||
"loadingAudioDevices": "Cargando dispositivos de audio...",
|
||||
"parsingHardware": "Analizando información de hardware",
|
||||
"limitQualityToDevice": "Limitar Calidad al Dispositivo",
|
||||
"limitQualityToDeviceDesc": "Cuando está activado, anula la configuración de calidad si tu dispositivo de audio no soporta la tasa de muestreo seleccionada. Previene el remuestreo para garantizar reproducción bit-perfect.",
|
||||
"audioBackend": "Backend de Audio",
|
||||
"audioBackendDesc": "Elige el sistema de audio: Auto (recomendado), PipeWire (moderno), ALSA Direct (bit-perfect, exclusivo), o PulseAudio (legacy).",
|
||||
"alsaPlugin": "Plugin ALSA",
|
||||
"alsaPluginDesc": "hw: Bit-perfect, exclusivo. plughw: Auto-conversión. pcm: Más compatible.",
|
||||
"hardwareVolume": "Habilitar Control de Volumen por Hardware",
|
||||
"hardwareVolumeDesc": "Experimental: Controla el volumen del DAC vía mixer ALSA. Algunos DACs no lo soportan - desactívalo para máxima compatibilidad. Si falla, la reproducción continúa normalmente.",
|
||||
"helpBitPerfect": "Ayuda con detección de dispositivos bit-perfect",
|
||||
"flatpakWarningTitle": "Limitación de Flatpak:",
|
||||
"flatpakWarningDesc": "PipeWire no puede garantizar reproducción bit-perfect en entornos sandbox debido a restricciones de acceso al daemon.",
|
||||
"flatpakRecommended": "Recomendado:",
|
||||
"flatpakRecommendedDesc": "Cambiar al backend ALSA Direct para audio bit-perfect verdadero.",
|
||||
"streamUncached": "Transmitir Pistas Sin Caché",
|
||||
"streamUncachedDesc": "Inicia la reproducción más rápido cuando la pista no está en caché. La búsqueda puede estar limitada durante el buffering inicial.",
|
||||
"initialBuffer": "Tamaño del Buffer Inicial",
|
||||
"initialBufferDesc": "Segundos a almacenar en buffer antes de iniciar reproducción ({seconds}s)",
|
||||
"streamingOnly": "Solo Streaming",
|
||||
"streamingOnlyDesc": "Desactiva el caché temporal. No recomendado sin una conexión rápida. Las pistas de la biblioteca offline siempre se reproducen primero.",
|
||||
"streamBuffer": "Buffering de Stream",
|
||||
"streamBufferDesc": "Inicia la reproducción más rápido cuando la pista no está en caché. La búsqueda puede estar limitada durante el buffering inicial.",
|
||||
"streamBufferSeconds": "Segundos a almacenar antes de iniciar reproducción ({seconds}s)",
|
||||
@@ -489,10 +536,15 @@
|
||||
"connect": "Conectar",
|
||||
"disconnect": "Desconectar",
|
||||
"scrobbling": "Scrobbling",
|
||||
"configureApi": "Configurar API"
|
||||
"configureApi": "Configurar API",
|
||||
"musicbrainz": "MusicBrainz",
|
||||
"musicbrainzDesc": "Habilita relaciones de artistas y metadatos mejorados de MusicBrainz.",
|
||||
"musicbrainzCache": "Caché de MusicBrainz",
|
||||
"listenbrainz": "ListenBrainz"
|
||||
},
|
||||
"storage": {
|
||||
"title": "Almacenamiento",
|
||||
"sectionSummary": "Cachés de cola, letras, carátulas y metadatos",
|
||||
"queueCacheNote": "Almacenamiento de la cola. Límpialo si tienes problemas de reproducción. (No es la Biblioteca Offline.)",
|
||||
"queueCacheStats": "{tracks} pistas usando {used} / {max}",
|
||||
"cachedTracks": "Pistas de la cola en caché",
|
||||
@@ -500,6 +552,25 @@
|
||||
"clearCache": "Limpiar caché de la cola",
|
||||
"clearing": "Limpiando..."
|
||||
},
|
||||
"blacklist": {
|
||||
"title": "Lista Negra de Artistas",
|
||||
"description": "Los artistas en la lista negra están ocultos en resultados de búsqueda, radio, sugerencias y artistas similares.",
|
||||
"enabled": "Activado",
|
||||
"disabled": "Desactivado",
|
||||
"clearAll": "Limpiar Todo",
|
||||
"noArtists": "No hay artistas en la lista negra",
|
||||
"noArtistsHint": "Para añadir un artista a la lista negra, ve a su página y haz clic en el ícono de prohibición.",
|
||||
"artistCount": "{count} artistas",
|
||||
"loadingBlacklist": "Cargando lista negra...",
|
||||
"confirmClear": "¿Eliminar todos los artistas de la lista negra?",
|
||||
"disabledWarning": "La lista negra está desactivada. Los artistas de abajo aparecerán en los resultados.",
|
||||
"searchPlaceholder": "Buscar artistas en la lista negra...",
|
||||
"noResults": "Sin resultados para \"{query}\"",
|
||||
"addedOn": "Añadido {date}",
|
||||
"remove": "Eliminar de la lista negra",
|
||||
"confirmClearTitle": "¿Limpiar Lista Negra?",
|
||||
"confirmClearText": "Esto eliminará los {count} artistas de tu lista negra. Esta acción no se puede deshacer."
|
||||
},
|
||||
"offlineLibrary": {
|
||||
"title": "Biblioteca Offline",
|
||||
"disclaimer": "La biblioteca offline es un caché temporal de reproducción para escucha sin conexión mientras tengas una suscripción válida de Qobuz. No recibes una licencia para conservar o redistribuir el contenido. Si tu suscripción deja de ser válida, QBZ eliminará todo el contenido en caché después de 3 días.",
|
||||
@@ -507,9 +578,23 @@
|
||||
"cachedTracksDesc": "Pistas en caché para reproducción offline",
|
||||
"clearCache": "Limpiar Todo",
|
||||
"openFolder": "Abrir Carpeta",
|
||||
"repair": "Reparar",
|
||||
"repair": "Reparar Biblioteca Offline",
|
||||
"repairDesc": "Corregir marcadores offline perdidos durante escaneos de biblioteca",
|
||||
"openFolderDesc": "Abrir la carpeta de caché en tu gestor de archivos"
|
||||
"manageCache": "Administrar Caché Offline",
|
||||
"manageCacheDesc": "Abrir la carpeta de caché en tu administrador de archivos",
|
||||
"openFolderDesc": "Abrir la carpeta de caché en tu gestor de archivos",
|
||||
"sectionSummary": "Pistas en caché para reproducción offline",
|
||||
"repairing": "Reparando..."
|
||||
},
|
||||
"updates": {
|
||||
"title": "Actualizaciones",
|
||||
"checkOnLaunch": "Buscar nuevas versiones al iniciar",
|
||||
"checkNow": "Buscar actualizaciones ahora",
|
||||
"checking": "Verificando...",
|
||||
"check": "Verificar",
|
||||
"showWhatsNew": "Mostrar novedades al lanzar nueva versión",
|
||||
"showChangelog": "Mostrar registro de cambios para la versión actual",
|
||||
"currentVersion": "Versión actual: v{version}"
|
||||
},
|
||||
"networkCacheWarning": "La carpeta seleccionada está en una unidad de red.",
|
||||
"networkCacheWarningDetail": "Las pistas en caché pueden no estar disponibles cuando la red esté desconectada o en modo offline. ¿Estás seguro de que quieres usar esta ubicación?",
|
||||
@@ -623,12 +708,120 @@
|
||||
"customGreeting": "Saludo personalizado",
|
||||
"customGreetingDesc": "Usa {name} para tu nombre. Deja vacío para basado en hora.",
|
||||
"customGreetingPlaceholder": "¡Buenas vibras, {name}!",
|
||||
"goodMorning": "Buenos días, {name}",
|
||||
"goodAfternoon": "Buenas tardes, {name}",
|
||||
"goodEvening": "Buenas noches, {name}",
|
||||
"goodNight": "Buenas noches, {name}",
|
||||
"sections": "Secciones",
|
||||
"sectionsDesc": "Reordena con flechas. Activa/desactiva con interruptor.",
|
||||
"moveUp": "Subir",
|
||||
"moveDown": "Bajar",
|
||||
"resetToDefaults": "Restablecer valores"
|
||||
},
|
||||
"toast": {
|
||||
"loadingAlbum": "Cargando álbum...",
|
||||
"loadingArtist": "Cargando artista...",
|
||||
"loadingPlaylist": "Cargando playlist...",
|
||||
"lookingUp": "Buscando {name}...",
|
||||
"failedLoadAlbum": "Error al cargar álbum",
|
||||
"failedLoadArtist": "Error al cargar artista",
|
||||
"failedLoadPlaylist": "Error al cargar playlist",
|
||||
"failedLoadMoreAlbums": "Error al cargar más álbumes",
|
||||
"failedAddToQueue": "Error al agregar a la cola",
|
||||
"failedPlayTrack": "Error al reproducir pista",
|
||||
"queueCleared": "Cola limpiada",
|
||||
"queueEmpty": "La cola está vacía",
|
||||
"queueEnded": "Cola terminada",
|
||||
"failedClearQueue": "Error al limpiar cola",
|
||||
"shuffleEnabled": "Reproducción aleatoria activada",
|
||||
"shuffleDisabled": "Reproducción aleatoria desactivada",
|
||||
"repeatOff": "Repetir: Desactivado",
|
||||
"repeatOne": "Repetir: Una",
|
||||
"repeatAll": "Repetir: Todas",
|
||||
"infinitePlayEnabled": "Reproducción infinita activada",
|
||||
"infinitePlayDisabled": "Reproducción infinita desactivada",
|
||||
"linkCopied": "Enlace copiado al portapapeles",
|
||||
"albumLinkCopied": "Enlace del álbum copiado",
|
||||
"playlistLinkCopied": "Enlace de la playlist copiado",
|
||||
"trackLinkCopied": "Enlace de la pista copiado",
|
||||
"fetchingSonglink": "Obteniendo song.link...",
|
||||
"fetchingAlbumLink": "Obteniendo Album.link...",
|
||||
"songlinkCopied": "song.link copiado al portapapeles",
|
||||
"albumLinkCopiedSonglink": "Album.link copiado al portapapeles",
|
||||
"songlinkError": "Error en song.link: {error}",
|
||||
"albumLinkError": "Error en Album.link: {error}",
|
||||
"allTracksOffline": "Todas las pistas ya están disponibles offline",
|
||||
"removedFromOffline": "Eliminado de la biblioteca offline",
|
||||
"failedRemoveOffline": "Error al eliminar de biblioteca offline",
|
||||
"failedOpenFolder": "Error al abrir carpeta",
|
||||
"failedOpenAlbumFolder": "Error al abrir carpeta del álbum",
|
||||
"playlistNoTracks": "La playlist no tiene pistas",
|
||||
"copiedToLibrary": "Copiado a la biblioteca",
|
||||
"copyingToLibrary": "Copiando a la biblioteca...",
|
||||
"playlistCopied": "Playlist copiada a la biblioteca",
|
||||
"failedCopyPlaylist": "Error al copiar playlist: {error}",
|
||||
"removedFromFavorites": "Eliminado de favoritos",
|
||||
"playlistRemovedFavorites": "Playlist eliminada de favoritos",
|
||||
"failedRemoveFavorites": "Error al eliminar de favoritos: {error}",
|
||||
"failedUpdateFavorites": "Error al actualizar favoritos",
|
||||
"addedToFavorites": "Agregado a favoritos",
|
||||
"noAvailableTracks": "Sin pistas disponibles en la cola (modo offline)",
|
||||
"failedReorderQueue": "Error al reordenar cola",
|
||||
"trackNotInHistory": "Pista no encontrada en historial",
|
||||
"failedNavigateSource": "Error al navegar al origen",
|
||||
"failedLookupMusician": "No se pudo buscar info del músico",
|
||||
"couldNotFetchDetails": "No se pudieron obtener los detalles",
|
||||
"failedPrepareOffline": "Error al preparar para offline",
|
||||
"failedRefreshOffline": "Error al actualizar copia offline",
|
||||
"preparingTrackOffline": "Preparando \"{title}\" para offline",
|
||||
"preparingTracksOffline": "Preparando {count} pistas de \"{album}\" para offline",
|
||||
"refreshingTrackOffline": "Actualizando \"{title}\" para offline",
|
||||
"refreshingAlbumOffline": "Actualizando todas las pistas de \"{album}\" para offline",
|
||||
"failedLoadAlbumRefresh": "Error al cargar álbum para actualizar offline",
|
||||
"playingTracksNext": "Reproduciendo {count} pistas a continuación",
|
||||
"addedTracksToQueue": "{count} pistas agregadas a la cola",
|
||||
"failedPreviousTrack": "Error al ir a pista anterior",
|
||||
"failedNextTrack": "Error al ir a siguiente pista",
|
||||
"tracksAddedToPlaylist": "Pista(s) agregada(s) a la playlist",
|
||||
"playlistCreated": "Playlist creada",
|
||||
"offlineModeStarted": "Iniciado en modo offline",
|
||||
"welcomeUser": "¡Bienvenido, {name}!",
|
||||
"restoredTrack": "Restaurado: {title}",
|
||||
"logoutSuccess": "Sesión cerrada exitosamente",
|
||||
"failedLogout": "Error al cerrar sesión",
|
||||
"localTrackSearch": "Pista local - busca el artista en Búsqueda",
|
||||
"queuedPlayNext": "En cola para reproducir siguiente",
|
||||
"failedQueueTrack": "Error al agregar pista a la cola",
|
||||
"addedToQueue": "Agregado a la cola",
|
||||
"offlineModeActivated": "Modo offline activado",
|
||||
"backOnline": "De vuelta en línea",
|
||||
"syncedPlaylist": "Playlist \"{name}\" sincronizada",
|
||||
"failedSyncPlaylist": "Error al sincronizar playlist \"{name}\": {error}",
|
||||
"playbackError": "Error de reproducción: {error}",
|
||||
"trackUnavailable": "\"{title}\" ya no está disponible",
|
||||
"playing": "Reproduciendo: {title}",
|
||||
"failedCopyLink": "Error al copiar enlace",
|
||||
"failedShareQobuz": "Error al compartir enlace de Qobuz: {error}",
|
||||
"playlistCreatedOffline": "Playlist creada offline - se sincronizará cuando vuelvas a estar en línea",
|
||||
"playlistImported": "Playlist importada",
|
||||
"playlistImportFailed": "Error al importar playlist",
|
||||
"enterSearchTerms": "Ingresa título del álbum o artista para buscar",
|
||||
"searchFailed": "Error en búsqueda: {error}",
|
||||
"appliedMetadata": "Metadatos aplicados de {provider}",
|
||||
"rateLimited": "Límite de consultas. Por favor espera un momento e intenta de nuevo.",
|
||||
"failedFetchMetadata": "Error al obtener metadatos: {error}",
|
||||
"failedOpenBrowser": "Error al abrir navegador",
|
||||
"albumMetadataSaved": "Metadatos del álbum guardados",
|
||||
"removedMissingFiles": "{count} pistas con archivos faltantes eliminadas",
|
||||
"noMissingFilesFound": "No se encontraron archivos faltantes",
|
||||
"failedCleanupMissing": "Error al limpiar archivos faltantes",
|
||||
"metadataRefreshed": "Metadatos actualizados desde archivos",
|
||||
"failedSaveTray": "Error al guardar configuración de bandeja",
|
||||
"failedSaveAutoplay": "Error al guardar preferencia de reproducción automática",
|
||||
"failedSaveIconVisibility": "Error al guardar preferencia de visibilidad de iconos",
|
||||
"failedRepairOffline": "Error al reparar biblioteca offline: {error}",
|
||||
"failedOpenCacheFolder": "Error al abrir carpeta de caché"
|
||||
},
|
||||
"offline": {
|
||||
"title": "Modo Offline",
|
||||
"message": "Conecta a internet para acceder al contenido de Qobuz™.",
|
||||
|
||||
@@ -280,7 +280,7 @@ export function setCustomGreeting(text: string | null): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get greeting text for display
|
||||
* Get greeting text for display (legacy - use getGreetingInfo for i18n support)
|
||||
*/
|
||||
export function getGreetingText(userName: string): string {
|
||||
if (settings.greeting.customText) {
|
||||
@@ -290,7 +290,45 @@ export function getGreetingText(userName: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time-based greeting
|
||||
* Get greeting info for i18n-aware display
|
||||
* Returns either custom text or a translation key with params
|
||||
*/
|
||||
export function getGreetingInfo(userName: string): { type: 'custom'; text: string } | { type: 'key'; key: string; name: string } {
|
||||
const firstName = getFirstName(userName);
|
||||
|
||||
if (settings.greeting.customText) {
|
||||
return {
|
||||
type: 'custom',
|
||||
text: settings.greeting.customText.replace('{name}', firstName)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'key',
|
||||
key: getTimeBasedGreetingKey(),
|
||||
name: firstName
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time-based greeting translation key
|
||||
*/
|
||||
export function getTimeBasedGreetingKey(): string {
|
||||
const hour = new Date().getHours();
|
||||
|
||||
if (hour >= 5 && hour < 12) {
|
||||
return 'homeSettings.goodMorning';
|
||||
} else if (hour >= 12 && hour < 17) {
|
||||
return 'homeSettings.goodAfternoon';
|
||||
} else if (hour >= 17 && hour < 21) {
|
||||
return 'homeSettings.goodEvening';
|
||||
} else {
|
||||
return 'homeSettings.goodNight';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time-based greeting (legacy)
|
||||
*/
|
||||
function getTimeBasedGreeting(userName: string): string {
|
||||
const hour = new Date().getHours();
|
||||
|
||||
+91
-84
@@ -225,6 +225,9 @@
|
||||
shareSonglinkTrack
|
||||
} from '$lib/services/trackActions';
|
||||
|
||||
// Internationalization
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
// App bootstrap
|
||||
import { bootstrapApp } from '$lib/app/bootstrap';
|
||||
|
||||
@@ -558,7 +561,7 @@
|
||||
|
||||
async function handleAlbumClick(albumId: string) {
|
||||
try {
|
||||
showToast('Loading album...', 'info');
|
||||
showToast($t('toast.loadingAlbum'), 'info');
|
||||
const album = await invoke<QobuzAlbum>('get_album', { albumId });
|
||||
console.log('Album details:', album);
|
||||
|
||||
@@ -574,7 +577,7 @@
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load album:', err);
|
||||
showToast('Failed to load album', 'error');
|
||||
showToast($t('toast.failedLoadAlbum'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,7 +639,7 @@
|
||||
|
||||
async function handleArtistClick(artistId: number) {
|
||||
try {
|
||||
showToast('Loading artist...', 'info');
|
||||
showToast($t('toast.loadingArtist'), 'info');
|
||||
const artist = await invoke<QobuzArtist>('get_artist_detail', { artistId });
|
||||
console.log('Artist details:', artist);
|
||||
|
||||
@@ -645,7 +648,7 @@
|
||||
hideToast();
|
||||
} catch (err) {
|
||||
console.error('Failed to load artist:', err);
|
||||
showToast('Failed to load artist', 'error');
|
||||
showToast($t('toast.failedLoadArtist'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +665,7 @@
|
||||
* - Weak (1), None (0): Show Informational Modal
|
||||
*/
|
||||
async function handleMusicianClick(name: string, role: string) {
|
||||
showToast(`Looking up ${name}...`, 'info');
|
||||
showToast($t('toast.lookingUp', { values: { name } }), 'info');
|
||||
try {
|
||||
const musician = await invoke<ResolvedMusician>('resolve_musician', { name, role });
|
||||
console.log('Resolved musician:', musician);
|
||||
@@ -693,7 +696,7 @@
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to resolve musician:', err);
|
||||
showToast('Could not look up musician info', 'error');
|
||||
showToast($t('toast.failedLookupMusician'), 'error');
|
||||
// Fallback: open modal with basic info
|
||||
musicianModalData = {
|
||||
name,
|
||||
@@ -798,7 +801,7 @@
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ContextNav] Navigation failed:', err);
|
||||
showToast('Failed to navigate to source', 'error');
|
||||
showToast($t('toast.failedNavigateSource'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -833,7 +836,7 @@
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to load more artist albums:', err);
|
||||
showToast('Failed to load more albums', 'error');
|
||||
showToast($t('toast.failedLoadMoreAlbums'), 'error');
|
||||
} finally {
|
||||
isArtistAlbumsLoading = false;
|
||||
}
|
||||
@@ -858,7 +861,7 @@
|
||||
return convertQobuzAlbum(album);
|
||||
} catch (err) {
|
||||
console.error('Failed to load album:', err);
|
||||
showToast('Failed to load album', 'error');
|
||||
showToast($t('toast.failedLoadAlbum'), 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -928,7 +931,7 @@
|
||||
artist_id: t.artistId ?? album.artistId
|
||||
});
|
||||
}
|
||||
showToast(`Playing ${album.tracks.length} tracks next`, 'success');
|
||||
showToast($t('toast.playingTracksNext', { values: { count: album.tracks.length } }), 'success');
|
||||
}
|
||||
|
||||
async function queueAlbumLaterById(albumId: string) {
|
||||
@@ -953,24 +956,24 @@
|
||||
|
||||
const success = await addTracksToQueue(queueTracks);
|
||||
if (success) {
|
||||
showToast(`Added ${queueTracks.length} tracks to queue`, 'success');
|
||||
showToast($t('toast.addedTracksToQueue', { values: { count: queueTracks.length } }), 'success');
|
||||
} else {
|
||||
showToast('Failed to add to queue', 'error');
|
||||
showToast($t('toast.failedAddToQueue'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function shareAlbumQobuzLinkById(albumId: string) {
|
||||
const url = `https://play.qobuz.com/album/${albumId}`;
|
||||
writeText(url);
|
||||
showToast('Album link copied to clipboard', 'success');
|
||||
showToast($t('toast.albumLinkCopied'), 'success');
|
||||
}
|
||||
|
||||
async function shareAlbumSonglinkById(albumId: string) {
|
||||
try {
|
||||
showToast('Fetching Album.link...', 'info');
|
||||
showToast($t('toast.fetchingAlbumLink'), 'info');
|
||||
const album = await fetchAlbumDetail(albumId);
|
||||
if (!album) {
|
||||
showToast('Could not fetch album details', 'error');
|
||||
showToast($t('toast.couldNotFetchDetails'), 'error');
|
||||
return;
|
||||
}
|
||||
const response = await invoke<{ pageUrl: string }>('share_album_songlink', {
|
||||
@@ -980,10 +983,10 @@
|
||||
artist: album.artist
|
||||
});
|
||||
writeText(response.pageUrl);
|
||||
showToast('Album.link copied to clipboard', 'success');
|
||||
showToast($t('toast.albumLinkCopiedSonglink'), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to get Album.link:', err);
|
||||
showToast(`Album.link error: ${err}`, 'error');
|
||||
showToast($t('toast.albumLinkError', { values: { error: String(err) } }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -997,11 +1000,11 @@
|
||||
});
|
||||
|
||||
if (tracksToDownload.length === 0) {
|
||||
showToast('All tracks already available offline', 'info');
|
||||
showToast($t('toast.allTracksOffline'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(`Preparing ${tracksToDownload.length} tracks from "${album.title}" for offline`, 'info');
|
||||
showToast($t('toast.preparingTracksOffline', { values: { count: tracksToDownload.length, album: album.title } }), 'info');
|
||||
|
||||
for (const track of tracksToDownload) {
|
||||
try {
|
||||
@@ -1055,7 +1058,7 @@
|
||||
return playlist;
|
||||
} catch (err) {
|
||||
console.error('Failed to load playlist:', err);
|
||||
showToast('Failed to load playlist', 'error');
|
||||
showToast($t('toast.failedLoadPlaylist'), 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1063,7 +1066,7 @@
|
||||
async function playPlaylistById(playlistId: number) {
|
||||
const playlist = await fetchPlaylistData(playlistId);
|
||||
if (!playlist?.tracks?.items?.length) {
|
||||
showToast('Playlist has no tracks', 'info');
|
||||
showToast($t('toast.playlistNoTracks'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1111,7 +1114,7 @@
|
||||
async function queuePlaylistNextById(playlistId: number) {
|
||||
const playlist = await fetchPlaylistData(playlistId);
|
||||
if (!playlist?.tracks?.items?.length) {
|
||||
showToast('Playlist has no tracks', 'info');
|
||||
showToast($t('toast.playlistNoTracks'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1134,13 +1137,13 @@
|
||||
artist_id: t.performer?.id
|
||||
});
|
||||
}
|
||||
showToast(`Playing ${tracks.length} tracks next`, 'success');
|
||||
showToast($t('toast.playingTracksNext', { values: { count: tracks.length } }), 'success');
|
||||
}
|
||||
|
||||
async function queuePlaylistLaterById(playlistId: number) {
|
||||
const playlist = await fetchPlaylistData(playlistId);
|
||||
if (!playlist?.tracks?.items?.length) {
|
||||
showToast('Playlist has no tracks', 'info');
|
||||
showToast($t('toast.playlistNoTracks'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1162,39 +1165,39 @@
|
||||
|
||||
const success = await addTracksToQueue(queueTracks);
|
||||
if (success) {
|
||||
showToast(`Added ${queueTracks.length} tracks to queue`, 'success');
|
||||
showToast($t('toast.addedTracksToQueue', { values: { count: queueTracks.length } }), 'success');
|
||||
} else {
|
||||
showToast('Failed to add to queue', 'error');
|
||||
showToast($t('toast.failedAddToQueue'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPlaylistToLibraryById(playlistId: number) {
|
||||
try {
|
||||
showToast('Copying playlist to library...', 'info');
|
||||
showToast($t('toast.copyingToLibrary'), 'info');
|
||||
await invoke('subscribe_playlist', { playlistId });
|
||||
sidebarRef?.refreshPlaylists();
|
||||
showToast('Playlist copied to library', 'success');
|
||||
showToast($t('toast.playlistCopied'), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy playlist:', err);
|
||||
showToast(`Failed to copy playlist: ${err}`, 'error');
|
||||
showToast($t('toast.failedCopyPlaylist', { values: { error: String(err) } }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function sharePlaylistQobuzLinkById(playlistId: number) {
|
||||
const url = `https://play.qobuz.com/playlist/${playlistId}`;
|
||||
writeText(url);
|
||||
showToast('Playlist link copied to clipboard', 'success');
|
||||
showToast($t('toast.playlistLinkCopied'), 'success');
|
||||
}
|
||||
|
||||
async function removePlaylistFavoriteById(playlistId: number) {
|
||||
try {
|
||||
await invoke('playlist_set_favorite', { playlistId, favorite: false });
|
||||
showToast('Playlist removed from favorites', 'success');
|
||||
showToast($t('toast.playlistRemovedFavorites'), 'success');
|
||||
sidebarRef?.refreshPlaylists();
|
||||
sidebarRef?.refreshPlaylistSettings();
|
||||
} catch (err) {
|
||||
console.error('Failed to remove playlist favorite:', err);
|
||||
showToast(`Failed to remove from favorites: ${err}`, 'error');
|
||||
showToast($t('toast.failedRemoveFavorites', { values: { error: String(err) } }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1320,7 +1323,7 @@
|
||||
async function toggleShuffle() {
|
||||
const result = await queueToggleShuffle();
|
||||
if (result.success) {
|
||||
showToast(result.enabled ? 'Shuffle enabled' : 'Shuffle disabled', 'info');
|
||||
showToast(result.enabled ? $t('toast.shuffleEnabled') : $t('toast.shuffleDisabled'), 'info');
|
||||
// Persist playback mode to session
|
||||
saveSessionPlaybackMode(result.enabled, repeatMode);
|
||||
}
|
||||
@@ -1329,7 +1332,11 @@
|
||||
async function toggleRepeat() {
|
||||
const result = await queueToggleRepeat();
|
||||
if (result.success) {
|
||||
const messages: Record<RepeatMode, string> = { off: 'Repeat off', all: 'Repeat all', one: 'Repeat one' };
|
||||
const messages: Record<RepeatMode, string> = {
|
||||
off: $t('toast.repeatOff'),
|
||||
all: $t('toast.repeatAll'),
|
||||
one: $t('toast.repeatOne')
|
||||
};
|
||||
showToast(messages[result.mode], 'info');
|
||||
// Persist playback mode to session
|
||||
saveSessionPlaybackMode(isShuffle, result.mode);
|
||||
@@ -1342,9 +1349,9 @@
|
||||
const result = await toggleTrackFavorite(currentTrack.id, isFavorite);
|
||||
if (result.success) {
|
||||
setIsFavorite(result.isFavorite);
|
||||
showToast(result.isFavorite ? 'Added to favorites' : 'Removed from favorites', 'success');
|
||||
showToast(result.isFavorite ? $t('toast.addedToFavorites') : $t('toast.removedFromFavorites'), 'success');
|
||||
} else {
|
||||
showToast('Failed to update favorites', 'error');
|
||||
showToast($t('toast.failedUpdateFavorites'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1376,7 +1383,7 @@
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to go to previous track:', err);
|
||||
showToast('Failed to go to previous track', 'error');
|
||||
showToast($t('toast.failedPreviousTrack'), 'error');
|
||||
} finally {
|
||||
setIsSkipping(false);
|
||||
}
|
||||
@@ -1395,11 +1402,11 @@
|
||||
// No next track - stop playback
|
||||
await stopPlayback();
|
||||
setIsPlaying(false);
|
||||
showToast('Queue ended', 'info');
|
||||
showToast($t('toast.queueEnded'), 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to go to next track:', err);
|
||||
showToast('Failed to go to next track', 'error');
|
||||
showToast($t('toast.failedNextTrack'), 'error');
|
||||
} finally {
|
||||
setIsSkipping(false);
|
||||
}
|
||||
@@ -1436,7 +1443,7 @@
|
||||
if (skippedIds.has(track.id)) {
|
||||
// Already tried this track, stop to prevent infinite loop
|
||||
setQueueEnded(true);
|
||||
showToast('No available tracks in queue (offline mode)', 'info');
|
||||
showToast($t('toast.noAvailableTracks'), 'info');
|
||||
return;
|
||||
}
|
||||
skippedIds.add(track.id);
|
||||
@@ -1488,7 +1495,7 @@
|
||||
// Find the index in the queue
|
||||
const queueState = await getBackendQueueState();
|
||||
if (!queueState) {
|
||||
showToast('Failed to play track', 'error');
|
||||
showToast($t('toast.failedPlayTrack'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1514,7 +1521,7 @@
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to play queue track:', err);
|
||||
showToast('Failed to play track', 'error');
|
||||
showToast($t('toast.failedPlayTrack'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1522,9 +1529,9 @@
|
||||
async function handleClearQueue() {
|
||||
const success = await clearQueue();
|
||||
if (success) {
|
||||
showToast('Queue cleared', 'info');
|
||||
showToast($t('toast.queueCleared'), 'info');
|
||||
} else {
|
||||
showToast('Failed to clear queue', 'error');
|
||||
showToast($t('toast.failedClearQueue'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1532,7 +1539,7 @@
|
||||
async function handleQueueReorder(fromIndex: number, toIndex: number) {
|
||||
const success = await moveQueueTrack(fromIndex, toIndex);
|
||||
if (!success) {
|
||||
showToast('Failed to reorder queue', 'error');
|
||||
showToast($t('toast.failedReorderQueue'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1555,7 +1562,7 @@
|
||||
}
|
||||
|
||||
if (trackIds.length === 0) {
|
||||
showToast('Queue is empty', 'info');
|
||||
showToast($t('toast.queueEmpty'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1574,7 +1581,7 @@
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
showToast(infinitePlayEnabled ? 'Infinite play enabled' : 'Infinite play disabled', 'info');
|
||||
showToast(infinitePlayEnabled ? $t('toast.infinitePlayEnabled') : $t('toast.infinitePlayDisabled'), 'info');
|
||||
}
|
||||
|
||||
// Play a track from history
|
||||
@@ -1583,7 +1590,7 @@
|
||||
// Get the full queue state to find the track in history
|
||||
const queueState = await getBackendQueueState();
|
||||
if (!queueState) {
|
||||
showToast('Failed to play track', 'error');
|
||||
showToast($t('toast.failedPlayTrack'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1591,7 +1598,7 @@
|
||||
const numericId = parseInt(trackId, 10);
|
||||
const historyTrack = queueState.history.find(t => t.id === numericId);
|
||||
if (!historyTrack) {
|
||||
showToast('Track not found in history', 'error');
|
||||
showToast($t('toast.trackNotInHistory'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1609,7 +1616,7 @@
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to play history track:', err);
|
||||
showToast('Failed to play track', 'error');
|
||||
showToast($t('toast.failedPlayTrack'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1655,7 +1662,7 @@
|
||||
|
||||
// Play from random track (queue will be shuffled by backend)
|
||||
await handleAlbumTrackPlay(randomTrack);
|
||||
showToast('Shuffle play enabled', 'info');
|
||||
showToast($t('toast.shuffleEnabled'), 'info');
|
||||
}
|
||||
|
||||
// Add all album tracks next in queue (after current track)
|
||||
@@ -1689,7 +1696,7 @@
|
||||
artist_id: t.artistId ?? selectedAlbum?.artistId
|
||||
});
|
||||
}
|
||||
showToast(`Playing ${playableTracks.length} tracks next`, 'success');
|
||||
showToast($t('toast.playingTracksNext', { values: { count: playableTracks.length } }), 'success');
|
||||
}
|
||||
|
||||
// Add all album tracks to end of queue
|
||||
@@ -1722,9 +1729,9 @@
|
||||
|
||||
const success = await addTracksToQueue(queueTracks);
|
||||
if (success) {
|
||||
showToast(`Added ${queueTracks.length} tracks to queue`, 'success');
|
||||
showToast($t('toast.addedTracksToQueue', { values: { count: queueTracks.length } }), 'success');
|
||||
} else {
|
||||
showToast('Failed to add to queue', 'error');
|
||||
showToast($t('toast.failedAddToQueue'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1732,7 +1739,7 @@
|
||||
const album = await fetchAlbumDetail(albumId);
|
||||
addAlbumToPlaylist(album);
|
||||
}
|
||||
|
||||
|
||||
function addAlbumToPlaylist(album: AlbumDetail | null) {
|
||||
if (!album?.tracks?.length) return;
|
||||
const trackIds = album.tracks.map(t => t.id);
|
||||
@@ -1744,14 +1751,14 @@
|
||||
if (!selectedAlbum?.id) return;
|
||||
const url = `https://play.qobuz.com/album/${selectedAlbum.id}`;
|
||||
writeText(url);
|
||||
showToast('Album link copied to clipboard', 'success');
|
||||
showToast($t('toast.albumLinkCopied'), 'success');
|
||||
}
|
||||
|
||||
// Share album via album.link
|
||||
async function shareAlbumSonglink() {
|
||||
if (!selectedAlbum?.id) return;
|
||||
try {
|
||||
showToast('Fetching Album.link...', 'info');
|
||||
showToast($t('toast.fetchingAlbumLink'), 'info');
|
||||
const response = await invoke<{ pageUrl: string }>('share_album_songlink', {
|
||||
upc: selectedAlbum.upc || null,
|
||||
albumId: selectedAlbum.id,
|
||||
@@ -1759,10 +1766,10 @@
|
||||
artist: selectedAlbum.artist
|
||||
});
|
||||
writeText(response.pageUrl);
|
||||
showToast('Album.link copied to clipboard', 'success');
|
||||
showToast($t('toast.albumLinkCopiedSonglink'), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to get Album.link:', err);
|
||||
showToast(`Album.link error: ${err}`, 'error');
|
||||
showToast($t('toast.albumLinkError', { values: { error: String(err) } }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1788,20 +1795,20 @@
|
||||
bitDepth: track.bitDepth,
|
||||
sampleRate: track.samplingRate,
|
||||
});
|
||||
showToast(`Preparing "${track.title}" for offline`, 'info');
|
||||
showToast($t('toast.preparingTrackOffline', { values: { title: track.title } }), 'info');
|
||||
} catch (err) {
|
||||
console.error('Failed to cache for offline:', err);
|
||||
showToast('Failed to prepare for offline', 'error');
|
||||
showToast($t('toast.failedPrepareOffline'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTrackRemoveDownload(trackId: number) {
|
||||
try {
|
||||
await removeCachedTrack(trackId);
|
||||
showToast('Removed from offline library', 'info');
|
||||
showToast($t('toast.removedFromOffline'), 'info');
|
||||
} catch (err) {
|
||||
console.error('Failed to remove from offline:', err);
|
||||
showToast('Failed to remove from offline library', 'error');
|
||||
showToast($t('toast.failedRemoveOffline'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1810,7 +1817,7 @@
|
||||
await openTrackFolder(trackId);
|
||||
} catch (err) {
|
||||
console.error('Failed to open folder:', err);
|
||||
showToast('Failed to open folder', 'error');
|
||||
showToast($t('toast.failedOpenFolder'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1828,10 +1835,10 @@
|
||||
bitDepth: 'bitDepth' in track ? track.bitDepth : undefined,
|
||||
sampleRate: 'samplingRate' in track ? track.samplingRate : undefined,
|
||||
});
|
||||
showToast(`Refreshing "${track.title}" for offline`, 'info');
|
||||
showToast($t('toast.refreshingTrackOffline', { values: { title: track.title } }), 'info');
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh offline copy:', err);
|
||||
showToast('Failed to refresh offline copy', 'error');
|
||||
showToast($t('toast.failedRefreshOffline'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1848,11 +1855,11 @@
|
||||
});
|
||||
|
||||
if (tracksToDownload.length === 0) {
|
||||
showToast('All tracks already available offline', 'info');
|
||||
showToast($t('toast.allTracksOffline'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(`Preparing ${tracksToDownload.length} tracks from "${selectedAlbum.title}" for offline`, 'info');
|
||||
showToast($t('toast.preparingTracksOffline', { values: { count: tracksToDownload.length, album: selectedAlbum.title } }), 'info');
|
||||
|
||||
for (const track of tracksToDownload) {
|
||||
try {
|
||||
@@ -1875,19 +1882,19 @@
|
||||
|
||||
async function handleOpenAlbumFolder() {
|
||||
if (!selectedAlbum) return;
|
||||
|
||||
|
||||
try {
|
||||
await openAlbumFolder(selectedAlbum.id);
|
||||
} catch (err) {
|
||||
console.error('Failed to open album folder:', err);
|
||||
showToast('Failed to open album folder', 'error');
|
||||
showToast($t('toast.failedOpenAlbumFolder'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReDownloadAlbum() {
|
||||
if (!selectedAlbum) return;
|
||||
|
||||
showToast(`Refreshing all tracks from "${selectedAlbum.title}" for offline`, 'info');
|
||||
showToast($t('toast.refreshingAlbumOffline', { values: { album: selectedAlbum.title } }), 'info');
|
||||
|
||||
for (const track of selectedAlbum.tracks) {
|
||||
try {
|
||||
@@ -1913,7 +1920,7 @@
|
||||
await openAlbumFolder(albumId);
|
||||
} catch (err) {
|
||||
console.error('Failed to open album folder:', err);
|
||||
showToast('Failed to open album folder', 'error');
|
||||
showToast($t('toast.failedOpenAlbumFolder'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1921,11 +1928,11 @@
|
||||
try {
|
||||
const album = await invoke<QobuzAlbum>('get_album', { albumId });
|
||||
if (!album || !album.tracks || album.tracks.data.length === 0) {
|
||||
showToast('Failed to load album for offline refresh', 'error');
|
||||
showToast($t('toast.failedLoadAlbumRefresh'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(`Refreshing all tracks from "${album.title}" for offline`, 'info');
|
||||
showToast($t('toast.refreshingAlbumOffline', { values: { album: album.title } }), 'info');
|
||||
|
||||
for (const track of album.tracks.data) {
|
||||
try {
|
||||
@@ -1946,7 +1953,7 @@
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load album:', err);
|
||||
showToast('Failed to load album for offline refresh', 'error');
|
||||
showToast($t('toast.failedLoadAlbumRefresh'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1974,10 +1981,10 @@
|
||||
bitDepth: track.bitDepth,
|
||||
sampleRate: track.samplingRate,
|
||||
});
|
||||
showToast(`Preparing "${track.title}" for offline`, 'info');
|
||||
showToast($t('toast.preparingTrackOffline', { values: { title: track.title } }), 'info');
|
||||
} catch (err) {
|
||||
console.error('Failed to prepare for offline:', err);
|
||||
showToast('Failed to prepare for offline', 'error');
|
||||
showToast($t('toast.failedPrepareOffline'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2090,9 +2097,9 @@
|
||||
const isLocal = playlistModalTracksAreLocal;
|
||||
|
||||
if (playlistModalMode === 'addTrack') {
|
||||
showToast('Track(s) added to playlist', 'success');
|
||||
showToast($t('toast.tracksAddedToPlaylist'), 'success');
|
||||
} else {
|
||||
showToast('Playlist created', 'success');
|
||||
showToast($t('toast.playlistCreated'), 'success');
|
||||
}
|
||||
sidebarRef?.refreshPlaylists();
|
||||
sidebarRef?.refreshPlaylistSettings();
|
||||
@@ -2143,12 +2150,12 @@
|
||||
subscription: 'Local Library Only'
|
||||
});
|
||||
navigateTo('library');
|
||||
showToast('Started in offline mode', 'info');
|
||||
showToast($t('toast.offlineModeStarted'), 'info');
|
||||
}
|
||||
|
||||
async function handleLoginSuccess(info: UserInfo) {
|
||||
setLoggedIn(info);
|
||||
showToast(`Welcome, ${info.userName}!`, 'success');
|
||||
showToast($t('toast.welcomeUser', { values: { name: info.userName } }), 'success');
|
||||
|
||||
// Load favorites now that login is confirmed (sync with Qobuz)
|
||||
loadFavorites(); // Track favorites
|
||||
@@ -2276,10 +2283,10 @@
|
||||
setLoggedOut();
|
||||
currentTrack = null;
|
||||
isPlaying = false;
|
||||
showToast('Logged out successfully', 'info');
|
||||
showToast($t('toast.logoutSuccess'), 'info');
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
showToast('Failed to logout', 'error');
|
||||
showToast($t('toast.failedLogout'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3266,7 +3273,7 @@
|
||||
lyricsActive={lyricsSidebarVisible}
|
||||
onArtistClick={() => {
|
||||
if (currentTrack?.isLocal) {
|
||||
showToast('Local track - search for artist in Search', 'info');
|
||||
showToast($t('toast.localTrackSearch'), 'info');
|
||||
} else if (currentTrack?.artistId) {
|
||||
handleArtistClick(currentTrack.artistId);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user