Copy/Sync, unify success handling, lockable options

This commit is contained in:
FTCHD
2025-01-31 16:26:51 +02:00
parent ad6d10acd6
commit 66a6a2e2aa
3 changed files with 193 additions and 88 deletions
+5
View File
@@ -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]
}
+92 -49
View File
@@ -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<string | undefined>(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<Record<string, string>>({})
const [copyOptionsJson, setCopyOptionsJson] = useState<string>('{}')
const [filterOptionsLocked, setFilterOptionsLocked] = useState(false)
const [filterOptions, setFilterOptions] = useState<Record<string, string>>({})
const [filterOptionsJson, setFilterOptionsJson] = useState<string>('{}')
const [globalOptions, setGlobalOptions] = useState<any[]>([])
// 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}
/>
</AccordionItem>
<AccordionItem
@@ -178,26 +185,62 @@ export default function Copy() {
setOptionsJson={setFilterOptionsJson}
optionsFetcher={getFilterFlags}
rows={4}
isLocked={filterOptionsLocked}
setIsLocked={setFilterOptionsLocked}
/>
</AccordionItem>
</Accordion>
</div>
<div className="sticky bottom-0 z-50 flex items-center justify-center flex-none p-4 border-t border-neutral-500/20 bg-neutral-900/50 backdrop-blur-lg">
<Button
onPress={handleStartCopy}
size="lg"
fullWidth={true}
type="button"
color="primary"
isDisabled={isLoading || !!jsonError || !source || !dest || source === dest}
isLoading={isLoading}
endContent={buttonIcon}
className="max-w-2xl"
data-focus-visible="false"
>
{buttonText}
</Button>
{isStarted ? (
<>
<Button
fullWidth={true}
size="lg"
onPress={() => {
setCopyOptionsJson('{}')
setFilterOptionsJson('{}')
setSource(undefined)
setDest(undefined)
setIsStarted(false)
}}
data-focus-visible="false"
>
New Copy
</Button>
<Button
fullWidth={true}
size="lg"
color="primary"
onPress={async () => {
await openTrayWindow({ name: 'Jobs', url: '/jobs' })
// await getCurrentWindow().hide()
await getCurrentWindow().destroy()
}}
data-focus-visible="false"
>
Show Jobs
</Button>
</>
) : (
<Button
onPress={handleStartCopy}
size="lg"
fullWidth={true}
type="button"
color="primary"
isDisabled={isLoading || !!jsonError || !source || !dest || source === dest}
isLoading={isLoading}
endContent={buttonIcon}
className="max-w-2xl"
data-focus-visible="false"
>
{buttonText}
</Button>
)}
</div>
</div>
)
+96 -39
View File
@@ -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<string | undefined>(
@@ -18,44 +19,63 @@ export default function Sync() {
)
const [dest, setDest] = useState<string | undefined>(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<Record<string, string>>({})
const [syncOptionsJson, setSyncOptionsJson] = useState<string>('{}')
const [filterOptionsLocked, setFilterOptionsLocked] = useState(false)
const [filterOptions, setFilterOptions] = useState<Record<string, string>>({})
const [filterOptionsJson, setFilterOptionsJson] = useState<string>('{}')
const [globalOptions, setGlobalOptions] = useState<any[]>([])
// 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}
/>
</AccordionItem>
<AccordionItem
@@ -163,26 +184,62 @@ export default function Sync() {
setOptionsJson={setFilterOptionsJson}
optionsFetcher={getFilterFlags}
rows={4}
isLocked={filterOptionsLocked}
setIsLocked={setFilterOptionsLocked}
/>
</AccordionItem>
</Accordion>
</div>
<div className="sticky bottom-0 z-50 flex items-center justify-center flex-none p-4 border-t border-neutral-500/20 bg-neutral-900/50 backdrop-blur-lg">
<Button
onPress={handleStartSync}
size="lg"
fullWidth={true}
type="button"
color="primary"
isDisabled={isLoading || !!jsonError || !source || !dest || source === dest}
isLoading={isLoading}
endContent={buttonIcon}
className="max-w-2xl"
data-focus-visible="false"
>
{buttonText}
</Button>
{isStarted ? (
<>
<Button
fullWidth={true}
size="lg"
onPress={() => {
setSyncOptionsJson('{}')
setFilterOptionsJson('{}')
setDest(undefined)
setSource(undefined)
setIsStarted(false)
}}
data-focus-visible="false"
>
New Sync
</Button>
<Button
fullWidth={true}
size="lg"
color="primary"
onPress={async () => {
await openTrayWindow({ name: 'Jobs', url: '/jobs' })
// await getCurrentWindow().hide()
await getCurrentWindow().destroy()
}}
data-focus-visible="false"
>
Show Jobs
</Button>
</>
) : (
<Button
onPress={handleStartSync}
size="lg"
fullWidth={true}
type="button"
color="primary"
isDisabled={isLoading || !!jsonError || !source || !dest || source === dest}
isLoading={isLoading}
endContent={buttonIcon}
className="max-w-2xl"
data-focus-visible="false"
>
{buttonText}
</Button>
)}
</div>
</div>
)