diff --git a/src/pages/Move.tsx b/src/pages/Move.tsx
index e9462d5..157845a 100644
--- a/src/pages/Move.tsx
+++ b/src/pages/Move.tsx
@@ -12,7 +12,7 @@ import {
PlayIcon,
WrenchIcon,
} from 'lucide-react'
-import { useCallback, useEffect, useMemo, useState } from 'react'
+import { useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { getRemoteName } from '../../lib/format'
import { isRemotePath } from '../../lib/fs'
@@ -143,7 +143,7 @@ export default function Move() {
}
}, [moveOptionsJson, filterOptionsJson, configOptionsJson])
- const handleStartMove = useCallback(async () => {
+ async function handleStartMove() {
setIsLoading(true)
if (!sources || sources.length === 0 || !dest) {
@@ -151,12 +151,13 @@ export default function Move() {
title: 'Error',
kind: 'error',
})
+ setIsLoading(false)
return
}
- try {
- // check local paths exists
- for (const source of sources) {
+ // check local paths exists
+ for (const source of sources) {
+ try {
if (!isRemotePath(source)) {
const sourceExists = await exists(source)
if (sourceExists) {
@@ -169,8 +170,8 @@ export default function Move() {
setIsLoading(false)
return
}
- }
- } catch {}
+ } catch {}
+ }
if (!isRemotePath(dest)) {
const destExists = await exists(dest)
@@ -198,9 +199,15 @@ export default function Move() {
filterOptions &&
('IncludeRule' in filterOptions || 'IncludeFrom' in filterOptions)
) {
- throw new Error(
- 'Include rules are not supported when the input is one or multiple files'
+ await message(
+ 'Include rules are not supported when the input is one or multiple files',
+ {
+ title: 'Error',
+ kind: 'error',
+ }
)
+ setIsLoading(false)
+ return
}
const mergedConfig = {
@@ -247,16 +254,16 @@ export default function Move() {
const failedPaths: Record
= {}
for (const source of sources) {
- try {
- const customFilterOptions = isFolder
- ? filterOptions
- : {
- ...filterOptions,
- IncludeRule: [source.split('/').pop()!],
- }
+ const customFilterOptions = isFolder
+ ? filterOptions
+ : {
+ ...filterOptions,
+ IncludeRule: [source.split('/').pop()!],
+ }
+ const customSource = isFolder ? source : source.split('/').slice(0, -1).join('/')
+ try {
// Use parent folder path if the input is a file
- const customSource = isFolder ? source : source.split('/').slice(0, -1).join('/')
const jobId = await startMove({
srcFs: customSource,
@@ -336,33 +343,24 @@ export default function Move() {
}
setIsLoading(false)
- }, [
- sources,
- dest,
- moveOptions,
- filterOptions,
- cronExpression,
- createEmptySrcDirs,
- deleteEmptyDstDirs,
- configOptions,
- ])
+ }
- const buttonText = useMemo(() => {
+ const buttonText = (() => {
if (isLoading) return 'STARTING...'
if (!sources || sources.length === 0) return 'Please select a source path'
if (!dest) return 'Please select a destination path'
if (sources[0] === dest) return 'Source and destination cannot be the same'
if (jsonError) return 'Invalid JSON for ' + jsonError.toUpperCase() + ' options'
return 'START MOVE'
- }, [isLoading, jsonError, sources, dest])
+ })()
- const buttonIcon = useMemo(() => {
+ const buttonIcon = (() => {
if (isLoading) return
if (!sources || sources.length === 0 || !dest || sources[0] === dest)
return
if (jsonError) return
return
- }, [isLoading, jsonError, sources, dest])
+ })()
return (
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index c482124..ddb9571 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -48,14 +48,7 @@ import {
ServerIcon,
Trash2Icon,
} from 'lucide-react'
-import {
- type DetailedHTMLProps,
- type HTMLAttributes,
- useCallback,
- useEffect,
- useRef,
- useState,
-} from 'react'
+import { type DetailedHTMLProps, type HTMLAttributes, useEffect, useRef, useState } from 'react'
import { revokeMachineLicense, validateLicense } from '../../lib/license'
import { deleteRemote, getVersion as getCliVersion, getVersion } from '../../lib/rclone/api'
import { getConfigPath, getDefaultPaths } from '../../lib/rclone/common'
@@ -269,7 +262,7 @@ function GeneralSection() {
const [isWorkingUpdate, setIsWorkingUpdate] = useState(false)
const [update, setUpdate] = useState
(null)
- const updateCallback = useCallback(async () => {
+ async function updateCallback() {
if (!update) {
try {
console.log('checking for updates')
@@ -300,9 +293,8 @@ function GeneralSection() {
} catch (e) {
Sentry.captureException(e)
console.error(e)
- } finally {
- setIsWorkingUpdate(false)
}
+ setIsWorkingUpdate(false)
return
}
@@ -355,7 +347,7 @@ function GeneralSection() {
}
await getCurrentWindow().emit('close-app')
- }, [update])
+ }
useEffect(() => {
// needed since the first value from the persisted store is undefined
@@ -661,9 +653,8 @@ function LicenseSection() {
okLabel: 'Ok',
cancelLabel: '',
})
- } finally {
- setIsActivating(false)
}
+ setIsActivating(false)
await message('Your license has been successfully activated.', {
title: 'Congrats!',
@@ -720,9 +711,8 @@ function LicenseSection() {
okLabel: 'Ok',
cancelLabel: '',
})
- } finally {
- setIsRevoking(false)
}
+ setIsRevoking(false)
await message('Your license has been successfully deactivated.', {
title: 'License deactivated',
@@ -929,10 +919,9 @@ function ConfigSection() {
const [isExportingId, setIsExportingId] = useState('')
- const exportConfig = useCallback(async ({ id, label }: { id: string; label: string }) => {
+ async function exportConfig({ id, label }: { id: string; label: string }) {
+ setIsExportingId(id)
try {
- setIsExportingId(id)
-
const configPath = await getConfigPath({ id: id, validate: true })
const text = await readTextFile(configPath)
@@ -959,10 +948,9 @@ function ConfigSection() {
kind: 'error',
okLabel: 'OK',
})
- } finally {
- setIsExportingId('')
}
- }, [])
+ setIsExportingId('')
+ }
return (
@@ -1177,7 +1165,7 @@ function AboutSection() {
const isLoadingLogs = useRef(false)
const [last30Lines, setLast30Lines] = useState
([])
- const fetchInfo = useCallback(async () => {
+ async function fetchInfo() {
const defaultPaths = await getDefaultPaths()
const version = await getVersion()
const dirs = {
@@ -1205,24 +1193,34 @@ function AboutSection() {
isEncrypted: currentConfig?.isEncrypted!,
},
}
- }, [currentConfig])
+ }
- const fetchLogs = useCallback(async (logFilePath: string) => {
+ async function fetchLogs(logFilePath: string) {
if (isLoadingLogs.current) {
return []
}
isLoadingLogs.current = true
const logLines = await readTextFileLines(logFilePath)
- const lines = []
- for await (const line of logLines) {
- lines.push(line)
+
+ const lines: string[] = []
+ const iterator = logLines[Symbol.asyncIterator]()
+ let result = await iterator.next()
+
+ while (!result.done) {
+ lines.push(result.value)
+ if (lines.length > 35) {
+ lines.splice(0, 5)
+ }
+ result = await iterator.next()
}
+
isLoadingLogs.current = false
- return lines.slice(-30)
- }, [])
+ return lines
+ }
useEffect(() => {
fetchInfo().then(setInfo)
+ // biome-ignore lint/correctness/useExhaustiveDependencies:
}, [fetchInfo])
useEffect(() => {
@@ -1233,6 +1231,7 @@ function AboutSection() {
return
}
fetchLogs(info.dirs.appLog + '/Rclone UI.log').then(setLast30Lines)
+ // biome-ignore lint/correctness/useExhaustiveDependencies:
}, [fetchLogs, info, last30Lines.length])
return (
diff --git a/src/pages/Sync.tsx b/src/pages/Sync.tsx
index ee6eb91..afca93b 100644
--- a/src/pages/Sync.tsx
+++ b/src/pages/Sync.tsx
@@ -11,7 +11,7 @@ import {
PlayIcon,
WrenchIcon,
} from 'lucide-react'
-import { useCallback, useEffect, useMemo, useState } from 'react'
+import { useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { getRemoteName } from '../../lib/format'
import { isRemotePath } from '../../lib/fs'
@@ -57,93 +57,23 @@ export default function Sync() {
const [currentGlobalOptions, setCurrentGlobalOptions] = useState([])
- useEffect(() => {
- setConfigOptionsJson(JSON.stringify(RCLONE_CONFIG_DEFAULTS, null, 2))
+ const buttonText = (() => {
+ if (isLoading) return 'STARTING...'
+ if (!source) return 'Please select a source path'
+ if (!dest) return 'Please select a destination path'
+ if (source === dest) return 'Source and destination cannot be the same'
+ if (jsonError) return 'Invalid JSON for ' + jsonError.toUpperCase() + ' options'
+ return 'START SYNC'
+ })()
- return () => {
- setConfigOptionsJson('{}')
- }
- }, [])
+ const buttonIcon = (() => {
+ if (isLoading) return
+ if (!source || !dest || source === dest) return
+ if (jsonError) return
+ return
+ })()
- useEffect(() => {
- getCurrentGlobalFlags().then((flags) => setCurrentGlobalOptions(flags))
- }, [])
-
- // biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect
- useEffect(() => {
- const storeData = usePersistedStore.getState()
-
- const sourceRemote = getRemoteName(source)
- const destRemote = getRemoteName(dest)
-
- let mergedSyncDefaults = {}
- let mergedFilterDefaults = {}
- let mergedConfigDefaults = {}
-
- // Helper function to merge defaults from a remote
- const mergeRemoteDefaults = (remote: string | null) => {
- if (!remote) return
-
- const remoteConfig = storeData.remoteConfigList?.[remote] || {}
-
- if (remoteConfig.syncDefaults) {
- mergedSyncDefaults = {
- ...mergedSyncDefaults,
- ...remoteConfig.syncDefaults,
- }
- }
-
- if (remoteConfig.filterDefaults) {
- mergedFilterDefaults = {
- ...mergedFilterDefaults,
- ...remoteConfig.filterDefaults,
- }
- }
-
- if (remoteConfig.configDefaults) {
- mergedConfigDefaults = {
- ...mergedConfigDefaults,
- ...remoteConfig.configDefaults,
- }
- }
- }
-
- // 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))
- }
-
- if (Object.keys(mergedFilterDefaults).length > 0 && !filterOptionsLocked) {
- setFilterOptionsJson(JSON.stringify(mergedFilterDefaults, null, 2))
- }
-
- if (Object.keys(mergedConfigDefaults).length > 0 && !configOptionsLocked) {
- setConfigOptionsJson(JSON.stringify(mergedConfigDefaults, null, 2))
- }
- }, [source, dest])
-
- useEffect(() => {
- let step: 'sync' | 'filter' | 'config' = 'sync'
- try {
- setSyncOptions(JSON.parse(syncOptionsJson))
-
- step = 'filter'
- setFilterOptions(JSON.parse(filterOptionsJson))
-
- step = 'config'
- setConfigOptions(JSON.parse(configOptionsJson))
-
- setJsonError(null)
- } catch (error) {
- setJsonError(step)
- console.error(`Error parsing ${step} options:`, error)
- }
- }, [syncOptionsJson, filterOptionsJson, configOptionsJson])
-
- const handleStartSync = useCallback(async () => {
+ async function handleStartSync() {
setIsLoading(true)
if (!source || !dest) {
@@ -238,26 +168,95 @@ export default function Sync() {
title: 'Error',
kind: 'error',
})
- } finally {
- setIsLoading(false)
}
- }, [source, dest, syncOptions, filterOptions, cronExpression, configOptions])
+ setIsLoading(false)
+ }
- const buttonText = useMemo(() => {
- if (isLoading) return 'STARTING...'
- if (!source) return 'Please select a source path'
- if (!dest) return 'Please select a destination path'
- if (source === dest) return 'Source and destination cannot be the same'
- if (jsonError) return 'Invalid JSON for ' + jsonError.toUpperCase() + ' options'
- return 'START SYNC'
- }, [isLoading, jsonError, source, dest])
+ useEffect(() => {
+ setConfigOptionsJson(JSON.stringify(RCLONE_CONFIG_DEFAULTS, null, 2))
- const buttonIcon = useMemo(() => {
- if (isLoading) return
- if (!source || !dest || source === dest) return
- if (jsonError) return
- return
- }, [isLoading, jsonError, source, dest])
+ return () => {
+ setConfigOptionsJson('{}')
+ }
+ }, [])
+
+ useEffect(() => {
+ getCurrentGlobalFlags().then((flags) => setCurrentGlobalOptions(flags))
+ }, [])
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect
+ useEffect(() => {
+ const storeData = usePersistedStore.getState()
+
+ const sourceRemote = getRemoteName(source)
+ const destRemote = getRemoteName(dest)
+
+ let mergedSyncDefaults = {}
+ let mergedFilterDefaults = {}
+ let mergedConfigDefaults = {}
+
+ // Helper function to merge defaults from a remote
+ const mergeRemoteDefaults = (remote: string | null) => {
+ if (!remote) return
+
+ const remoteConfig = storeData.remoteConfigList?.[remote] || {}
+
+ if (remoteConfig.syncDefaults) {
+ mergedSyncDefaults = {
+ ...mergedSyncDefaults,
+ ...remoteConfig.syncDefaults,
+ }
+ }
+
+ if (remoteConfig.filterDefaults) {
+ mergedFilterDefaults = {
+ ...mergedFilterDefaults,
+ ...remoteConfig.filterDefaults,
+ }
+ }
+
+ if (remoteConfig.configDefaults) {
+ mergedConfigDefaults = {
+ ...mergedConfigDefaults,
+ ...remoteConfig.configDefaults,
+ }
+ }
+ }
+
+ // 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))
+ }
+
+ if (Object.keys(mergedFilterDefaults).length > 0 && !filterOptionsLocked) {
+ setFilterOptionsJson(JSON.stringify(mergedFilterDefaults, null, 2))
+ }
+
+ if (Object.keys(mergedConfigDefaults).length > 0 && !configOptionsLocked) {
+ setConfigOptionsJson(JSON.stringify(mergedConfigDefaults, null, 2))
+ }
+ }, [source, dest])
+
+ useEffect(() => {
+ let step: 'sync' | 'filter' | 'config' = 'sync'
+ try {
+ setSyncOptions(JSON.parse(syncOptionsJson))
+
+ step = 'filter'
+ setFilterOptions(JSON.parse(filterOptionsJson))
+
+ step = 'config'
+ setConfigOptions(JSON.parse(configOptionsJson))
+
+ setJsonError(null)
+ } catch (error) {
+ setJsonError(step)
+ console.error(`Error parsing ${step} options:`, error)
+ }
+ }, [syncOptionsJson, filterOptionsJson, configOptionsJson])
return (
diff --git a/vite.config.ts b/vite.config.ts
index 84eea89..726056f 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,7 +5,26 @@ const host = process.env.TAURI_DEV_HOST
// https://vitejs.dev/config/
export default defineConfig(async () => ({
- plugins: [react()],
+ plugins: [
+ react({
+ babel: {
+ plugins: [
+ [
+ 'babel-plugin-react-compiler',
+ {
+ target: '18',
+ panicThreshold: 'all_errors',
+ logger: {
+ logEvent(filename, event) {
+ console.log(`[Compiler] ${event.kind}: ${filename}`)
+ },
+ },
+ },
+ ],
+ ],
+ },
+ }),
+ ],
esbuild: {
supported: {