diff --git a/app/src/renderer/src/lib/api.ts b/app/src/renderer/src/lib/api.ts index 804b8c7..5c73e30 100644 --- a/app/src/renderer/src/lib/api.ts +++ b/app/src/renderer/src/lib/api.ts @@ -79,10 +79,10 @@ export const api = { usbLibraries: (volumeId: string) => call('usb.libraries', { volumeId }), usbSelection: (volumeId: string) => call<{ playlistIds: string[] }>('usb.selection', { volumeId }), usbTracks: (volumeId: string, playlistId: string | null) => call('usb.tracks', { volumeId, playlistId }), - usbSyncReview: (volumeId: string, playlistIds: string[]) => - call('usb.syncReview', { volumeId, playlistIds }), - usbSync: (volumeId: string, playlistIds: string[], review?: SyncDecisions) => - call<{ volumeId: string; tracks: number; playlists: number }>('usb.sync', { volumeId, playlistIds, review }), + usbSyncReview: (volumeId: string, playlistIds: string[], convert?: ConvertPreset) => + call('usb.syncReview', { volumeId, playlistIds, convert }), + usbSync: (volumeId: string, playlistIds: string[], review?: SyncDecisions, convert?: ConvertPreset) => + call<{ volumeId: string; tracks: number; playlists: number }>('usb.sync', { volumeId, playlistIds, review, convert }), usbRecoveryStatus: (volumeId: string) => call<{ required: boolean }>('usb.recoveryStatus', { volumeId }), usbRecover: (volumeId: string) => call<{ recovered: boolean }>('usb.recover', { volumeId }), usbImportPreview: (volumeId: string, folder: string | null, policy: PlaylistPolicy = 'skipExisting') => diff --git a/app/src/renderer/src/lib/components/SyncDialog.svelte b/app/src/renderer/src/lib/components/SyncDialog.svelte index 0a68a41..908e878 100644 --- a/app/src/renderer/src/lib/components/SyncDialog.svelte +++ b/app/src/renderer/src/lib/components/SyncDialog.svelte @@ -4,10 +4,12 @@ import { SvelteSet } from 'svelte/reactivity' import { api } from '$lib/api' import { getApp } from '$lib/context' + import { loadPresets } from '$lib/convertPresetStore' + import { FORMAT_NAMES, presetKey, presetLabel } from '$lib/convertPresets' import { suspendModal } from '$lib/modal' import { buildTree, type TreeNode } from '$lib/library/tree' import type { DeviceLibraryView, Volume } from '$lib/types' - import type { SyncInspection, SyncResolution } from '../../../../preload/engine' + import type { SyncInspection, SyncPlan, SyncResolution } from '../../../../preload/engine' const COLLECTION_ID = 'collection' @@ -50,6 +52,10 @@ let resolutions = $state>>({}) let reviewing = $state(false) const showInformation = new SvelteSet() + const presets = loadPresets() + /** The chosen format by preset key; empty copies every track as it is. */ + let convertKey = $state('') + const convert = $derived(presets.find((preset) => presetKey(preset) === convertKey)) const selectedIds = $derived.by(() => { const present = new Set(library.devices.map((item) => item.id)) @@ -110,7 +116,7 @@ return userTicks ?? baseSelection() }) - const reviewKey = $derived(JSON.stringify([selectedIds, [...checked].sort(), refreshToken])) + const reviewKey = $derived(JSON.stringify([selectedIds, [...checked].sort(), refreshToken, convertKey])) const reviewBlocked = $derived(selectedIds.some((id) => inspections[id]?.diagnostics.some((item) => item.severity === 'blocked'))) const reviewUnresolved = $derived(selectedIds.some((id) => { const inspection = inspections[id] @@ -121,6 +127,7 @@ const key = reviewKey const ids = untrack(() => [...selectedIds]) const playlistIds = untrack(() => [...checked].sort()) + const preset = untrack(() => convert) if (!ids.length || pending || !playlistIds.length) { inspections = {} inspectionErrors = {} @@ -135,7 +142,7 @@ resolutions = {} void Promise.all(ids.map(async (id) => { try { - return [id, await api.usbSyncReview(id, playlistIds)] as const + return [id, await api.usbSyncReview(id, playlistIds, preset)] as const } catch (reason) { const message = reason instanceof Error ? reason.message : 'Could not inspect USB changes' return [id, message] as const @@ -364,6 +371,15 @@ return `${value.toFixed(digits)} ${units[unit]}` } + function planChanges(plan: SyncPlan): string { + if (plan.upToDate) return 'Up to date · no changes' + const parts: string[] = [] + if (plan.convert) parts.push(`${plan.convert} to convert`) + if (plan.copy) parts.push(`${plan.copy} to copy`) + if (plan.remove) parts.push(`${plan.remove} to remove`) + return parts.length ? parts.join(' · ') : 'No audio to copy or convert' + } + function phaseLabel(): string { const device = activeDevice ? library.devices.find((item) => item.id === activeDevice) : null const prefix = device && selectedIds.length > 1 ? `${device.name} · ` : '' @@ -375,6 +391,12 @@ const total = progress.total ?? 0 return prefix + (total ? `Copying ${done + 1} of ${total}` : 'Copying audio') } + if (progress.phase === 'convert') { + const done = progress.done ?? 0 + const total = progress.total ?? 0 + const name = progress.name ? ` · ${progress.name}` : '' + return prefix + (total ? `Converting ${done + 1} of ${total}${name}` : 'Converting audio') + } if (progress.phase === 'import') { const done = progress.done ?? 0 const total = progress.total ?? 0 @@ -407,7 +429,7 @@ const review = inspection && Object.keys(selectedResolutions).length ? { token: inspection.token, resolutions: { ...selectedResolutions } } : undefined - const ok = await library.syncTo(id, playlistIds, review) + const ok = await library.syncTo(id, playlistIds, review, convert) if (!ok) { error = library.syncError refreshToken += 1 @@ -789,6 +811,7 @@ {inspection.plan.tracks} tracks · {inspection.plan.playlists} playlists {inspection.plan.estimatedPeakBytes == null ? ' · space estimate unavailable' : ` · ${inspection.plan.approximate ? 'approximately ' : ''}up to ${formatBytes(inspection.plan.estimatedPeakBytes)}`}

+

{planChanges(inspection.plan)}

{/if} {#if informationCount > 0}