Files
psysonic/src/hooks/useDeviceSyncDeviceScan.ts
T
Frank Stellmacher 6fcf2259f6 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.
2026-05-13 15:17:19 +02:00

65 lines
2.2 KiB
TypeScript

import { useCallback, useEffect, useRef } from 'react';
import { invoke } from '@tauri-apps/api/core';
import type { TFunction } from 'i18next';
import { useDeviceSyncStore, type DeviceSyncSource } from '../store/deviceSyncStore';
import { showToast } from '../utils/toast';
export interface DeviceSyncDeviceScanResult {
scanDevice: () => Promise<void>;
}
export function useDeviceSyncDeviceScan(
targetDir: string | null,
sourcesLength: number,
driveDetected: boolean,
t: TFunction,
): DeviceSyncDeviceScanResult {
const setDeviceFilePaths = useDeviceSyncStore.getState().setDeviceFilePaths;
const setScanning = useDeviceSyncStore.getState().setScanning;
const scanDevice = useCallback(async () => {
if (!targetDir || sourcesLength === 0) {
setDeviceFilePaths([]);
return;
}
setScanning(true);
try {
const files = await invoke<string[]>('list_device_dir_files', { dir: targetDir });
setDeviceFilePaths(files);
} catch {
setDeviceFilePaths([]);
} finally {
setScanning(false);
}
}, [targetDir, sourcesLength, setDeviceFilePaths, setScanning]);
// Scan device on mount and when targetDir changes
useEffect(() => { scanDevice(); }, [scanDevice]);
// Auto-import manifest when page loads and drive is already connected
const manifestImportedRef = useRef(false);
useEffect(() => {
if (!targetDir || !driveDetected || manifestImportedRef.current) return;
manifestImportedRef.current = true;
invoke<{ version: number; sources: DeviceSyncSource[] } | null>(
'read_device_manifest', { destDir: targetDir }
).then(manifest => {
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(() => {});
}, [targetDir, driveDetected, t]);
// Clear device file list and reset import flag when stick is unplugged
useEffect(() => {
if (!driveDetected) {
setDeviceFilePaths([]);
manifestImportedRef.current = false;
}
}, [driveDetected, setDeviceFilePaths]);
return { scanDevice };
}