refactor(deviceSync): co-locate device sync feature into features/deviceSync

This commit is contained in:
Psychotoxical
2026-06-29 22:55:35 +02:00
parent 236acc33dd
commit ea3e3f1adf
23 changed files with 78 additions and 68 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ const Settings = lazy(() => import('../pages/Settings'));
const Statistics = lazy(() => import('@/features/stats/pages/Statistics'));
const Help = lazy(() => import('../pages/Help'));
const WhatsNew = lazy(() => import('../pages/WhatsNew'));
const DeviceSync = lazy(() => import('../pages/DeviceSync'));
const DeviceSync = lazy(() => import('@/features/deviceSync/pages/DeviceSync'));
const OfflineLibrary = lazy(() => import('../pages/OfflineLibrary'));
const LabelAlbums = lazy(() => import('../pages/LabelAlbums'));
const SearchBrowsePage = lazy(() => import('../pages/SearchBrowsePage'));
+1 -1
View File
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
import { usePlayerStore } from '../store/playerStore';
import { useOfflineJobStore } from '../store/offlineJobStore';
import { clearOfflinePinTasks } from '../utils/offline/offlinePinQueue';
import { useDeviceSyncJobStore } from '../store/deviceSyncJobStore';
import { useDeviceSyncJobStore } from '@/features/deviceSync';
import { useAuthStore } from '../store/authStore';
import { useSidebarStore } from '../store/sidebarStore';
import { useLocation } from 'react-router-dom';
@@ -6,10 +6,10 @@ import {
} from 'lucide-react';
import type {
SubsonicAlbum, SubsonicArtist, SubsonicPlaylist,
} from '../../api/subsonicTypes';
import type { DeviceSyncSource } from '../../store/deviceSyncStore';
import type { SourceTab } from '../../utils/deviceSync/deviceSyncHelpers';
import BrowserRow from './BrowserRow';
} from '@/api/subsonicTypes';
import type { DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
import type { SourceTab } from '@/features/deviceSync/utils/deviceSyncHelpers';
import BrowserRow from '@/features/deviceSync/components/BrowserRow';
interface Props {
activeTab: SourceTab;
@@ -5,9 +5,9 @@ import {
AlertCircle, CheckCircle2, Clock, HardDriveUpload, Loader2,
Trash2, Undo2,
} from 'lucide-react';
import { useDeviceSyncJobStore } from '../../store/deviceSyncJobStore';
import type { DeviceSyncSource } from '../../store/deviceSyncStore';
import type { SyncStatus } from '../../utils/deviceSync/deviceSyncHelpers';
import { useDeviceSyncJobStore } from '@/features/deviceSync/store/deviceSyncJobStore';
import type { DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
import type { SyncStatus } from '@/features/deviceSync/utils/deviceSyncHelpers';
interface Props {
sources: DeviceSyncSource[];
@@ -4,9 +4,9 @@ import {
AlertCircle, FolderOpen, HardDriveUpload, RefreshCw, Usb,
} from 'lucide-react';
import CustomSelect from '@/ui/CustomSelect';
import type { RemovableDrive } from '../../utils/deviceSync/deviceSyncHelpers';
import { formatBytes } from '../../utils/deviceSync/deviceSyncHelpers';
import type { DeviceSyncSource } from '../../store/deviceSyncStore';
import type { RemovableDrive } from '@/features/deviceSync/utils/deviceSyncHelpers';
import { formatBytes } from '@/features/deviceSync/utils/deviceSyncHelpers';
import type { DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
interface Props {
targetDir: string | null;
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { AlertCircle, CheckCircle2, Loader2 } from 'lucide-react';
import type {
MigrationPair, MigrationPhase, MigrationResult,
} from '../../utils/deviceSync/runDeviceSyncMigration';
} from '@/features/deviceSync/utils/runDeviceSyncMigration';
interface Props {
migrationPhase: MigrationPhase;
@@ -1,8 +1,8 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertCircle, Loader2 } from 'lucide-react';
import type { SyncDelta } from '../../utils/deviceSync/runDeviceSyncExecution';
import { formatMb } from '../../utils/format/formatBytes';
import type { SyncDelta } from '@/features/deviceSync/utils/runDeviceSyncExecution';
import { formatMb } from '@/utils/format/formatBytes';
interface Props {
preSyncOpen: boolean;
@@ -1,12 +1,12 @@
import { useCallback, useEffect, useState } from 'react';
import { getPlaylists } from '../api/subsonicPlaylists';
import { getArtists, getArtist } from '../api/subsonicArtists';
import { getAlbumList } from '../api/subsonicLibrary';
import { search as searchSubsonic } from '../api/subsonicSearch';
import { getPlaylists } from '@/api/subsonicPlaylists';
import { getArtists, getArtist } from '@/api/subsonicArtists';
import { getAlbumList } from '@/api/subsonicLibrary';
import { search as searchSubsonic } from '@/api/subsonicSearch';
import type {
SubsonicAlbum, SubsonicArtist, SubsonicPlaylist,
} from '../api/subsonicTypes';
import type { SourceTab } from '../utils/deviceSync/deviceSyncHelpers';
} from '@/api/subsonicTypes';
import type { SourceTab } from '@/features/deviceSync/utils/deviceSyncHelpers';
export interface DeviceSyncBrowserResult {
playlists: SubsonicPlaylist[];
@@ -1,8 +1,8 @@
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/ui/toast';
import { useDeviceSyncStore, type DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
import { showToast } from '@/utils/ui/toast';
export interface DeviceSyncDeviceScanResult {
scanDevice: () => Promise<void>;
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import type { RemovableDrive } from '../utils/deviceSync/deviceSyncHelpers';
import type { RemovableDrive } from '@/features/deviceSync/utils/deviceSyncHelpers';
export interface DeviceSyncDrivesResult {
drives: RemovableDrive[];
@@ -2,11 +2,11 @@ import { useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import type { TFunction } from 'i18next';
import { useDeviceSyncJobStore } from '../store/deviceSyncJobStore';
import { useDeviceSyncStore } from '../store/deviceSyncStore';
import { showToast } from '../utils/ui/toast';
import { trackToSyncInfo } from '../utils/deviceSync/deviceSyncHelpers';
import { fetchTracksForSource } from '../utils/playback/fetchTracksForSource';
import { useDeviceSyncJobStore } from '@/features/deviceSync/store/deviceSyncJobStore';
import { useDeviceSyncStore } from '@/features/deviceSync/store/deviceSyncStore';
import { showToast } from '@/utils/ui/toast';
import { trackToSyncInfo } from '@/features/deviceSync/utils/deviceSyncHelpers';
import { fetchTracksForSource } from '@/utils/playback/fetchTracksForSource';
export function useDeviceSyncJobEvents(
t: TFunction,
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { fetchTracksForSource } from '../utils/playback/fetchTracksForSource';
import { trackToSyncInfo, type SyncStatus } from '../utils/deviceSync/deviceSyncHelpers';
import type { DeviceSyncSource } from '../store/deviceSyncStore';
import { fetchTracksForSource } from '@/utils/playback/fetchTracksForSource';
import { trackToSyncInfo, type SyncStatus } from '@/features/deviceSync/utils/deviceSyncHelpers';
import type { DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
export interface DeviceSyncSourceStatusesResult {
sourcePathsMap: Map<string, string[]>;
+10
View File
@@ -0,0 +1,10 @@
/**
* Device Sync feature — copies/transcodes library tracks onto external devices
* (USB drives, players). The `DeviceSync` page is lazy-loaded by the router via
* its deep path, so it is intentionally not re-exported here.
*
* Note: `audioListenerSetup/eqDeviceSync` is NOT part of this feature — it is the
* audio-core EQ-per-output-device profile sync, kept in the audio core.
*/
export { useDeviceSyncJobStore } from './store/deviceSyncJobStore';
export type { DeviceSyncSource } from './store/deviceSyncStore';
@@ -1,33 +1,33 @@
import type { SubsonicSong } from '../api/subsonicTypes';
import type { SubsonicSong } from '@/api/subsonicTypes';
import React, { useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDeviceSyncStore, DeviceSyncSource } from '../store/deviceSyncStore';
import { useDeviceSyncJobStore } from '../store/deviceSyncJobStore';
import { useDeviceSyncStore, DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
import { useDeviceSyncJobStore } from '@/features/deviceSync/store/deviceSyncJobStore';
import {
type SourceTab,
} from '../utils/deviceSync/deviceSyncHelpers';
import { useDeviceSyncDrives } from '../hooks/useDeviceSyncDrives';
import { useDeviceSyncSourceStatuses } from '../hooks/useDeviceSyncSourceStatuses';
import { useDeviceSyncBrowser } from '../hooks/useDeviceSyncBrowser';
import { useDeviceSyncDeviceScan } from '../hooks/useDeviceSyncDeviceScan';
import { useDeviceSyncJobEvents } from '../hooks/useDeviceSyncJobEvents';
} from '@/features/deviceSync/utils/deviceSyncHelpers';
import { useDeviceSyncDrives } from '@/features/deviceSync/hooks/useDeviceSyncDrives';
import { useDeviceSyncSourceStatuses } from '@/features/deviceSync/hooks/useDeviceSyncSourceStatuses';
import { useDeviceSyncBrowser } from '@/features/deviceSync/hooks/useDeviceSyncBrowser';
import { useDeviceSyncDeviceScan } from '@/features/deviceSync/hooks/useDeviceSyncDeviceScan';
import { useDeviceSyncJobEvents } from '@/features/deviceSync/hooks/useDeviceSyncJobEvents';
import {
runDeviceSyncMigrationPreview,
runDeviceSyncMigrationExecute,
type MigrationPhase, type MigrationPair, type MigrationResult,
} from '../utils/deviceSync/runDeviceSyncMigration';
} from '@/features/deviceSync/utils/runDeviceSyncMigration';
import {
runDeviceSyncSummaryPrompt,
runDeviceSyncExecute,
type SyncDelta,
} from '../utils/deviceSync/runDeviceSyncExecution';
import { runDeviceSyncChooseFolder } from '../utils/deviceSync/runDeviceSyncChooseFolder';
import DeviceSyncHeader from '../components/deviceSync/DeviceSyncHeader';
import DeviceSyncPreSyncModal from '../components/deviceSync/DeviceSyncPreSyncModal';
import DeviceSyncMigrationModal from '../components/deviceSync/DeviceSyncMigrationModal';
import DeviceSyncBrowserPanel from '../components/deviceSync/DeviceSyncBrowserPanel';
import DeviceSyncDevicePanel from '../components/deviceSync/DeviceSyncDevicePanel';
} from '@/features/deviceSync/utils/runDeviceSyncExecution';
import { runDeviceSyncChooseFolder } from '@/features/deviceSync/utils/runDeviceSyncChooseFolder';
import DeviceSyncHeader from '@/features/deviceSync/components/DeviceSyncHeader';
import DeviceSyncPreSyncModal from '@/features/deviceSync/components/DeviceSyncPreSyncModal';
import DeviceSyncMigrationModal from '@/features/deviceSync/components/DeviceSyncMigrationModal';
import DeviceSyncBrowserPanel from '@/features/deviceSync/components/DeviceSyncBrowserPanel';
import DeviceSyncDevicePanel from '@/features/deviceSync/components/DeviceSyncDevicePanel';
// ─── component ───────────────────────────────────────────────────────────────
@@ -1,4 +1,4 @@
import type { SubsonicSong } from '../../api/subsonicTypes';
import type { SubsonicSong } from '@/api/subsonicTypes';
export type SourceTab = 'playlists' | 'albums' | 'artists';
@@ -1,4 +1,4 @@
import { IS_WINDOWS } from '../platform';
import { IS_WINDOWS } from '@/utils/platform';
// Same sanitize rules the Rust side uses (`sanitize_path_component`): strip
// Windows-illegal chars and control chars, trim leading/trailing dots + spaces.
@@ -1,8 +1,8 @@
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 '../ui/toast';
import { useDeviceSyncStore, type DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
import { showToast } from '@/utils/ui/toast';
export interface RunDeviceSyncChooseFolderDeps {
t: TFunction;
@@ -1,12 +1,12 @@
import type { TFunction } from 'i18next';
import { invoke } from '@tauri-apps/api/core';
import { buildDownloadUrl } from '../../api/subsonicStreamUrl';
import type { SubsonicSong } from '../../api/subsonicTypes';
import { useDeviceSyncStore, type DeviceSyncSource } from '../../store/deviceSyncStore';
import { useDeviceSyncJobStore } from '../../store/deviceSyncJobStore';
import { showToast } from '../ui/toast';
import { trackToSyncInfo, uuid } from './deviceSyncHelpers';
import { fetchTracksForSource } from '../playback/fetchTracksForSource';
import { buildDownloadUrl } from '@/api/subsonicStreamUrl';
import type { SubsonicSong } from '@/api/subsonicTypes';
import { useDeviceSyncStore, type DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
import { useDeviceSyncJobStore } from '@/features/deviceSync/store/deviceSyncJobStore';
import { showToast } from '@/utils/ui/toast';
import { trackToSyncInfo, uuid } from '@/features/deviceSync/utils/deviceSyncHelpers';
import { fetchTracksForSource } from '@/utils/playback/fetchTracksForSource';
export interface SyncDelta {
addBytes: number;
@@ -37,7 +37,7 @@ export async function runDeviceSyncSummaryPrompt(deps: RunDeviceSyncSummaryDeps)
setPreSyncOpen(true);
try {
const { getClient } = await import('../../api/subsonicClient');
const { getClient } = await import('@/api/subsonicClient');
const { baseUrl, params } = getClient();
const payload = await invoke<SyncDelta>('calculate_sync_payload', {
sources,
@@ -1,11 +1,11 @@
import type React from 'react';
import { invoke } from '@tauri-apps/api/core';
import type { DeviceSyncSource } from '../../store/deviceSyncStore';
import type { SubsonicSong } from '../../api/subsonicTypes';
import { applyLegacyTemplate } from './deviceSyncLegacyTemplate';
import { trackToSyncInfo } from './deviceSyncHelpers';
import { fetchTracksForSource } from '../playback/fetchTracksForSource';
import { IS_WINDOWS } from '../platform';
import type { DeviceSyncSource } from '@/features/deviceSync/store/deviceSyncStore';
import type { SubsonicSong } from '@/api/subsonicTypes';
import { applyLegacyTemplate } from '@/features/deviceSync/utils/deviceSyncLegacyTemplate';
import { trackToSyncInfo } from '@/features/deviceSync/utils/deviceSyncHelpers';
import { fetchTracksForSource } from '@/utils/playback/fetchTracksForSource';
import { IS_WINDOWS } from '@/utils/platform';
export type MigrationPhase = 'closed' | 'loading' | 'preview' | 'executing' | 'done' | 'nothing';
+1 -1
View File
@@ -1,5 +1,5 @@
import type { SubsonicSong } from '../../api/subsonicTypes';
import type { DeviceSyncSource } from '../../store/deviceSyncStore';
import type { DeviceSyncSource } from '@/features/deviceSync';
import {
resolveAlbum,
resolveArtist,