mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
feat(device-sync): overhaul Device Sync page
- New two-panel layout with live album search (search3, 300ms debounce) and 10 random albums when search is empty — replaces full paginated load - Pre-sync summary modal: shows files to add/delete, net change, available space; Proceed button disabled when space is insufficient - calculate_sync_payload now filters already-synced tracks (path exists on device) so add_bytes/add_count reflect the true delta - Space check accounts for pending deletions: addBytes > availableBytes + delBytes - Separate deviceSyncJobStore for ephemeral job progress state - Removable drive detection + auto-poll every 5 s via sysinfo - Desired-state diff: de-selecting a synced source stages it for deletion instead of silently removing it - Status badges (Synced / Pending / Deletion) with matching row highlights - Live-Search badge (⚡) and Zufallsalben section label (⇌) in album browser - Status summary row height aligned with search field (52 px) - i18n: all new keys added to all 8 locales (en/de/fr/nl/nb/zh/ru/es) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface DeviceSyncJobState {
|
||||
jobId: string | null;
|
||||
total: number;
|
||||
done: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
status: 'idle' | 'running' | 'done' | 'cancelled';
|
||||
|
||||
startSync: (jobId: string, total: number) => void;
|
||||
updateProgress: (done: number, skipped: number, failed: number) => void;
|
||||
complete: (done: number, skipped: number, failed: number) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const useDeviceSyncJobStore = create<DeviceSyncJobState>()((set) => ({
|
||||
jobId: null,
|
||||
total: 0,
|
||||
done: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
status: 'idle',
|
||||
|
||||
startSync: (jobId, total) =>
|
||||
set({ jobId, total, done: 0, skipped: 0, failed: 0, status: 'running' }),
|
||||
|
||||
updateProgress: (done, skipped, failed) =>
|
||||
set({ done, skipped, failed }),
|
||||
|
||||
complete: (done, skipped, failed) =>
|
||||
set({ done, skipped, failed, status: 'done' }),
|
||||
|
||||
reset: () =>
|
||||
set({ jobId: null, total: 0, done: 0, skipped: 0, failed: 0, status: 'idle' }),
|
||||
}));
|
||||
@@ -7,21 +7,14 @@ export interface DeviceSyncSource {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DeviceSyncJob {
|
||||
id: string;
|
||||
total: number;
|
||||
done: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
status: 'running' | 'done' | 'cancelled';
|
||||
}
|
||||
|
||||
interface DeviceSyncState {
|
||||
targetDir: string | null;
|
||||
filenameTemplate: string;
|
||||
sources: DeviceSyncSource[]; // persistent device content list
|
||||
checkedIds: string[]; // currently checked for deletion (not persisted)
|
||||
activeJob: DeviceSyncJob | null;
|
||||
checkedIds: string[]; // currently checked for bulk actions (not persisted)
|
||||
pendingDeletion: string[]; // source IDs marked for deletion (not persisted)
|
||||
deviceFilePaths: string[]; // actual file paths found on the device (not persisted)
|
||||
scanning: boolean; // true while scanning the device
|
||||
|
||||
setTargetDir: (dir: string | null) => void;
|
||||
setFilenameTemplate: (t: string) => void;
|
||||
@@ -30,8 +23,12 @@ interface DeviceSyncState {
|
||||
clearSources: () => void;
|
||||
toggleChecked: (id: string) => void;
|
||||
setCheckedIds: (ids: string[]) => void;
|
||||
setActiveJob: (job: DeviceSyncJob | null) => void;
|
||||
updateJob: (update: Partial<DeviceSyncJob>) => void;
|
||||
markForDeletion: (ids: string[]) => void;
|
||||
unmarkDeletion: (id: string) => void;
|
||||
clearPendingDeletion: () => void;
|
||||
removeSources: (ids: string[]) => void;
|
||||
setDeviceFilePaths: (paths: string[]) => void;
|
||||
setScanning: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export const useDeviceSyncStore = create<DeviceSyncState>()(
|
||||
@@ -41,7 +38,9 @@ export const useDeviceSyncStore = create<DeviceSyncState>()(
|
||||
filenameTemplate: '{artist}/{album}/{track_number} - {title}',
|
||||
sources: [],
|
||||
checkedIds: [],
|
||||
activeJob: null,
|
||||
pendingDeletion: [],
|
||||
deviceFilePaths: [],
|
||||
scanning: false,
|
||||
|
||||
setTargetDir: (dir) => set({ targetDir: dir }),
|
||||
setFilenameTemplate: (t) => set({ filenameTemplate: t }),
|
||||
@@ -57,9 +56,10 @@ export const useDeviceSyncStore = create<DeviceSyncState>()(
|
||||
set((s) => ({
|
||||
sources: s.sources.filter((x) => x.id !== id),
|
||||
checkedIds: s.checkedIds.filter((x) => x !== id),
|
||||
pendingDeletion: s.pendingDeletion.filter((x) => x !== id),
|
||||
})),
|
||||
|
||||
clearSources: () => set({ sources: [], checkedIds: [] }),
|
||||
clearSources: () => set({ sources: [], checkedIds: [], pendingDeletion: [] }),
|
||||
|
||||
toggleChecked: (id) =>
|
||||
set((s) => ({
|
||||
@@ -70,12 +70,28 @@ export const useDeviceSyncStore = create<DeviceSyncState>()(
|
||||
|
||||
setCheckedIds: (ids) => set({ checkedIds: ids }),
|
||||
|
||||
setActiveJob: (job) => set({ activeJob: job }),
|
||||
|
||||
updateJob: (update) =>
|
||||
markForDeletion: (ids) =>
|
||||
set((s) => ({
|
||||
activeJob: s.activeJob ? { ...s.activeJob, ...update } : null,
|
||||
pendingDeletion: [...new Set([...s.pendingDeletion, ...ids])],
|
||||
checkedIds: s.checkedIds.filter((x) => !ids.includes(x)),
|
||||
})),
|
||||
|
||||
unmarkDeletion: (id) =>
|
||||
set((s) => ({
|
||||
pendingDeletion: s.pendingDeletion.filter((x) => x !== id),
|
||||
})),
|
||||
|
||||
clearPendingDeletion: () => set({ pendingDeletion: [] }),
|
||||
|
||||
removeSources: (ids) =>
|
||||
set((s) => ({
|
||||
sources: s.sources.filter((x) => !ids.includes(x.id)),
|
||||
checkedIds: s.checkedIds.filter((x) => !ids.includes(x)),
|
||||
pendingDeletion: s.pendingDeletion.filter((x) => !ids.includes(x)),
|
||||
})),
|
||||
|
||||
setDeviceFilePaths: (paths) => set({ deviceFilePaths: paths }),
|
||||
setScanning: (v) => set({ scanning: v }),
|
||||
}),
|
||||
{
|
||||
name: 'psysonic_device_sync',
|
||||
|
||||
Reference in New Issue
Block a user