diff --git a/lib/format.ts b/lib/format.ts index 0906127..ad974ab 100644 --- a/lib/format.ts +++ b/lib/format.ts @@ -26,3 +26,8 @@ export function replaceSmartQuotes(value: string) { } return value.replace(/[‘’‚“”„]/g, (match) => replacements[match]) } + +export function getRemoteName(path?: string) { + if (!path?.includes(':')) return null // Return null for local paths + return path.split(':')[0] +} diff --git a/src/pages/Copy.tsx b/src/pages/Copy.tsx index 27a8e1f..146d9ff 100644 --- a/src/pages/Copy.tsx +++ b/src/pages/Copy.tsx @@ -4,9 +4,10 @@ import { message } from '@tauri-apps/plugin-dialog' import { AlertOctagonIcon, CopyIcon, FilterIcon, FoldersIcon, PlayIcon } from 'lucide-react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' +import { getRemoteName } from '../../lib/format' import { getCopyFlags, getFilterFlags, getGlobalFlags, startCopy } from '../../lib/rclone/api' import { usePersistedStore } from '../../lib/store' -import { getLoadingTray, getMainTray } from '../../lib/tray' +import {} from '../../lib/tray' import { openTrayWindow } from '../../lib/window' import OptionsSection from '../components/OptionsSection' import PathFinder from '../components/PathFinder' @@ -19,44 +20,63 @@ export default function Copy() { ) const [dest, setDest] = useState(undefined) + const [isStarted, setIsStarted] = useState(false) const [isLoading, setIsLoading] = useState(false) const [jsonError, setJsonError] = useState<'copy' | 'filter' | null>(null) + const [copyOptionsLocked, setCopyOptionsLocked] = useState(false) const [copyOptions, setCopyOptions] = useState>({}) const [copyOptionsJson, setCopyOptionsJson] = useState('{}') + const [filterOptionsLocked, setFilterOptionsLocked] = useState(false) const [filterOptions, setFilterOptions] = useState>({}) const [filterOptionsJson, setFilterOptionsJson] = useState('{}') const [globalOptions, setGlobalOptions] = useState([]) + // biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect useEffect(() => { const storeData = usePersistedStore.getState() - const remote = source?.split(':')[0] + const sourceRemote = getRemoteName(source) + const destRemote = getRemoteName(dest) - if (!remote) return + let mergedCopyDefaults = {} + let mergedFilterDefaults = {} - if (!(remote in storeData.remoteConfigList)) return + // Helper function to merge defaults from a remote + const mergeRemoteDefaults = (remote: string | null) => { + if (!remote || !(remote in storeData.remoteConfigList)) return - if ( - storeData.remoteConfigList[remote].copyDefaults && - Object.keys(storeData.remoteConfigList[remote].copyDefaults).length > 0 - ) { - setCopyOptionsJson( - JSON.stringify(storeData.remoteConfigList[remote].copyDefaults, null, 2) - ) + const remoteConfig = storeData.remoteConfigList[remote] + + if (remoteConfig.copyDefaults) { + mergedCopyDefaults = { + ...mergedCopyDefaults, + ...remoteConfig.copyDefaults, + } + } + + if (remoteConfig.filterDefaults) { + mergedFilterDefaults = { + ...mergedFilterDefaults, + ...remoteConfig.filterDefaults, + } + } } - if ( - storeData.remoteConfigList[remote].filterDefaults && - Object.keys(storeData.remoteConfigList[remote].filterDefaults).length > 0 - ) { - setFilterOptionsJson( - JSON.stringify(storeData.remoteConfigList[remote].filterDefaults, null, 2) - ) + // Only merge defaults for remote paths + if (sourceRemote) mergeRemoteDefaults(sourceRemote) + if (destRemote) mergeRemoteDefaults(destRemote) + + if (Object.keys(mergedCopyDefaults).length > 0 && !copyOptionsLocked) { + setCopyOptionsJson(JSON.stringify(mergedCopyDefaults, null, 2)) } - }, [source]) + + if (Object.keys(mergedFilterDefaults).length > 0 && !filterOptionsLocked) { + setFilterOptionsJson(JSON.stringify(mergedFilterDefaults, null, 2)) + } + }, [source, dest]) useEffect(() => { getGlobalFlags().then((flags) => setGlobalOptions(flags)) @@ -81,9 +101,6 @@ export default function Copy() { setIsLoading(true) try { - await getMainTray().then((tray) => tray?.setVisible(false)) - await getLoadingTray().then((tray) => tray?.setVisible(true)) - await startCopy({ source: source!, dest: dest!, @@ -91,20 +108,8 @@ export default function Copy() { filterOptions, }) - // delay for the job to appear in the API - await new Promise((resolve) => setTimeout(resolve, 2500)) - - await openTrayWindow({ name: 'Jobs', url: '/jobs' }) - await getCurrentWindow().hide() - - await getMainTray().then((tray) => tray?.setVisible(true)) - await getLoadingTray().then((tray) => tray?.setVisible(false)) - - await getCurrentWindow().destroy() + setIsStarted(true) } catch (err) { - await getMainTray().then((tray) => tray?.setVisible(true)) - await getLoadingTray().then((tray) => tray?.setVisible(false)) - console.error('Failed to start copy:', err) const errorMessage = err instanceof Error ? err.message : 'Failed to start copy operation' @@ -161,6 +166,8 @@ export default function Copy() { setOptionsJson={setCopyOptionsJson} optionsFetcher={getCopyFlags} rows={20} + isLocked={copyOptionsLocked} + setIsLocked={setCopyOptionsLocked} />
- + {isStarted ? ( + <> + + + + + ) : ( + + )}
) diff --git a/src/pages/Sync.tsx b/src/pages/Sync.tsx index 7e23292..7be5614 100644 --- a/src/pages/Sync.tsx +++ b/src/pages/Sync.tsx @@ -1,16 +1,17 @@ import { Accordion, AccordionItem, Avatar, Button } from '@nextui-org/react' -import { Window } from '@tauri-apps/api/window' +import { getCurrentWindow } from '@tauri-apps/api/window' import { message } from '@tauri-apps/plugin-dialog' import { AlertOctagonIcon, FilterIcon, FolderSyncIcon, FoldersIcon, PlayIcon } from 'lucide-react' import { useCallback, useEffect, useMemo, useState } from 'react' -import { useNavigate, useSearchParams } from 'react-router-dom' +import { useSearchParams } from 'react-router-dom' +import { getRemoteName } from '../../lib/format' import { getFilterFlags, getGlobalFlags, getSyncFlags, startSync } from '../../lib/rclone/api' import { usePersistedStore } from '../../lib/store' +import { openTrayWindow } from '../../lib/window' import OptionsSection from '../components/OptionsSection' import PathFinder from '../components/PathFinder' export default function Sync() { - const navigate = useNavigate() const [searchParams] = useSearchParams() const [source, setSource] = useState( @@ -18,44 +19,63 @@ export default function Sync() { ) const [dest, setDest] = useState(undefined) + const [isStarted, setIsStarted] = useState(false) const [isLoading, setIsLoading] = useState(false) const [jsonError, setJsonError] = useState<'sync' | 'filter' | null>(null) + const [syncOptionsLocked, setSyncOptionsLocked] = useState(false) const [syncOptions, setSyncOptions] = useState>({}) const [syncOptionsJson, setSyncOptionsJson] = useState('{}') + const [filterOptionsLocked, setFilterOptionsLocked] = useState(false) const [filterOptions, setFilterOptions] = useState>({}) const [filterOptionsJson, setFilterOptionsJson] = useState('{}') const [globalOptions, setGlobalOptions] = useState([]) + // biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect useEffect(() => { const storeData = usePersistedStore.getState() - const remote = source?.split(':')[0] + const sourceRemote = getRemoteName(source) + const destRemote = getRemoteName(dest) - if (!remote) return + let mergedSyncDefaults = {} + let mergedFilterDefaults = {} - if (!(remote in storeData.remoteConfigList)) return + // Helper function to merge defaults from a remote + const mergeRemoteDefaults = (remote: string | null) => { + if (!remote || !(remote in storeData.remoteConfigList)) return - if ( - storeData.remoteConfigList[remote].syncDefaults && - Object.keys(storeData.remoteConfigList[remote].syncDefaults).length > 0 - ) { - setSyncOptionsJson( - JSON.stringify(storeData.remoteConfigList[remote].syncDefaults, null, 2) - ) + const remoteConfig = storeData.remoteConfigList[remote] + + if (remoteConfig.syncDefaults) { + mergedSyncDefaults = { + ...mergedSyncDefaults, + ...remoteConfig.syncDefaults, + } + } + + if (remoteConfig.filterDefaults) { + mergedFilterDefaults = { + ...mergedFilterDefaults, + ...remoteConfig.filterDefaults, + } + } } - if ( - storeData.remoteConfigList[remote].filterDefaults && - Object.keys(storeData.remoteConfigList[remote].filterDefaults).length > 0 - ) { - setFilterOptionsJson( - JSON.stringify(storeData.remoteConfigList[remote].filterDefaults, null, 2) - ) + // Only merge defaults for remote paths + if (sourceRemote) mergeRemoteDefaults(sourceRemote) + if (destRemote) mergeRemoteDefaults(destRemote) + + if (Object.keys(mergedSyncDefaults).length > 0 && !syncOptionsLocked) { + setSyncOptionsJson(JSON.stringify(mergedSyncDefaults, null, 2)) } - }, [source]) + + if (Object.keys(mergedFilterDefaults).length > 0 && !filterOptionsLocked) { + setFilterOptionsJson(JSON.stringify(mergedFilterDefaults, null, 2)) + } + }, [source, dest]) useEffect(() => { getGlobalFlags().then((flags) => setGlobalOptions(flags)) @@ -86,9 +106,8 @@ export default function Sync() { syncOptions, filterOptions, }) - await new Promise((resolve) => setTimeout(resolve, 1000)) - navigate('/jobs') - await Window.getCurrent().setTitle('Jobs') + + setIsStarted(true) } catch (err) { console.error('Failed to start sync:', err) const errorMessage = @@ -100,7 +119,7 @@ export default function Sync() { } finally { setIsLoading(false) } - }, [source, dest, syncOptions, filterOptions, navigate]) + }, [source, dest, syncOptions, filterOptions]) const buttonText = useMemo(() => { if (isLoading) return 'STARTING...' @@ -146,6 +165,8 @@ export default function Sync() { globalOptions={globalOptions['main' as keyof typeof globalOptions]} optionsFetcher={getSyncFlags} rows={20} + isLocked={syncOptionsLocked} + setIsLocked={setSyncOptionsLocked} />
- + {isStarted ? ( + <> + + + + + ) : ( + + )}
)