From 651dc856b070ff8819bf03a7d7391971f8e83db6 Mon Sep 17 00:00:00 2001 From: FTCHD <144691102+FTCHD@users.noreply.github.com> Date: Wed, 17 Sep 2025 21:45:18 +0200 Subject: [PATCH] RemoteOptions handling for Copy Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com> --- lib/rclone/api.ts | 8 ++ package-lock.json | 7 + package.json | 1 + src-tauri/tauri.conf.json | 3 +- src/pages/Copy.tsx | 285 ++++++++++++++++++++++++-------------- tailwind.config.js | 22 +++ 6 files changed, 223 insertions(+), 103 deletions(-) diff --git a/lib/rclone/api.ts b/lib/rclone/api.ts index b8cf7f2..f15f98b 100644 --- a/lib/rclone/api.ts +++ b/lib/rclone/api.ts @@ -494,11 +494,13 @@ export async function startCopy({ dstFs, _config, _filter, + remoteOptions, }: { srcFs: string dstFs: string _config?: Record _filter?: Record + remoteOptions?: Record }) { 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(), diff --git a/package-lock.json b/package-lock.json index 4dd249d..3b88795 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 28d7632..9c92b20 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5ab426e..ce2e3ca 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -24,7 +24,8 @@ "decorations": false, "focus": true, "url": "tray.html", - "theme": "Dark" + "theme": "Dark", + "backgroundThrottling": "disabled" } ], "security": { diff --git a/src/pages/Copy.tsx b/src/pages/Copy.tsx index 725d701..72002ed 100644 --- a/src/pages/Copy.tsx +++ b/src/pages/Copy.tsx @@ -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>({}) @@ -53,26 +56,32 @@ export default function Copy() { const [configOptions, setConfigOptions] = useState>({}) const [configOptionsJson, setConfigOptionsJson] = useState('{}') - // const [remoteOptionsLocked, setRemoteOptionsLocked] = useState(false) - // const [remoteOptions, setRemoteOptions] = useState>({}) - // const [remoteOptionsJson, setRemoteOptionsJson] = useState('{}') + const [remoteOptionsLocked, setRemoteOptionsLocked] = useState(false) + const [remoteOptions, setRemoteOptions] = useState>({}) + const [remoteOptionsJson, setRemoteOptionsJson] = useState('{}') const [cronExpression, setCronExpression] = useState(null) const [currentGlobalOptions, setCurrentGlobalOptions] = useState([]) - // const [backends, setBackends] = useState([]) - // 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} /> - - } /> - } - indicator={} - subtitle="Tap to toggle copy options for this operation" - title="Copy" - > - + + } /> } - optionsJson={copyOptionsJson} - setOptionsJson={setCopyOptionsJson} - getAvailableOptions={getCopyFlags} - rows={15} - isLocked={copyOptionsLocked} - setIsLocked={setCopyOptionsLocked} - /> - - } /> - } - indicator={} - subtitle="Tap to toggle filtering options for this operation" - title="Filters" - > - } + subtitle="Tap to toggle copy options for this operation" + title="Copy" + > + + + } /> } - optionsJson={filterOptionsJson} - setOptionsJson={setFilterOptionsJson} - getAvailableOptions={getFilterFlags} - rows={4} - isLocked={filterOptionsLocked} - setIsLocked={setFilterOptionsLocked} - /> - - } /> - } - indicator={} - subtitle="Tap to toggle config options for this operation" - title="Config" - > - } + subtitle="Tap to toggle filtering options for this operation" + title="Filters" + > + + + } /> } - optionsJson={configOptionsJson} - setOptionsJson={setConfigOptionsJson} - getAvailableOptions={getConfigFlags} - rows={10} - isLocked={configOptionsLocked} - setIsLocked={setConfigOptionsLocked} - /> - - } /> - } - indicator={} - subtitle="Tap to toggle cron options for this operation" - title="Cron" - > - - - + indicator={} + subtitle="Tap to toggle cron options for this operation" + title="Cron" + > + + + + + {showMore ? ( + + ) : ( +
{ + startTransition(() => { + setShowMore(true) + }) + requestAnimationFrame(() => { + setTimeout(() => { + scrollTo({ + top: document.body.scrollHeight, + behavior: 'smooth', + }) + }, 400) + }) + }} + > +

+ Show more options +

+ +
+ )} + + {showMore ? ( + // @ts-expect-error + + } /> + } + indicator={} + subtitle="Tap to toggle config options for this operation" + title="Config" + > + + + + {selectedRemotes.length > 0 && ( + } + /> + } + indicator={} + subtitle="Tap to toggle remote options for this operation" + title={'Remotes'} + > + + + )} + + ) : undefined} +
diff --git a/tailwind.config.js b/tailwind.config.js index 0f37f2d..de09daf 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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', }, }, },