Wire settings: account info, themes, and cache management

- Account section now shows real user name and subscription
- Logout button connected to logout handler
- Theme switching with persistence (Dark/Light/OLED Black/Warm)
- Theme loads on app startup from localStorage
- Cache stats display (size, max, track count)
- Clear cache functionality with disabled state
This commit is contained in:
vicrodh committed 2026-01-09 11:10:41 -06:00
1 parent ef8feebc45
commit 9b8e1352eb
5 files changed
+163 -11

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
//! Cache management commands
use tauri::State;
use crate::cache::CacheStats;
use crate::AppState;
/// Get cache statistics
#[tauri::command]
pub fn get_cache_stats(state: State<'_, AppState>) -> CacheStats {
log::info!("Command: get_cache_stats");
state.audio_cache.stats()
}
/// Clear the audio cache
#[tauri::command]
pub fn clear_cache(state: State<'_, AppState>) -> Result<(), String> {
log::info!("Command: clear_cache");
state.audio_cache.clear();
Ok(())
}
+2
View File
@@ -3,6 +3,7 @@
//! Exposes backend functionality to the frontend via IPC //! Exposes backend functionality to the frontend via IPC
pub mod auth; pub mod auth;
pub mod cache;
pub mod favorites; pub mod favorites;
pub mod notification; pub mod notification;
pub mod playback; pub mod playback;
@@ -11,6 +12,7 @@ pub mod queue;
pub mod search; pub mod search;
pub use auth::*; pub use auth::*;
pub use cache::*;
pub use favorites::*; pub use favorites::*;
pub use notification::*; pub use notification::*;
pub use playback::*; pub use playback::*;
+3
View File
@@ -127,6 +127,9 @@ pub fn run() {
// Notification commands // Notification commands
commands::show_track_notification, commands::show_track_notification,
commands::show_notification, commands::show_notification,
// Cache commands
commands::get_cache_stats,
commands::clear_cache,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
+125 -10
View File
@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { ArrowLeft } from 'lucide-svelte'; import { ArrowLeft } from 'lucide-svelte';
import Toggle from '../Toggle.svelte'; import Toggle from '../Toggle.svelte';
import Dropdown from '../Dropdown.svelte'; import Dropdown from '../Dropdown.svelte';
@@ -7,9 +9,39 @@
interface Props { interface Props {
onBack?: () => void; onBack?: () => void;
onLogout?: () => void;
userName?: string;
userEmail?: string;
subscription?: string;
} }
let { onBack }: Props = $props(); interface CacheStats {
cached_tracks: number;
current_size_bytes: number;
max_size_bytes: number;
fetching_count: number;
}
let { onBack, onLogout, userName = 'User', userEmail = '', subscription = 'Qobuz' }: Props = $props();
// Cache state
let cacheStats = $state<CacheStats | null>(null);
let isClearing = $state(false);
// Theme mapping: display name -> data-theme value
const themeMap: Record<string, string> = {
'Dark': '',
'Light': 'light',
'OLED Black': 'oled',
'Warm': 'warm'
};
const themeReverseMap: Record<string, string> = {
'': 'Dark',
'light': 'Light',
'oled': 'OLED Black',
'warm': 'Warm'
};
// Audio settings // Audio settings
let streamingQuality = $state('Hi-Res'); let streamingQuality = $state('Hi-Res');
@@ -30,6 +62,60 @@
// Integrations // Integrations
let lastfmConnected = $state(false); let lastfmConnected = $state(false);
let scrobbling = $state(false); let scrobbling = $state(false);
// Load saved settings on mount
onMount(() => {
const savedTheme = localStorage.getItem('qbz-theme') || '';
theme = themeReverseMap[savedTheme] || 'Dark';
applyTheme(savedTheme);
// Load cache stats
loadCacheStats();
});
async function loadCacheStats() {
try {
cacheStats = await invoke<CacheStats>('get_cache_stats');
} catch (err) {
console.error('Failed to load cache stats:', err);
}
}
async function handleClearCache() {
if (isClearing) return;
isClearing = true;
try {
await invoke('clear_cache');
await loadCacheStats();
} catch (err) {
console.error('Failed to clear cache:', err);
} finally {
isClearing = false;
}
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
}
function applyTheme(themeValue: string) {
if (themeValue) {
document.documentElement.setAttribute('data-theme', themeValue);
} else {
document.documentElement.removeAttribute('data-theme');
}
}
function handleThemeChange(newTheme: string) {
theme = newTheme;
const themeValue = themeMap[newTheme] || '';
applyTheme(themeValue);
localStorage.setItem('qbz-theme', themeValue);
}
</script> </script>
<div class="settings-view"> <div class="settings-view">
@@ -48,13 +134,15 @@
<section class="section"> <section class="section">
<h3 class="section-title">Account</h3> <h3 class="section-title">Account</h3>
<div class="account-card"> <div class="account-card">
<div class="avatar">U</div> <div class="avatar">{userName.charAt(0).toUpperCase()}</div>
<div class="account-info"> <div class="account-info">
<div class="username">Username</div> <div class="username">{userName}</div>
<div class="email">email@example.com</div> {#if userEmail}
<div class="subscription">Studio Premier</div> <div class="email">{userEmail}</div>
{/if}
<div class="subscription">{subscription}</div>
</div> </div>
<button class="logout-btn">Logout</button> <button class="logout-btn" onclick={onLogout}>Logout</button>
</div> </div>
</section> </section>
@@ -128,7 +216,7 @@
<Dropdown <Dropdown
value={theme} value={theme}
options={['Dark', 'Light', 'OLED Black', 'Warm']} options={['Dark', 'Light', 'OLED Black', 'Warm']}
onchange={(v) => (theme = v)} onchange={handleThemeChange}
/> />
</div> </div>
<div class="setting-row last"> <div class="setting-row last">
@@ -163,11 +251,33 @@
<h3 class="section-title">Storage</h3> <h3 class="section-title">Storage</h3>
<div class="setting-row"> <div class="setting-row">
<span class="setting-label">Cache Size</span> <span class="setting-label">Cache Size</span>
<span class="setting-value">2.4 GB</span> <span class="setting-value">
{#if cacheStats}
{formatBytes(cacheStats.current_size_bytes)} / {formatBytes(cacheStats.max_size_bytes)}
{:else}
Loading...
{/if}
</span>
</div>
<div class="setting-row">
<span class="setting-label">Cached Tracks</span>
<span class="setting-value">
{#if cacheStats}
{cacheStats.cached_tracks} tracks
{:else}
-
{/if}
</span>
</div> </div>
<div class="setting-row last"> <div class="setting-row last">
<span class="setting-label">Clear Cache</span> <span class="setting-label">Clear Cache</span>
<button class="clear-btn">Clear</button> <button
class="clear-btn"
onclick={handleClearCache}
disabled={isClearing || !cacheStats || cacheStats.current_size_bytes === 0}
>
{isClearing ? 'Clearing...' : 'Clear'}
</button>
</div> </div>
</section> </section>
</div> </div>
@@ -345,7 +455,12 @@
transition: background-color 150ms ease; transition: background-color 150ms ease;
} }
.clear-btn:hover { .clear-btn:hover:not(:disabled) {
background-color: rgba(255, 107, 107, 0.1); background-color: rgba(255, 107, 107, 0.1);
} }
.clear-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style> </style>
+12 -1
View File
@@ -839,6 +839,12 @@
}); });
onMount(() => { onMount(() => {
// Load saved theme
const savedTheme = localStorage.getItem('qbz-theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
document.addEventListener('keydown', handleKeydown); document.addEventListener('keydown', handleKeydown);
return () => document.removeEventListener('keydown', handleKeydown); return () => document.removeEventListener('keydown', handleKeydown);
}); });
@@ -888,7 +894,12 @@
onTrackPlay={handleTrackPlay} onTrackPlay={handleTrackPlay}
/> />
{:else if activeView === 'settings'} {:else if activeView === 'settings'}
<SettingsView onBack={goBack} /> <SettingsView
onBack={goBack}
onLogout={handleLogout}
userName={userInfo?.userName}
subscription={userInfo?.subscription}
/>
{:else if activeView === 'album' && selectedAlbum} {:else if activeView === 'album' && selectedAlbum}
<AlbumDetailView <AlbumDetailView
album={selectedAlbum} album={selectedAlbum}