mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
d34de09673
- 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>
106 lines
3.4 KiB
TypeScript
106 lines
3.4 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
export interface DeviceSyncSource {
|
|
type: 'album' | 'playlist' | 'artist';
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
interface DeviceSyncState {
|
|
targetDir: string | null;
|
|
filenameTemplate: string;
|
|
sources: DeviceSyncSource[]; // persistent device content list
|
|
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;
|
|
addSource: (source: DeviceSyncSource) => void;
|
|
removeSource: (id: string) => void;
|
|
clearSources: () => void;
|
|
toggleChecked: (id: string) => void;
|
|
setCheckedIds: (ids: string[]) => 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>()(
|
|
persist(
|
|
(set) => ({
|
|
targetDir: null,
|
|
filenameTemplate: '{artist}/{album}/{track_number} - {title}',
|
|
sources: [],
|
|
checkedIds: [],
|
|
pendingDeletion: [],
|
|
deviceFilePaths: [],
|
|
scanning: false,
|
|
|
|
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),
|
|
pendingDeletion: s.pendingDeletion.filter((x) => x !== id),
|
|
})),
|
|
|
|
clearSources: () => set({ sources: [], checkedIds: [], pendingDeletion: [] }),
|
|
|
|
toggleChecked: (id) =>
|
|
set((s) => ({
|
|
checkedIds: s.checkedIds.includes(id)
|
|
? s.checkedIds.filter((x) => x !== id)
|
|
: [...s.checkedIds, id],
|
|
})),
|
|
|
|
setCheckedIds: (ids) => set({ checkedIds: ids }),
|
|
|
|
markForDeletion: (ids) =>
|
|
set((s) => ({
|
|
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',
|
|
partialize: (s) => ({
|
|
targetDir: s.targetDir,
|
|
filenameTemplate: s.filenameTemplate,
|
|
sources: s.sources,
|
|
}),
|
|
}
|
|
)
|
|
);
|