From 9205af78fd390979d4044a489c3203f4104db53f Mon Sep 17 00:00:00 2001
From: FTCHD <144691102+FTCHD@users.noreply.github.com>
Date: Tue, 5 Aug 2025 05:27:11 +0300
Subject: [PATCH] CronEditor, use in Copy/Sync, type
---
src/components/CronEditor.tsx | 184 ++++++++++++++++++++++++++++++++++
src/pages/Copy.tsx | 23 ++++-
src/pages/Sync.tsx | 23 ++++-
types/task.d.ts | 12 +++
4 files changed, 240 insertions(+), 2 deletions(-)
create mode 100644 src/components/CronEditor.tsx
create mode 100644 types/task.d.ts
diff --git a/src/components/CronEditor.tsx b/src/components/CronEditor.tsx
new file mode 100644
index 0000000..e026972
--- /dev/null
+++ b/src/components/CronEditor.tsx
@@ -0,0 +1,184 @@
+import { Button, Input, Select, SelectItem } from '@heroui/react'
+import cronstrue from 'cronstrue'
+import { ClockIcon, XIcon } from 'lucide-react'
+import type React from 'react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+
+interface CronEditorProps {
+ expression: string | null
+ onChange: (newExpression: string | null) => void
+}
+
+interface CronFieldProps {
+ label: string
+ value: string
+ onChange: (value: string) => void
+ options: string[]
+}
+
+const DEFAULT_OPTIONS = ['*', '*/5', '*/10', '*/15', '*/30']
+
+export default function CronEditor({ expression, onChange }: CronEditorProps) {
+ const [cronExpression, setCronExpression] = useState(expression)
+
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = useMemo(
+ () => (cronExpression || '* * * * *').split(' '),
+ [cronExpression]
+ )
+
+ const readableDescription = useMemo(() => {
+ if (!cronExpression) return 'This task is not scheduled'
+ let description: string
+ try {
+ description = cronstrue.toString(cronExpression)
+ description +=
+ '. Jobs trigger when the UI is running, if the active config is the same.'
+ } catch {
+ description = 'Invalid cron expression'
+ }
+ return description
+ }, [cronExpression])
+
+ const handleFieldChange = useCallback(
+ (field: string, value: string) => {
+ const parts = (cronExpression || '* * * * *').split(' ')
+ const index = ['minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'].indexOf(field)
+ if (index !== -1) {
+ parts[index] = value
+ setCronExpression(parts.join(' '))
+ }
+ },
+ [cronExpression]
+ )
+
+ useEffect(() => {
+ onChange(cronExpression)
+ }, [cronExpression, onChange])
+
+ return (
+
+
{
+ const expression = e.target.value
+
+ if (expression.length > 0) {
+ setCronExpression(expression)
+ } else {
+ setCronExpression(null)
+ }
+ }}
+ placeholder="Enter cron expression (e.g. 0 0 * * *)"
+ size="lg"
+ startContent={
}
+ isClearable={true}
+ onClear={() => setCronExpression(null)}
+ />
+
+
+ handleFieldChange('minute', v)}
+ options={generateOptions(0, 59)}
+ />
+ handleFieldChange('hour', v)}
+ options={generateOptions(0, 23)}
+ />
+ handleFieldChange('dayOfMonth', v)}
+ options={generateOptions(1, 31)}
+ />
+ handleFieldChange('month', v)}
+ options={generateOptions(1, 12)}
+ />
+ handleFieldChange('dayOfWeek', v)}
+ options={generateOptions(0, 7)}
+ />
+
+
+
{readableDescription}
+
+ )
+}
+
+function CronField({ label, value, onChange, options }: CronFieldProps) {
+ const [isCustom, setIsCustom] = useState(false)
+
+ useEffect(() => {
+ const isCustom =
+ !DEFAULT_OPTIONS.includes(value) &&
+ !options.includes(value) &&
+ value !== '*' &&
+ value !== 'custom'
+ setIsCustom(isCustom)
+ }, [value, options])
+
+ const handleCustomChange = (e: React.ChangeEvent) => {
+ onChange(e.target.value)
+ }
+
+ return (
+
+ {isCustom ? (
+ setIsCustom(false)}
+ >
+
+
+ }
+ />
+ ) : (
+
+ )}
+
+ )
+}
+
+function generateOptions(start: number, end: number): string[] {
+ return Array.from({ length: end - start + 1 }, (_, i) => (start + i).toString())
+}
diff --git a/src/pages/Copy.tsx b/src/pages/Copy.tsx
index 241da63..f0fdc47 100644
--- a/src/pages/Copy.tsx
+++ b/src/pages/Copy.tsx
@@ -1,7 +1,14 @@
import { Accordion, AccordionItem, Avatar, Button } from '@heroui/react'
import { message } from '@tauri-apps/plugin-dialog'
import { exists } from '@tauri-apps/plugin-fs'
-import { AlertOctagonIcon, CopyIcon, FilterIcon, FoldersIcon, PlayIcon } from 'lucide-react'
+import {
+ AlertOctagonIcon,
+ ClockIcon,
+ 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'
@@ -9,6 +16,7 @@ import { isRemotePath } from '../../lib/fs'
import { getCopyFlags, getFilterFlags, getGlobalFlags, startCopy } from '../../lib/rclone/api'
import { usePersistedStore } from '../../lib/store'
import { openWindow } from '../../lib/window'
+import CronEditor from '../components/CronEditor'
import OptionsSection from '../components/OptionsSection'
import PathFinder from '../components/PathFinder'
@@ -32,6 +40,8 @@ export default function Copy() {
const [filterOptions, setFilterOptions] = useState>({})
const [filterOptionsJson, setFilterOptionsJson] = useState('{}')
+ const [cronExpression, setCronExpression] = useState(null)
+
const [globalOptions, setGlobalOptions] = useState([])
// biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect
@@ -236,6 +246,17 @@ export default function Copy() {
setIsLocked={setFilterOptionsLocked}
/>
+ } />
+ }
+ indicator={}
+ subtitle="Tap to toggle cron options for this operation"
+ title="Cron"
+ >
+
+
diff --git a/src/pages/Sync.tsx b/src/pages/Sync.tsx
index 888538d..0c2feab 100644
--- a/src/pages/Sync.tsx
+++ b/src/pages/Sync.tsx
@@ -1,7 +1,14 @@
import { Accordion, AccordionItem, Avatar, Button } from '@heroui/react'
import { message } from '@tauri-apps/plugin-dialog'
import { exists } from '@tauri-apps/plugin-fs'
-import { AlertOctagonIcon, FilterIcon, FolderSyncIcon, FoldersIcon, PlayIcon } from 'lucide-react'
+import {
+ AlertOctagonIcon,
+ ClockIcon,
+ FilterIcon,
+ FolderSyncIcon,
+ FoldersIcon,
+ PlayIcon,
+} from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { getRemoteName } from '../../lib/format'
@@ -9,6 +16,7 @@ import { isRemotePath } from '../../lib/fs'
import { getFilterFlags, getGlobalFlags, getSyncFlags, startSync } from '../../lib/rclone/api'
import { usePersistedStore } from '../../lib/store'
import { openWindow } from '../../lib/window'
+import CronEditor from '../components/CronEditor'
import OptionsSection from '../components/OptionsSection'
import PathFinder from '../components/PathFinder'
@@ -32,6 +40,8 @@ export default function Sync() {
const [filterOptions, setFilterOptions] = useState>({})
const [filterOptionsJson, setFilterOptionsJson] = useState('{}')
+ const [cronExpression, setCronExpression] = useState(null)
+
const [globalOptions, setGlobalOptions] = useState([])
// biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect
@@ -236,6 +246,17 @@ export default function Sync() {
setIsLocked={setFilterOptionsLocked}
/>
+ } />
+ }
+ indicator={}
+ subtitle="Tap to toggle cron options for this operation"
+ title="Cron"
+ >
+
+
diff --git a/types/task.d.ts b/types/task.d.ts
new file mode 100644
index 0000000..f5ffe21
--- /dev/null
+++ b/types/task.d.ts
@@ -0,0 +1,12 @@
+export interface ScheduledTask {
+ id: string
+ type: 'delete' | 'sync' | 'copy'
+ cron: string
+ isRunning: boolean
+ isEnabled: boolean
+ currentRunId?: string
+ lastRun?: string
+ configId: string
+ args: Record
+ error?: string
+}