RemoteOptions handling for Sync

Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com>
This commit is contained in:
FTCHD
2025-09-17 21:45:45 +02:00
parent 483415f43e
commit fd0e4e5dce
2 changed files with 173 additions and 79 deletions
+8
View File
@@ -605,11 +605,13 @@ export async function startSync({
dstFs,
_config,
_filter,
remoteOptions,
}: {
srcFs: string
dstFs: string
_config?: Record<string, string | number | boolean | string[]>
_filter?: Record<string, string | number | boolean | string[]>
remoteOptions?: Record<string, string | number | boolean | string[]>
}) {
console.log('[startSync]', srcFs, dstFs)
@@ -627,6 +629,12 @@ export async function startSync({
params.set('_filter', JSON.stringify(parseRcloneOptions(_filter)))
}
if (remoteOptions && Object.keys(remoteOptions).length > 0) {
for (const [key, value] of Object.entries(remoteOptions)) {
params.set(key, value.toString())
}
}
const r = await fetch(`http://localhost:5572/sync/sync?${params.toString()}`, {
method: 'POST',
headers: getAuthHeader(),
+165 -79
View File
@@ -1,17 +1,20 @@
import { Accordion, AccordionItem, Avatar, Button } from '@heroui/react'
import { useAutoAnimate } from '@formkit/auto-animate/react'
import { Accordion, AccordionItem, Avatar, Button, Divider, cn } from '@heroui/react'
import { message } from '@tauri-apps/plugin-dialog'
import { exists } from '@tauri-apps/plugin-fs'
import cronstrue from 'cronstrue'
import {
AlertOctagonIcon,
ChevronDownIcon,
ClockIcon,
CopyIcon,
FilterIcon,
FolderSyncIcon,
FoldersIcon,
PlayIcon,
WrenchIcon,
} from 'lucide-react'
import { useEffect, useState } from 'react'
import { startTransition, useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { getRemoteName } from '../../lib/format'
import { isRemotePath } from '../../lib/fs'
@@ -28,6 +31,7 @@ import { openWindow } from '../../lib/window'
import CronEditor from '../components/CronEditor'
import OptionsSection from '../components/OptionsSection'
import { PathFinder } from '../components/PathFinder'
import RemoteOptionsSection from '../components/RemoteOptionsSection'
export default function Sync() {
const [searchParams] = useSearchParams()
@@ -39,7 +43,7 @@ export default function Sync() {
const [isStarted, setIsStarted] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [jsonError, setJsonError] = useState<'sync' | 'filter' | 'config' | null>(null)
const [jsonError, setJsonError] = useState<'sync' | 'filter' | 'config' | 'remote' | null>(null)
const [syncOptionsLocked, setSyncOptionsLocked] = useState(false)
const [syncOptions, setSyncOptions] = useState<Record<string, string>>({})
@@ -53,10 +57,21 @@ export default function Sync() {
const [configOptions, setConfigOptions] = useState<Record<string, string>>({})
const [configOptionsJson, setConfigOptionsJson] = useState<string>('{}')
const [remoteOptionsLocked, setRemoteOptionsLocked] = useState(false)
const [remoteOptions, setRemoteOptions] = useState<Record<string, string>>({})
const [remoteOptionsJson, setRemoteOptionsJson] = useState<string>('{}')
const [cronExpression, setCronExpression] = useState<string | null>(null)
const [currentGlobalOptions, setCurrentGlobalOptions] = useState<any[]>([])
const [showMore, setShowMore] = useState(false)
const [animationParent] = useAutoAnimate()
const selectedRemotes = (() => {
return [source, dest].filter(Boolean) as string[]
})()
const buttonText = (() => {
if (isLoading) return 'STARTING...'
if (!source) return 'Please select a source path'
@@ -154,6 +169,7 @@ export default function Sync() {
dstFs: dest,
_config: mergedConfig,
_filter: filterOptions,
remoteOptions,
})
// dummy delay to avoid waiting when opening the Jobs page
@@ -181,7 +197,9 @@ export default function Sync() {
}, [])
useEffect(() => {
getCurrentGlobalFlags().then((flags) => setCurrentGlobalOptions(flags))
getCurrentGlobalFlags().then((flags) =>
startTransition(() => setCurrentGlobalOptions(flags))
)
}, [])
// biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect
@@ -241,7 +259,7 @@ export default function Sync() {
}, [source, dest])
useEffect(() => {
let step: 'sync' | 'filter' | 'config' = 'sync'
let step: 'sync' | 'filter' | 'config' | 'remote' = 'sync'
try {
setSyncOptions(JSON.parse(syncOptionsJson))
@@ -251,12 +269,15 @@ export default function Sync() {
step = 'config'
setConfigOptions(JSON.parse(configOptionsJson))
step = 'remote'
setRemoteOptions(JSON.parse(remoteOptionsJson))
setJsonError(null)
} catch (error) {
setJsonError(step)
console.error(`Error parsing ${step} options:`, error)
}
}, [syncOptionsJson, filterOptionsJson, configOptionsJson])
}, [syncOptionsJson, filterOptionsJson, configOptionsJson, remoteOptionsJson])
return (
<div className="flex flex-col h-screen gap-10 pt-10">
@@ -289,82 +310,147 @@ export default function Sync() {
}}
/>
<Accordion>
<AccordionItem
key="sync"
startContent={
<Avatar color="success" radius="lg" fallback={<FolderSyncIcon />} />
}
indicator={<FolderSyncIcon />}
subtitle="Tap to toggle sync options for this operation"
title="Sync"
>
<OptionsSection
optionsJson={syncOptionsJson}
setOptionsJson={setSyncOptionsJson}
globalOptions={
currentGlobalOptions['main' as keyof typeof currentGlobalOptions]
<div
className={cn('flex flex-col pb-10', showMore && 'pb-0')}
ref={animationParent}
>
<Accordion>
<AccordionItem
key="sync"
startContent={
<Avatar color="success" radius="lg" fallback={<FolderSyncIcon />} />
}
getAvailableOptions={getSyncFlags}
rows={20}
isLocked={syncOptionsLocked}
setIsLocked={setSyncOptionsLocked}
/>
</AccordionItem>
<AccordionItem
key="filters"
startContent={
<Avatar color="danger" radius="lg" fallback={<FilterIcon />} />
}
indicator={<FilterIcon />}
subtitle="Tap to toggle filtering options for this operation"
title="Filters"
>
<OptionsSection
globalOptions={
currentGlobalOptions['filter' as keyof typeof currentGlobalOptions]
indicator={<FolderSyncIcon />}
subtitle="Tap to toggle sync options for this operation"
title="Sync"
>
<OptionsSection
optionsJson={syncOptionsJson}
setOptionsJson={setSyncOptionsJson}
globalOptions={
currentGlobalOptions[
'main' as keyof typeof currentGlobalOptions
]
}
getAvailableOptions={getSyncFlags}
isLocked={syncOptionsLocked}
setIsLocked={setSyncOptionsLocked}
/>
</AccordionItem>
<AccordionItem
key="filters"
startContent={
<Avatar color="danger" radius="lg" fallback={<FilterIcon />} />
}
optionsJson={filterOptionsJson}
setOptionsJson={setFilterOptionsJson}
getAvailableOptions={getFilterFlags}
rows={4}
isLocked={filterOptionsLocked}
setIsLocked={setFilterOptionsLocked}
/>
</AccordionItem>
<AccordionItem
key="config"
startContent={
<Avatar color="default" radius="lg" fallback={<WrenchIcon />} />
}
indicator={<WrenchIcon />}
subtitle="Tap to toggle config options for this operation"
title="Config"
>
<OptionsSection
globalOptions={
currentGlobalOptions['main' as keyof typeof currentGlobalOptions]
indicator={<FilterIcon />}
subtitle="Tap to toggle filtering options for this operation"
title="Filters"
>
<OptionsSection
globalOptions={
currentGlobalOptions[
'filter' as keyof typeof currentGlobalOptions
]
}
optionsJson={filterOptionsJson}
setOptionsJson={setFilterOptionsJson}
getAvailableOptions={getFilterFlags}
isLocked={filterOptionsLocked}
setIsLocked={setFilterOptionsLocked}
/>
</AccordionItem>
<AccordionItem
key="cron"
startContent={
<Avatar color="warning" radius="lg" fallback={<ClockIcon />} />
}
optionsJson={configOptionsJson}
setOptionsJson={setConfigOptionsJson}
getAvailableOptions={getConfigFlags}
rows={10}
isLocked={configOptionsLocked}
setIsLocked={setConfigOptionsLocked}
/>
</AccordionItem>
<AccordionItem
key="cron"
startContent={
<Avatar color="warning" radius="lg" fallback={<ClockIcon />} />
}
indicator={<ClockIcon />}
subtitle="Tap to toggle cron options for this operation"
title="Cron"
>
<CronEditor expression={cronExpression} onChange={setCronExpression} />
</AccordionItem>
</Accordion>
indicator={<ClockIcon />}
subtitle="Tap to toggle cron options for this operation"
title="Cron"
>
<CronEditor expression={cronExpression} onChange={setCronExpression} />
</AccordionItem>
</Accordion>
{showMore ? (
<Divider />
) : (
<div
key="show-more-options"
className="absolute flex flex-col items-center justify-center w-full gap-1 -bottom-8 group "
onClick={() => {
startTransition(() => {
setShowMore(true)
})
requestAnimationFrame(() => {
setTimeout(() => {
scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth',
})
}, 400)
})
}}
>
<p className="text-small animate-show-more-title group-hover:text-foreground-500 text-foreground-400">
Show more options
</p>
<ChevronDownIcon className="size-5 stroke-foreground-400 animate-show-more group-hover:stroke-foreground-500" />
</div>
)}
{showMore && (
// @ts-expect-error
<Accordion>
<AccordionItem
key="config"
startContent={
<Avatar color="default" radius="lg" fallback={<WrenchIcon />} />
}
indicator={<WrenchIcon />}
subtitle="Tap to toggle config options for this operation"
title="Config"
>
<OptionsSection
globalOptions={
currentGlobalOptions[
'main' as keyof typeof currentGlobalOptions
]
}
optionsJson={configOptionsJson}
setOptionsJson={setConfigOptionsJson}
getAvailableOptions={getConfigFlags}
isLocked={configOptionsLocked}
setIsLocked={setConfigOptionsLocked}
/>
</AccordionItem>
{selectedRemotes.length > 0 && (
<AccordionItem
key={'remotes'}
startContent={
<Avatar
className="bg-fuchsia-500"
radius="lg"
fallback={<CopyIcon />}
/>
}
indicator={<CopyIcon />}
subtitle="Tap to toggle remote options for this operation"
title={'Remotes'}
>
<RemoteOptionsSection
selectedRemotes={selectedRemotes}
remoteOptionsJson={remoteOptionsJson}
setRemoteOptionsJson={setRemoteOptionsJson}
setRemoteOptionsLocked={setRemoteOptionsLocked}
remoteOptionsLocked={remoteOptionsLocked}
/>
</AccordionItem>
)}
</Accordion>
)}
</div>
</div>
<div className="sticky bottom-0 z-50 flex items-center justify-center flex-none gap-2 p-4 border-t border-neutral-500/20 bg-neutral-900/50 backdrop-blur-lg">