feat: v1.19.0 — NSIS Installer, Tray Removed, Storage Warning, Theme Polish

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-03-27 23:56:35 +01:00
parent 936e548f40
commit b6fb66c46d
29 changed files with 1766 additions and 1142 deletions
+28 -6
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { invoke } from '@tauri-apps/api/core';
import { getAlbum, getArtist, getArtistInfo, setRating, buildCoverArtUrl, coverArtCacheKey, buildDownloadUrl, star, unstar, SubsonicSong, SubsonicAlbum } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
@@ -43,7 +44,7 @@ export default function AlbumDetail() {
const [downloadProgress, setDownloadProgress] = useState<number | null>(null);
const [isStarred, setIsStarred] = useState(false);
const [starredSongs, setStarredSongs] = useState<Set<string>>(new Set());
const [hoveredSongId, setHoveredSongId] = useState<string | null>(null);
const [offlineStorageFull, setOfflineStorageFull] = useState(false);
const { downloadAlbum, deleteAlbum } = useOfflineStore();
const offlineTracks = useOfflineStore(s => s.tracks);
@@ -207,10 +208,21 @@ export default function AlbumDetail() {
}
};
const handleCacheOffline = () => {
const handleCacheOffline = useCallback(async () => {
if (!album) return;
const maxBytes = auth.maxCacheMb * 1024 * 1024;
try {
const usedBytes = await invoke<number>('get_offline_cache_size');
if (usedBytes >= maxBytes) {
setOfflineStorageFull(true);
return;
}
} catch {
// If we can't check, proceed anyway
}
setOfflineStorageFull(false);
downloadAlbum(album.album.id, album.album.name, album.album.artist, album.album.coverArt, album.album.year, album.songs, serverId);
};
}, [album, auth.maxCacheMb, downloadAlbum, serverId]);
const handleRemoveOffline = () => {
if (!album) return;
@@ -251,14 +263,24 @@ export default function AlbumDetail() {
onCacheOffline={handleCacheOffline}
onRemoveOffline={handleRemoveOffline}
/>
{offlineStorageFull && (
<div className="offline-storage-full-banner" role="alert">
<span>{t('albumDetail.offlineStorageFull', { mb: auth.maxCacheMb })}</span>
<button className="btn btn-ghost" style={{ fontSize: 13 }} onClick={() => navigate('/offline')}>
{t('albumDetail.offlineStorageGoToLibrary')}
</button>
<button className="btn btn-ghost" style={{ fontSize: 13 }} onClick={() => navigate('/settings', { state: { tab: 'library' } })}>
{t('albumDetail.offlineStorageGoToSettings')}
</button>
<button className="offline-storage-full-dismiss" onClick={() => setOfflineStorageFull(false)} aria-label="Dismiss">×</button>
</div>
)}
<AlbumTrackList
songs={songs}
hasVariousArtists={hasVariousArtists}
currentTrack={currentTrack}
isPlaying={isPlaying}
hoveredSongId={hoveredSongId}
setHoveredSongId={setHoveredSongId}
ratings={ratings}
starredSongs={new Set([
...[...starredSongs].filter(id => starredOverrides[id] !== false),
+12 -4
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { ChevronDown, Rocket, Play, LibraryBig, Settings2, Radio, Wrench, Shuffle } from 'lucide-react';
import { ChevronDown, Rocket, Play, LibraryBig, Settings2, Radio, Wrench, Shuffle, WifiOff } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface FaqItem { q: string; a: string; }
@@ -65,7 +65,6 @@ export default function Help() {
items: [
{ q: t('help.q12'), a: t('help.a12') },
{ q: t('help.q13'), a: t('help.a13') },
{ q: t('help.q14'), a: t('help.a14') },
{ q: t('help.q15'), a: t('help.a15') },
{ q: t('help.q31'), a: t('help.a31') },
{ q: t('help.q32'), a: t('help.a32') },
@@ -88,6 +87,15 @@ export default function Help() {
{ q: t('help.q28'), a: t('help.a28') },
],
},
{
icon: <WifiOff size={18} />,
title: t('help.s8'),
items: [
{ q: t('help.q34'), a: t('help.a34') },
{ q: t('help.q35'), a: t('help.a35') },
{ q: t('help.q36'), a: t('help.a36') },
],
},
{
icon: <Wrench size={18} />,
title: t('help.s6'),
@@ -104,9 +112,9 @@ export default function Help() {
<div className="content-body animate-fade-in">
<h1 className="page-title" style={{ marginBottom: '2rem' }}>{t('help.title')}</h1>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '1.25rem', alignItems: 'start' }}>
<div style={{ columns: 2, columnGap: '1.25rem' }}>
{sections.map((section, si) => (
<section key={si} className="settings-section">
<section key={si} className="settings-section" style={{ breakInside: 'avoid', marginBottom: '1.25rem' }}>
<div className="settings-section-header">
{section.icon}
<h2>{section.title}</h2>
-13
View File
@@ -944,19 +944,6 @@ export default function Settings() {
<h2>{t('settings.behavior')}</h2>
</div>
<div className="settings-card">
<div className="settings-toggle-row">
<div>
<div style={{ fontWeight: 500 }}>{t('settings.trayTitle')}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.trayDesc')}</div>
</div>
<label className="toggle-switch" aria-label={t('settings.trayTitle')}>
<input type="checkbox" checked={auth.minimizeToTray} onChange={e => auth.setMinimizeToTray(e.target.checked)} id="tray-toggle" />
<span className="toggle-track" />
</label>
</div>
<div className="divider" />
<div className="settings-toggle-row">
<div>
<div style={{ fontWeight: 500 }}>{t('settings.downloadsTitle')}</div>