RemoteOptions handling for Copy

Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com>
This commit is contained in:
FTCHD
2025-09-17 21:45:18 +02:00
parent 0a12a29770
commit 651dc856b0
6 changed files with 223 additions and 103 deletions
+8
View File
@@ -494,11 +494,13 @@ export async function startCopy({
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('[startCopy]', srcFs, dstFs)
@@ -516,6 +518,12 @@ export async function startCopy({
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/copy?${params.toString()}`, {
method: 'POST',
headers: getAuthHeader(),
+7
View File
@@ -8,6 +8,7 @@
"name": "rclone-ui",
"version": "2.2.0",
"dependencies": {
"@formkit/auto-animate": "^0.9.0",
"@heroui/react": "^2.7.11",
"@sentry/browser": "^10.8.0",
"@tauri-apps/api": "~2.7.0",
@@ -1020,6 +1021,12 @@
"tslib": "^2.8.0"
}
},
"node_modules/@formkit/auto-animate": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.9.0.tgz",
"integrity": "sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA==",
"license": "MIT"
},
"node_modules/@heroui/accordion": {
"version": "2.2.19",
"resolved": "https://registry.npmjs.org/@heroui/accordion/-/accordion-2.2.19.tgz",
+1
View File
@@ -19,6 +19,7 @@
"build:wasm": "tauri build --target wasm32-unknown-unknown"
},
"dependencies": {
"@formkit/auto-animate": "^0.9.0",
"@heroui/react": "^2.7.11",
"@sentry/browser": "^10.8.0",
"@tauri-apps/api": "~2.7.0",
+2 -1
View File
@@ -24,7 +24,8 @@
"decorations": false,
"focus": true,
"url": "tray.html",
"theme": "Dark"
"theme": "Dark",
"backgroundThrottling": "disabled"
}
],
"security": {
+183 -102
View File
@@ -1,9 +1,11 @@
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,
@@ -11,7 +13,7 @@ import {
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 +30,7 @@ import { openWindow } from '../../lib/window'
import CronEditor from '../components/CronEditor'
import OptionsSection from '../components/OptionsSection'
import { MultiPathFinder } from '../components/PathFinder'
import RemoteOptionsSection from '../components/RemoteOptionsSection'
export default function Copy() {
const [searchParams] = useSearchParams()
@@ -39,7 +42,7 @@ export default function Copy() {
const [isStarted, setIsStarted] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [jsonError, setJsonError] = useState<'copy' | 'filter' | 'config' | null>(null)
const [jsonError, setJsonError] = useState<'copy' | 'filter' | 'config' | 'remote' | null>(null)
const [copyOptionsLocked, setCopyOptionsLocked] = useState(false)
const [copyOptions, setCopyOptions] = useState<Record<string, string>>({})
@@ -53,26 +56,32 @@ export default function Copy() {
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 [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 [backends, setBackends] = useState<Backend[]>([])
// useEffect(() => {
// getBackends().then((b) => {
// setBackends(b)
// })
// }, [])
const [showMore, setShowMore] = useState(false)
const [animationParent] = useAutoAnimate()
const selectedRemotes = (() => {
return [...(sources || []), dest].filter(Boolean) as string[]
})()
console.log('[Copy] selectedRemotes', selectedRemotes)
// console.log('sources', sources)
// console.log('dest', dest)
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
@@ -137,22 +146,27 @@ export default function Copy() {
}, [sources, dest])
useEffect(() => {
let step: 'copy' | 'filter' | 'config' = 'copy'
try {
setCopyOptions(JSON.parse(copyOptionsJson))
startTransition(() => {
let step: 'copy' | 'filter' | 'config' | 'remote' = 'copy'
try {
setCopyOptions(JSON.parse(copyOptionsJson))
step = 'filter'
setFilterOptions(JSON.parse(filterOptionsJson))
step = 'filter'
setFilterOptions(JSON.parse(filterOptionsJson))
step = 'config'
setConfigOptions(JSON.parse(configOptionsJson))
step = 'config'
setConfigOptions(JSON.parse(configOptionsJson))
setJsonError(null)
} catch (error) {
setJsonError(step)
console.error(`Error parsing ${step} options:`, error)
}
}, [copyOptionsJson, filterOptionsJson, configOptionsJson])
step = 'remote'
setRemoteOptions(JSON.parse(remoteOptionsJson))
setJsonError(null)
} catch (error) {
setJsonError(step)
console.error(`Error parsing ${step} options:`, error)
}
})
}, [copyOptionsJson, filterOptionsJson, configOptionsJson, remoteOptionsJson])
const buttonText = (() => {
if (isLoading) return 'STARTING...'
@@ -292,6 +306,7 @@ export default function Copy() {
dstFs: destination,
_config: mergedConfig,
_filter: customFilterOptions,
remoteOptions,
})
await new Promise((resolve) => setTimeout(resolve, 500))
@@ -339,12 +354,13 @@ export default function Copy() {
// dummy delay to avoid waiting when opening the Jobs page
await new Promise((resolve) => setTimeout(resolve, 1000))
if (sources.length !== Object.keys(failedPaths).length) {
const failedPathsKeys = Object.keys(failedPaths)
console.log('[Copy] failedPathsKeys', failedPathsKeys)
if (sources.length !== failedPathsKeys.length) {
setIsStarted(true)
}
const failedPathsKeys = Object.keys(failedPaths)
if (failedPathsKeys.length > 0) {
if (sources.length === failedPathsKeys.length) {
await message(`${failedPathsKeys[0]} ${failedPaths[failedPathsKeys[0]]}`, {
@@ -377,82 +393,147 @@ export default function Copy() {
setDestPath={setDest}
/>
<Accordion>
<AccordionItem
key="copy"
startContent={
<Avatar color="primary" radius="lg" fallback={<CopyIcon />} />
}
indicator={<CopyIcon />}
subtitle="Tap to toggle copy options for this operation"
title="Copy"
>
<OptionsSection
globalOptions={
currentGlobalOptions['main' as keyof typeof currentGlobalOptions]
<div
className={cn('flex flex-col pb-10', showMore && 'pb-0')}
ref={animationParent}
>
<Accordion>
<AccordionItem
key="copy"
startContent={
<Avatar color="primary" radius="lg" fallback={<CopyIcon />} />
}
optionsJson={copyOptionsJson}
setOptionsJson={setCopyOptionsJson}
getAvailableOptions={getCopyFlags}
rows={15}
isLocked={copyOptionsLocked}
setIsLocked={setCopyOptionsLocked}
/>
</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={<CopyIcon />}
subtitle="Tap to toggle copy options for this operation"
title="Copy"
>
<OptionsSection
globalOptions={
currentGlobalOptions[
'main' as keyof typeof currentGlobalOptions
]
}
optionsJson={copyOptionsJson}
setOptionsJson={setCopyOptionsJson}
getAvailableOptions={getCopyFlags}
isLocked={copyOptionsLocked}
setIsLocked={setCopyOptionsLocked}
/>
</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>
) : undefined}
</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">
+22
View File
@@ -15,9 +15,31 @@ export default {
'60%': { opacity: '1' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
'show-more': {
'0%, 100%': {
transform: 'translateY(-25%)',
timingFunction: 'cubic-bezier(0.8,0,1,1)',
},
'60%': {
transform: 'none',
timingFunction: 'cubic-bezier(0,0,0.2,1)',
},
},
'show-more-title': {
'0%, 100%': {
transform: 'translateY(5%)',
timingFunction: 'cubic-bezier(0.8,0,1,1)',
},
'60%': {
transform: 'none',
timingFunction: 'cubic-bezier(0,0,0.2,1)',
},
},
},
animation: {
'fade-in-up': 'fade-in-up 900ms cubic-bezier(0.22, 1, 0.36, 1)',
'show-more': 'show-more 2s infinite',
'show-more-title': 'show-more-title 2s infinite',
},
},
},