refactor(device-sync): G.76 — extract browser + device-scan + job-events hooks + choose-folder util (cluster) (#643)

Four-cut cluster pulling the remaining lifecycle code out of
DeviceSync.tsx. 928 → 638 LOC (−290).

useDeviceSyncBrowser — playlists / randomAlbums / artists state +
their three loaders + the tab-switch useEffect that lazy-loads on
first visit + the 300 ms debounced album-search useEffect + the
expandedArtistIds / artistAlbumsMap / loadingArtistIds state with
toggleArtistExpand. Takes activeTab + search + a resetSearch
callback (so the tab-switch effect can clear the search input the
page still owns).

useDeviceSyncDeviceScan — scanDevice useCallback + the on-mount
useEffect + the auto-import-manifest useEffect (with the
manifestImportedRef gate so it only fires once per drive plug-in)
+ the clean-on-unplug useEffect that clears deviceFilePaths and
resets the import flag.

useDeviceSyncJobEvents — the device:sync:progress and
device:sync:complete event listeners. Complete handler dispatches
the toast, writes the manifest, generates per-playlist m3u8 files
(through fetchTracksForSource + trackToSyncInfo), and triggers
scanDevice. Cancelled state is preserved by re-calling
useDeviceSyncJobStore.cancel() after complete().

runDeviceSyncChooseFolder — the openDialog → setTargetDir →
optional manifest auto-import → scanDevice timer flow.

DeviceSync drops every direct import that those hooks now own
(getPlaylists, getArtists, getArtist, getAlbumList, searchSubsonic,
listen, openDialog, useEffect, useRef, SubsonicPlaylist /
SubsonicArtist / SubsonicAlbum type imports). Pure code move
otherwise — no behaviour change.
This commit is contained in:
Frank Stellmacher
2026-05-13 15:17:19 +02:00
committed by GitHub
parent c7946f26b6
commit 6fcf2259f6
5 changed files with 297 additions and 207 deletions
+34
View File
@@ -0,0 +1,34 @@
import { invoke } from '@tauri-apps/api/core';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
import type { TFunction } from 'i18next';
import { useDeviceSyncStore, type DeviceSyncSource } from '../store/deviceSyncStore';
import { showToast } from './toast';
export interface RunDeviceSyncChooseFolderDeps {
t: TFunction;
setTargetDir: (dir: string) => void;
scanDevice: () => Promise<void>;
}
export async function runDeviceSyncChooseFolder(deps: RunDeviceSyncChooseFolderDeps): Promise<void> {
const { t, setTargetDir, scanDevice } = deps;
const sel = await openDialog({ directory: true, multiple: false, title: t('deviceSync.chooseFolder') });
if (!sel) return;
const dir = sel as string;
setTargetDir(dir);
// If the device has a psysonic-sync.json, always import it — replacing any
// sources from a previous device so switching sticks works correctly.
try {
const manifest = await invoke<{ version: number; sources: DeviceSyncSource[] } | null>(
'read_device_manifest', { destDir: dir }
);
if (manifest?.sources?.length) {
useDeviceSyncStore.getState().clearSources();
manifest.sources.forEach(s => useDeviceSyncStore.getState().addSource(s));
showToast(t('deviceSync.manifestImported', { count: manifest.sources.length }), 4000, 'info');
}
} catch { /* no manifest, that's fine */ }
// Trigger a device scan after folder change
setTimeout(() => scanDevice(), 100);
}