Files
rclone-ui/src/components/ConfigCreateDrawer.tsx
T

337 lines
16 KiB
TypeScript

import {
Drawer,
DrawerBody,
DrawerContent,
DrawerFooter,
DrawerHeader,
Input,
Switch,
Textarea,
} from '@heroui/react'
import { Button } from '@heroui/react'
import { sep } from '@tauri-apps/api/path'
import { message, open } from '@tauri-apps/plugin-dialog'
import { mkdir, readTextFile, writeTextFile } from '@tauri-apps/plugin-fs'
import { UploadIcon } from 'lucide-react'
import { useState } from 'react'
import { getConfigPath } from '../../lib/rclone/common'
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 [isPasswordCommand, setIsPasswordCommand] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const isEncrypted = configContent?.includes('RCLONE_ENCRYPT_V0:')
async function handleCreate({
label,
pass,
passCommand,
content,
isPasswordCommand,
isEncrypted,
}: {
label?: string
pass?: string
passCommand?: string
content: string | null
isPasswordCommand: boolean
isEncrypted: boolean
}) {
if (!label) {
await message('Label is required', {
title: 'Failed to save config',
kind: 'error',
okLabel: 'OK',
})
return
}
if (!content) {
await message('Content is required', {
title: 'Failed to save config',
kind: 'error',
okLabel: 'OK',
})
return
}
if (isEncrypted && isPasswordCommand && !passCommand) {
await message('Password command is required for encrypted configs', {
title: 'Failed to save config',
kind: 'error',
okLabel: 'OK',
})
return
}
setIsSaving(true)
const savedPass = isPasswordCommand ? undefined : pass
const savedPassCommand = isPasswordCommand ? passCommand : undefined
try {
const generatedId = crypto.randomUUID()
const configPath = await getConfigPath({ id: generatedId, validate: false })
await mkdir(configPath.replace(sep() + 'rclone.conf', ''), {
recursive: true,
})
await writeTextFile(configPath, content)
console.log('[handleCreate] saved config to', configPath)
usePersistedStore.getState().addConfigFile({
id: generatedId,
label,
pass: savedPass,
passCommand: savedPassCommand,
isEncrypted: isEncrypted,
sync: undefined,
})
onClose()
} catch (error) {
console.error('[handleCreate] failed to save config', error)
await message(error instanceof Error ? error.message : 'An unknown error occurred', {
title: 'Failed to save config',
kind: 'error',
okLabel: 'OK',
})
}
setIsSaving(false)
}
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,
passCommand: config.passCommand,
content: configContent,
isPasswordCommand: isPasswordCommand,
isEncrypted:
configContent?.includes('RCLONE_ENCRYPT_V0:') || false,
})
}}
>
<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={
<div className="flex items-center gap-1.5">
<p className="text-medium">Password</p>
<Switch
size="sm"
isSelected={isPasswordCommand}
onValueChange={() =>
setIsPasswordCommand(!isPasswordCommand)
}
color="primary"
>
Command
</Switch>
</div>
}
labelPlacement="outside"
placeholder={
isPasswordCommand
? 'Enter the password command for your config file'
: 'Leave blank to be prompted on every startup'
}
type={isPasswordCommand ? 'text' : 'password'}
value={isPasswordCommand ? config.passCommand : config.pass}
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
onValueChange={(value) => {
setConfig({
...config,
...(isPasswordCommand
? { passCommand: value }
: { pass: value }),
})
}}
isClearable={true}
onClear={() => {
setConfig({
...config,
...(isPasswordCommand
? { passCommand: '' }
: { 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(sep()).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>
)
}