diff --git a/lib/menu.ts b/lib/menu.ts index eabb817..9c503a1 100644 --- a/lib/menu.ts +++ b/lib/menu.ts @@ -511,6 +511,20 @@ export async function buildMenu() { menuItems.push(syncMenuItem) } + if (!persistedStoreState.disabledActions?.includes('tray-download')) { + const downloadMenuItem = await MenuItem.new({ + id: 'download', + text: 'Download', + action: async () => { + await openWindow({ + name: 'Download', + url: '/download', + }) + }, + }) + // menuItems.push(downloadMenuItem) + } + const allowsAdditional = !persistedStoreState.disabledActions?.includes('tray-move') || !persistedStoreState.disabledActions?.includes('tray-serve') || diff --git a/lib/rclone/api.ts b/lib/rclone/api.ts index d2e35ee..dcad566 100644 --- a/lib/rclone/api.ts +++ b/lib/rclone/api.ts @@ -711,6 +711,36 @@ export async function startPurge({ } } +export async function startDownload({ + fs, + remote, + url, + autoFilename = true, +}: { + fs: string // a remote name string e.g. "drive:" + remote: string // a path within that remote e.g. "dir" + url: string // string, URL to read from + autoFilename?: boolean // boolean, set to true to retrieve destination file name from url +}) { + const params = new URLSearchParams() + params.set('fs', fs) + params.set('remote', remote) + params.set('url', url) + params.set('autoFilename', autoFilename.toString()) + params.set('_async', 'true') + + const r = await fetch(`http://localhost:5572/operations/copyurl?${params.toString()}`, { + method: 'POST', + headers: getAuthHeader(), + }) + + console.log('[startDownload] operation started:', r) + + if (!r.ok) { + throw new Error('Failed to start download job') + } +} + /* FLAGS */ export async function getCurrentGlobalFlags() { console.log('[getCurrentGlobalFlags]') diff --git a/lib/store.ts b/lib/store.ts index 89a40de..35f50be 100644 --- a/lib/store.ts +++ b/lib/store.ts @@ -50,31 +50,24 @@ interface State { startupDisplayed: boolean } +type SupportedAction = + | 'tray-mount' + | 'tray-sync' + | 'tray-copy' + | 'tray-serve' + | 'tray-move' + | 'tray-delete' + | 'tray-purge' + | 'tray-download' + interface PersistedState { remoteConfigList: Record setRemoteConfig: (remote: string, config: RemoteConfig) => void mergeRemoteConfig: (remote: string, config: RemoteConfig) => void - disabledActions: ( - | 'tray-mount' - | 'tray-sync' - | 'tray-copy' - | 'tray-serve' - | 'tray-move' - | 'tray-delete' - | 'tray-purge' - )[] - setDisabledActions: ( - actions: ( - | 'tray-mount' - | 'tray-sync' - | 'tray-copy' - | 'tray-serve' - | 'tray-move' - | 'tray-delete' - | 'tray-purge' - )[] - ) => void + disabledActions: SupportedAction[] + + setDisabledActions: (actions: SupportedAction[]) => void proxy: | { @@ -175,17 +168,8 @@ export const usePersistedStore = create()( })), disabledActions: [], - setDisabledActions: ( - actions: ( - | 'tray-mount' - | 'tray-sync' - | 'tray-copy' - | 'tray-serve' - | 'tray-move' - | 'tray-delete' - | 'tray-purge' - )[] - ) => set((_) => ({ disabledActions: actions })), + setDisabledActions: (actions: SupportedAction[]) => + set((_) => ({ disabledActions: actions })), favoritePaths: [], diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 073534e..656fdc1 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -14,6 +14,7 @@ "Jobs", "Cron", "Serve", + "Download", "Browse", "Startup", "Test" diff --git a/src/main.tsx b/src/main.tsx index 6ba1f2e..6019dfb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,6 +8,7 @@ import { debug, error, info, trace, warn } from '@tauri-apps/plugin-log' import Copy from './pages/Copy' import Cron from './pages/Cron' import Delete from './pages/Delete' +import Download from './pages/Download' import Jobs from './pages/Jobs' import Mount from './pages/Mount' import Move from './pages/Move' @@ -70,6 +71,10 @@ const router = createBrowserRouter([ path: '/purge', element: , }, + { + path: '/download', + element: , + }, { path: '/serve', element: , diff --git a/src/pages/Download.tsx b/src/pages/Download.tsx new file mode 100644 index 0000000..bbc8bc9 --- /dev/null +++ b/src/pages/Download.tsx @@ -0,0 +1,179 @@ +import { Button, Input } from '@heroui/react' +import { getCurrentWindow } from '@tauri-apps/api/window' +import { message } from '@tauri-apps/plugin-dialog' +import { DownloadIcon, FoldersIcon, XIcon } from 'lucide-react' +import { useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import { startDownload } from '../../lib/rclone/api' +import { PathField } from '../components/PathFinder' + +export default function Download() { + const [searchParams] = useSearchParams() + + const [url, setUrl] = useState() + const [source, setSource] = useState( + searchParams.get('initialSource') ? searchParams.get('initialSource')! : undefined + ) + + const [isStarted, setIsStarted] = useState(false) + const [isLoading, setIsLoading] = useState(false) + + async function handleStartDownload() { + setIsLoading(true) + + if (!url) { + await message('Please enter a URL', { + title: 'Error', + kind: 'error', + }) + setIsLoading(false) + return + } + + if (!source) { + await message('Please select a destination path', { + title: 'Error', + kind: 'error', + }) + setIsLoading(false) + return + } + + const [fs, remote] = source.split(':') + + if (!fs || !remote) { + await message('Invalid destination path', { + title: 'Error', + kind: 'error', + }) + setIsLoading(false) + return + } + + try { + await startDownload({ + fs, + remote, + url, + }) + + setIsStarted(true) + + await message('Download job started', { + title: 'Success', + okLabel: 'OK', + }) + setIsLoading(false) + } catch (error) { + await message(`Failed to start download job, ${error}`, { + title: 'Error', + kind: 'error', + okLabel: 'OK', + }) + setIsLoading(false) + } + } + + const buttonText = (() => { + if (isLoading) return 'STARTING...' + if (!source) return 'Please select a destination path' + return 'DOWNLOAD' + })() + + const buttonIcon = (() => { + if (isLoading) return + if (!source) return + return + })() + + return ( +
+ {/* Main Content */} +
+ setUrl(e.target.value)} + fullWidth={true} + size="lg" + data-focus-visible="false" + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + endContent={ + + } + /> + + {/* Path Display */} + +
+ +
+ {isStarted ? ( + <> + + + + + ) : ( + + )} +
+
+ ) +} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 86fbb35..edca4d8 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -573,6 +573,22 @@ function GeneralSection() { > Show Move option + { + if (value) { + setDisabledActions( + disabledActions?.filter( + (action) => action !== 'tray-download' + ) || [] + ) + } else { + setDisabledActions([...(disabledActions || []), 'tray-download']) + } + }} + > + Show Download option + {