manage multiple configs, spawn updates
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerContent,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
Input,
|
||||
Textarea,
|
||||
} from '@heroui/react'
|
||||
import { Button } from '@heroui/react'
|
||||
import { ask, open } from '@tauri-apps/plugin-dialog'
|
||||
import { mkdir, readTextFile, writeTextFile } from '@tauri-apps/plugin-fs'
|
||||
import { UploadIcon } from 'lucide-react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { getConfigPath } from '../../lib/rclone/api'
|
||||
import { usePersistedStore } from '../../lib/store'
|
||||
import type { ConfigFile } from '../../types/config'
|
||||
|
||||
export default function ConfigCreateDrawer({
|
||||
onClose,
|
||||
isOpen,
|
||||
}: {
|
||||
onClose: () => void
|
||||
isOpen: boolean
|
||||
}) {
|
||||
const [config, setConfig] = useState<Partial<ConfigFile>>({
|
||||
label: 'New Config',
|
||||
})
|
||||
const [configContent, setConfigContent] = useState<string | null>(null)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const isEncrypted = useMemo(() => {
|
||||
return configContent?.includes('RCLONE_ENCRYPT_V0:')
|
||||
}, [configContent])
|
||||
|
||||
const handleCreate = useCallback(
|
||||
async ({
|
||||
label,
|
||||
pass,
|
||||
content,
|
||||
}: { label?: string; pass?: string; content: string | null }) => {
|
||||
try {
|
||||
if (!label) {
|
||||
throw new Error('Label is required')
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
throw new Error('Content is required')
|
||||
}
|
||||
|
||||
if (!pass && content.includes('RCLONE_ENCRYPT_V0:')) {
|
||||
throw new Error('Password is required for encrypted configs')
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
|
||||
const generatedId = crypto.randomUUID()
|
||||
|
||||
const configPath = await getConfigPath({ id: generatedId, validate: false })
|
||||
|
||||
await mkdir(configPath.replace('/rclone.conf', ''), { recursive: true })
|
||||
await writeTextFile(configPath, content)
|
||||
console.log('[handleCreate] saved config to', configPath)
|
||||
|
||||
usePersistedStore.getState().addConfigFile({
|
||||
id: generatedId,
|
||||
label,
|
||||
pass,
|
||||
isEncrypted: content.includes('RCLONE_ENCRYPT_V0:'),
|
||||
sync: undefined,
|
||||
})
|
||||
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error('[handleCreate] failed to save config', error)
|
||||
await ask('Failed to save config', {
|
||||
title: 'Error',
|
||||
kind: 'error',
|
||||
okLabel: 'OK',
|
||||
cancelLabel: '',
|
||||
})
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
},
|
||||
[onClose]
|
||||
)
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
placement={'bottom'}
|
||||
size="full"
|
||||
onClose={onClose}
|
||||
hideCloseButton={true}
|
||||
>
|
||||
<DrawerContent>
|
||||
{(close) => (
|
||||
<>
|
||||
<DrawerHeader className="flex flex-col gap-1">Import Config</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<form
|
||||
id="config-form"
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleCreate({
|
||||
label: config.label,
|
||||
pass: config.pass,
|
||||
content: configContent,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
label="Name"
|
||||
labelPlacement="outside"
|
||||
placeholder="Enter a name for your config"
|
||||
type="text"
|
||||
value={config.label}
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
onValueChange={(value) => {
|
||||
setConfig({ ...config, label: value })
|
||||
}}
|
||||
isClearable={true}
|
||||
onClear={() => {
|
||||
setConfig({ ...config, label: '' })
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
{isEncrypted && (
|
||||
<Input
|
||||
label="Password"
|
||||
labelPlacement="outside"
|
||||
placeholder="Enter the password for your config file"
|
||||
type="password"
|
||||
value={config.pass}
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
onValueChange={(value) => {
|
||||
setConfig({ ...config, pass: value })
|
||||
}}
|
||||
isClearable={true}
|
||||
onClear={() => {
|
||||
setConfig({ ...config, pass: '' })
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
className="w-full"
|
||||
label={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="text-medium">Config</p>
|
||||
<Button
|
||||
isIconOnly={true}
|
||||
variant="light"
|
||||
size="sm"
|
||||
onPress={async () => {
|
||||
const selectedFile = await open({
|
||||
directory: false,
|
||||
multiple: false,
|
||||
title: 'Select a config file',
|
||||
})
|
||||
|
||||
if (!selectedFile) {
|
||||
return
|
||||
}
|
||||
|
||||
let content = await readTextFile(selectedFile)
|
||||
|
||||
if (selectedFile.endsWith('.json')) {
|
||||
const importedRemotes = JSON.parse(
|
||||
content
|
||||
) as Record<string, object>
|
||||
content = Object.entries(importedRemotes)
|
||||
.map(([name, remote]) => {
|
||||
const remoteName = `[${name}]`
|
||||
const remoteConfig = Object.entries(
|
||||
remote
|
||||
)
|
||||
.map(([key, value]) => {
|
||||
return `${key} = ${value}`
|
||||
})
|
||||
.join('\n')
|
||||
return `${remoteName}\n${remoteConfig}`
|
||||
})
|
||||
.reduce((acc, curr) => {
|
||||
return `${curr}\n\n${acc}`
|
||||
}, '')
|
||||
}
|
||||
|
||||
setConfigContent(content)
|
||||
setConfig({
|
||||
...config,
|
||||
label:
|
||||
config.label ||
|
||||
selectedFile.split('/').pop() ||
|
||||
'New Config',
|
||||
isEncrypted:
|
||||
content.includes('RCLONE_ENCRYPT_V0:'),
|
||||
})
|
||||
}}
|
||||
>
|
||||
<UploadIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
labelPlacement="outside"
|
||||
placeholder="Paste your config here or import an existing file"
|
||||
value={configContent || ''}
|
||||
onValueChange={(value) => {
|
||||
console.log(value)
|
||||
setConfigContent(value)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
//if it's tab key, add 2 spaces at the current text cursor position
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
const text = e.currentTarget.value
|
||||
const cursorPosition = e.currentTarget.selectionStart
|
||||
const newText =
|
||||
text.slice(0, cursorPosition) +
|
||||
' ' +
|
||||
text.slice(cursorPosition)
|
||||
e.currentTarget.value = newText
|
||||
e.currentTarget.selectionStart = cursorPosition + 2
|
||||
e.currentTarget.selectionEnd = cursorPosition + 2
|
||||
}
|
||||
}}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
minRows={14}
|
||||
rows={14}
|
||||
maxRows={14}
|
||||
disableAutosize={true}
|
||||
size="lg"
|
||||
onClear={() => {
|
||||
setConfigContent(null)
|
||||
}}
|
||||
data-focus-visible="false"
|
||||
/>
|
||||
</form>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
color="danger"
|
||||
variant="light"
|
||||
onPress={close}
|
||||
data-focus-visible="false"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
form="config-form"
|
||||
isDisabled={isSaving}
|
||||
data-focus-visible="false"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Import'}
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</>
|
||||
)}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerContent,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
Input,
|
||||
Textarea,
|
||||
} from '@heroui/react'
|
||||
import { Button } from '@heroui/react'
|
||||
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { getConfigPath } from '../../lib/rclone/api'
|
||||
import { usePersistedStore } from '../../lib/store'
|
||||
|
||||
export default function ConfigEditDrawer({
|
||||
id,
|
||||
onClose,
|
||||
isOpen,
|
||||
}: {
|
||||
id?: string | null
|
||||
onClose: () => void
|
||||
isOpen: boolean
|
||||
}) {
|
||||
const configFiles = usePersistedStore((state) => state.configFiles)
|
||||
const initialConfig = useMemo(() => configFiles.find((c) => c.id === id), [configFiles, id])
|
||||
|
||||
const [configLabel, setConfigLabel] = useState<string | null>(null)
|
||||
const [configPass, setConfigPass] = useState<string | null>(null)
|
||||
const [configContent, setConfigContent] = useState<string | null>(null)
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const isEncrypted = useMemo(() => {
|
||||
return configContent?.includes('RCLONE_ENCRYPT_V0:')
|
||||
}, [configContent])
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
async ({ label, pass, content }: { label?: string; pass?: string; content?: string }) => {
|
||||
if (!id) return
|
||||
|
||||
try {
|
||||
if (!label) {
|
||||
throw new Error('Label is required')
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
throw new Error('Content is required')
|
||||
}
|
||||
|
||||
if (content.includes('RCLONE_ENCRYPT_V0:') && !pass) {
|
||||
throw new Error('Password is required for encrypted configs')
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
|
||||
const configPath = await getConfigPath({ id: id, validate: true })
|
||||
await writeTextFile(configPath, content)
|
||||
|
||||
usePersistedStore.getState().updateConfigFile(id, {
|
||||
label,
|
||||
pass: pass || undefined,
|
||||
isEncrypted: content.includes('RCLONE_ENCRYPT_V0:'),
|
||||
})
|
||||
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
},
|
||||
[id, onClose]
|
||||
)
|
||||
|
||||
const initializeConfig = useCallback(async () => {
|
||||
if (!initialConfig) {
|
||||
return
|
||||
}
|
||||
|
||||
const configPath = await getConfigPath({ id: initialConfig.id!, validate: true })
|
||||
const text = await readTextFile(configPath)
|
||||
setConfigContent(text)
|
||||
setConfigLabel(initialConfig.label)
|
||||
setConfigPass(initialConfig.pass || null)
|
||||
}, [initialConfig])
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && configContent === null && configLabel === null) {
|
||||
initializeConfig()
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
setConfigContent(null)
|
||||
setConfigLabel(null)
|
||||
setConfigPass(null)
|
||||
}
|
||||
}, [isOpen, initializeConfig, configLabel, configContent])
|
||||
|
||||
if (!id) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
placement={'bottom'}
|
||||
size="full"
|
||||
onClose={onClose}
|
||||
hideCloseButton={true}
|
||||
>
|
||||
<DrawerContent>
|
||||
{(close) => (
|
||||
<>
|
||||
<DrawerHeader className="flex flex-col gap-1">
|
||||
Edit {configLabel}
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<form
|
||||
id="config-form"
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleUpdate({
|
||||
label: configLabel || undefined,
|
||||
pass: configPass || undefined,
|
||||
content: configContent || undefined,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
name="label"
|
||||
label="Name"
|
||||
labelPlacement="outside"
|
||||
placeholder="Enter a name for your config"
|
||||
type="text"
|
||||
value={configLabel || ''}
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
onValueChange={(value) => {
|
||||
setConfigLabel(value)
|
||||
}}
|
||||
isClearable={true}
|
||||
onClear={() => {
|
||||
setConfigLabel(null)
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
{isEncrypted && (
|
||||
<Input
|
||||
name="label"
|
||||
label="Password"
|
||||
labelPlacement="outside"
|
||||
placeholder="Enter the password for your config file"
|
||||
type="password"
|
||||
value={configPass || ''}
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
onValueChange={(value) => {
|
||||
setConfigPass(value)
|
||||
}}
|
||||
isClearable={true}
|
||||
onClear={() => {
|
||||
setConfigPass(null)
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
className="w-full"
|
||||
name="content"
|
||||
label={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="text-medium">Config</p>
|
||||
</div>
|
||||
}
|
||||
labelPlacement="outside"
|
||||
placeholder="Update your config here"
|
||||
value={configContent || ''}
|
||||
onValueChange={(value) => {
|
||||
console.log(value)
|
||||
setConfigContent(value)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
//if it's tab key, add 2 spaces at the current text cursor position
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
const text = e.currentTarget.value
|
||||
const cursorPosition = e.currentTarget.selectionStart
|
||||
const newText =
|
||||
text.slice(0, cursorPosition) +
|
||||
' ' +
|
||||
text.slice(cursorPosition)
|
||||
e.currentTarget.value = newText
|
||||
e.currentTarget.selectionStart = cursorPosition + 2
|
||||
e.currentTarget.selectionEnd = cursorPosition + 2
|
||||
}
|
||||
}}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
minRows={14}
|
||||
rows={14}
|
||||
maxRows={14}
|
||||
disableAutosize={true}
|
||||
size="lg"
|
||||
onClear={() => {
|
||||
setConfigContent(null)
|
||||
}}
|
||||
data-focus-visible="false"
|
||||
/>
|
||||
</form>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
color="danger"
|
||||
variant="light"
|
||||
onPress={close}
|
||||
data-focus-visible="false"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
form="config-form"
|
||||
isDisabled={isSaving}
|
||||
data-focus-visible="false"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</>
|
||||
)}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
+201
-1
@@ -1,22 +1,27 @@
|
||||
import { Button, Card, CardBody, Checkbox, Chip, Input, Tab, Tabs } from '@heroui/react'
|
||||
import { ask, message } from '@tauri-apps/plugin-dialog'
|
||||
import { remove } from '@tauri-apps/plugin-fs'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { relaunch } from '@tauri-apps/plugin-process'
|
||||
import { type Update, check } from '@tauri-apps/plugin-updater'
|
||||
import {
|
||||
CheckIcon,
|
||||
CodeIcon,
|
||||
CogIcon,
|
||||
EyeIcon,
|
||||
MedalIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
ServerIcon,
|
||||
Trash2Icon,
|
||||
} from 'lucide-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { revokeLicense, validateLicense } from '../../lib/license'
|
||||
import { deleteRemote } from '../../lib/rclone/api'
|
||||
import { deleteRemote, getConfigPath } from '../../lib/rclone/api'
|
||||
import { usePersistedStore, useStore } from '../../lib/store'
|
||||
import { triggerTrayRebuild } from '../../lib/tray'
|
||||
import ConfigCreateDrawer from '../components/ConfigCreateDrawer'
|
||||
import ConfigEditDrawer from '../components/ConfigEditDrawer'
|
||||
import RemoteCreateDrawer from '../components/RemoteCreateDrawer'
|
||||
import RemoteDefaultsDrawer from '../components/RemoteDefaultsDrawer'
|
||||
import RemoteEditDrawer from '../components/RemoteEditDrawer'
|
||||
@@ -131,6 +136,19 @@ function Settings() {
|
||||
>
|
||||
<LicenseSection />
|
||||
</Tab>
|
||||
<Tab
|
||||
key="config"
|
||||
title={
|
||||
<div className="flex items-center space-x-2">
|
||||
<CodeIcon className="w-5 h-5" />
|
||||
<span>Config</span>
|
||||
</div>
|
||||
}
|
||||
data-focus-visible="false"
|
||||
className="w-full max-h-screen p-0 overflow-scroll overscroll-none"
|
||||
>
|
||||
<ConfigSection />
|
||||
</Tab>
|
||||
{/* <Tab
|
||||
key="hosts"
|
||||
title={
|
||||
@@ -798,6 +816,188 @@ function RemotesSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function ConfigSection() {
|
||||
const configFiles = usePersistedStore((state) => state.configFiles)
|
||||
const activeConfigFile = usePersistedStore((state) => state.activeConfigFile)
|
||||
|
||||
const [isCreateDrawerOpen, setIsCreateDrawerOpen] = useState(false)
|
||||
const [isEditDrawerOpen, setIsEditDrawerOpen] = useState(false)
|
||||
const [focusedConfigId, setFocusedConfigId] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<BaseHeader
|
||||
title="Config"
|
||||
endContent={
|
||||
<Button
|
||||
onPress={async () => {
|
||||
// setFocusedConfig({
|
||||
// id: undefined,
|
||||
// label: filePath.split('/').pop() || 'New Config',
|
||||
// isEncrypted: configText.includes('RCLONE_ENCRYPT_V0:'),
|
||||
// pass: undefined,
|
||||
// sync: undefined,
|
||||
// })
|
||||
setIsCreateDrawerOpen(true)
|
||||
|
||||
// await ask(
|
||||
// 'Config loaded successfully, you can now remove the file.',
|
||||
// {
|
||||
// title: 'Success',
|
||||
// kind: 'info',
|
||||
// okLabel: 'OK',
|
||||
// cancelLabel: '',
|
||||
// }
|
||||
// )
|
||||
// } catch (error) {
|
||||
// console.error(error)
|
||||
// await ask('Failed to load config file', {
|
||||
// title: 'Error',
|
||||
// kind: 'error',
|
||||
// okLabel: 'OK',
|
||||
// cancelLabel: '',
|
||||
// })
|
||||
// }
|
||||
}}
|
||||
isIconOnly={true}
|
||||
variant="faded"
|
||||
color="primary"
|
||||
data-focus-visible="false"
|
||||
size="sm"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
{configFiles.map((configFile) => (
|
||||
<Button
|
||||
key={configFile.id}
|
||||
className="flex flex-row justify-start w-full gap-2"
|
||||
color={configFile.id === activeConfigFile?.id ? 'primary' : 'default'}
|
||||
onPress={() => {
|
||||
if (configFile.id === activeConfigFile?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
const confirmed = await ask(
|
||||
'This will cancel any active jobs or transfers and restart the app',
|
||||
{
|
||||
title: `Switch to config ${configFile.label}?`,
|
||||
kind: 'info',
|
||||
okLabel: 'OK',
|
||||
cancelLabel: 'Cancel',
|
||||
}
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
usePersistedStore.getState().setActiveConfigFile(configFile.id!)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
await relaunch()
|
||||
}, 100)
|
||||
}}
|
||||
>
|
||||
<p>{configFile.label}</p>
|
||||
<div className="flex-1" />
|
||||
<div className="flex flex-row gap-2">
|
||||
<Button
|
||||
isIconOnly={true}
|
||||
variant="light"
|
||||
size="sm"
|
||||
onPress={() => {
|
||||
setFocusedConfigId(configFile.id!)
|
||||
setIsEditDrawerOpen(true)
|
||||
}}
|
||||
isDisabled={
|
||||
configFile.id === 'default' ||
|
||||
configFile.id === activeConfigFile?.id
|
||||
}
|
||||
>
|
||||
<PencilIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly={true}
|
||||
variant="light"
|
||||
size="sm"
|
||||
onPress={() => {
|
||||
setTimeout(async () => {
|
||||
const confirmed = await ask(
|
||||
`Are you sure you want to delete config ${configFile.label}?`,
|
||||
{
|
||||
title: 'Delete Config',
|
||||
kind: 'warning',
|
||||
okLabel: 'Delete',
|
||||
cancelLabel: 'Cancel',
|
||||
}
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (configFile.id === 'default') {
|
||||
await ask('Default config cannot be deleted', {
|
||||
title: 'Error',
|
||||
kind: 'warning',
|
||||
okLabel: 'OK',
|
||||
cancelLabel: '',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const path = await getConfigPath({
|
||||
id: configFile.id!,
|
||||
validate: true,
|
||||
})
|
||||
|
||||
await remove(path.replace('rclone.conf', ''), {
|
||||
recursive: true,
|
||||
})
|
||||
|
||||
if (activeConfigFile?.id === configFile.id) {
|
||||
usePersistedStore
|
||||
.getState()
|
||||
.setActiveConfigFile('default')
|
||||
}
|
||||
|
||||
usePersistedStore
|
||||
.getState()
|
||||
.removeConfigFile(configFile.id!)
|
||||
}, 100)
|
||||
}}
|
||||
isDisabled={configFile.id === 'default'}
|
||||
>
|
||||
<Trash2Icon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ConfigCreateDrawer
|
||||
isOpen={isCreateDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsCreateDrawerOpen(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfigEditDrawer
|
||||
isOpen={isEditDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsEditDrawerOpen(false)
|
||||
setFocusedConfigId(null)
|
||||
}}
|
||||
id={focusedConfigId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BaseHeader({ title, endContent }: { title: string; endContent?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="sticky top-0 z-50 flex items-center justify-between p-4 h-14 bg-neutral-900/50 backdrop-blur-lg">
|
||||
|
||||
Reference in New Issue
Block a user