mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
feat(device-sync): USB/SD card sync page (WIP)
Adds a new Device Sync page for transferring music from Navidrome to USB drives or SD cards. Sidebar entry is hidden by default. - Four new Tauri commands: sync_track_to_device, compute_sync_paths, list_device_dir_files, delete_device_file - Filename template engine with cross-platform path sanitization - 4-concurrent-worker download, live progress via device:sync:progress event - Persistent source list (albums/playlists/artists) with checkbox deletion - Expandable artist tree in browser panel (per-album selection) - i18n: DE + EN complete; other locales have stub keys Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export interface DeviceSyncSource {
|
||||
type: 'album' | 'playlist' | 'artist';
|
||||
id: string;
|
||||
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;
|
||||
|
||||
setTargetDir: (dir: string | null) => void;
|
||||
setFilenameTemplate: (t: string) => void;
|
||||
addSource: (source: DeviceSyncSource) => void;
|
||||
removeSource: (id: string) => void;
|
||||
clearSources: () => void;
|
||||
toggleChecked: (id: string) => void;
|
||||
setCheckedIds: (ids: string[]) => void;
|
||||
setActiveJob: (job: DeviceSyncJob | null) => void;
|
||||
updateJob: (update: Partial<DeviceSyncJob>) => void;
|
||||
}
|
||||
|
||||
export const useDeviceSyncStore = create<DeviceSyncState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
targetDir: null,
|
||||
filenameTemplate: '{artist}/{album}/{track_number} - {title}',
|
||||
sources: [],
|
||||
checkedIds: [],
|
||||
activeJob: null,
|
||||
|
||||
setTargetDir: (dir) => set({ targetDir: dir }),
|
||||
setFilenameTemplate: (t) => set({ filenameTemplate: t }),
|
||||
|
||||
addSource: (source) =>
|
||||
set((s) => ({
|
||||
sources: s.sources.some((x) => x.id === source.id)
|
||||
? s.sources
|
||||
: [...s.sources, source],
|
||||
})),
|
||||
|
||||
removeSource: (id) =>
|
||||
set((s) => ({
|
||||
sources: s.sources.filter((x) => x.id !== id),
|
||||
checkedIds: s.checkedIds.filter((x) => x !== id),
|
||||
})),
|
||||
|
||||
clearSources: () => set({ sources: [], checkedIds: [] }),
|
||||
|
||||
toggleChecked: (id) =>
|
||||
set((s) => ({
|
||||
checkedIds: s.checkedIds.includes(id)
|
||||
? s.checkedIds.filter((x) => x !== id)
|
||||
: [...s.checkedIds, id],
|
||||
})),
|
||||
|
||||
setCheckedIds: (ids) => set({ checkedIds: ids }),
|
||||
|
||||
setActiveJob: (job) => set({ activeJob: job }),
|
||||
|
||||
updateJob: (update) =>
|
||||
set((s) => ({
|
||||
activeJob: s.activeJob ? { ...s.activeJob, ...update } : null,
|
||||
})),
|
||||
}),
|
||||
{
|
||||
name: 'psysonic_device_sync',
|
||||
partialize: (s) => ({
|
||||
targetDir: s.targetDir,
|
||||
filenameTemplate: s.filenameTemplate,
|
||||
sources: s.sources,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -20,6 +20,7 @@ export const DEFAULT_SIDEBAR_ITEMS: SidebarItemConfig[] = [
|
||||
{ id: 'mostPlayed', visible: true },
|
||||
{ id: 'radio', visible: true },
|
||||
{ id: 'folderBrowser', visible: false },
|
||||
{ id: 'deviceSync', visible: false },
|
||||
{ id: 'statistics', visible: true },
|
||||
{ id: 'help', visible: true },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user