From 3100d9388ffe4e6cbba38404c359730c0290bf8d Mon Sep 17 00:00:00 2001 From: FTCHD <144691102+FTCHD@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:40:25 +0300 Subject: [PATCH] import template with deep link --- lib/deep.ts | 39 +++++++++++++++++++++++++--- lib/events.ts | 10 +++++++ src/components/TemplateAddDrawer.tsx | 18 +++++++++++++ src/pages/Templates.tsx | 36 ++++++++++++++++++++++--- 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/lib/deep.ts b/lib/deep.ts index 0ea313a..eedcae0 100644 --- a/lib/deep.ts +++ b/lib/deep.ts @@ -1,5 +1,10 @@ +import { emitTo } from '@tauri-apps/api/event' +import { WebviewWindow } from '@tauri-apps/api/webviewWindow' +import { ADD_TEMPLATE, type AddTemplatePayload } from './events' import { openWindow } from './window' +const TEMPLATES_WINDOW = 'Templates' + export function getDeepLinkUrl(url: string) { let cleanedUrl = url.replace('rclone:', '') while (cleanedUrl.startsWith('/')) { @@ -8,12 +13,38 @@ export function getDeepLinkUrl(url: string) { return cleanedUrl } -export function handleDeepLinkUrl(url: string) { +export async function handleDeepLinkUrl(url: string) { console.log('deep link url', url) - const domain = url.split('/')[0] + // `add-template?cmd=…` — split the query off before matching the route, and tolerate a + // trailing slash (`add-template/?cmd=…`) that some platforms add. + const queryIndex = url.indexOf('?') + const route = queryIndex === -1 ? url : url.slice(0, queryIndex) + const params = new URLSearchParams(queryIndex === -1 ? '' : url.slice(queryIndex + 1)) + const domain = route.split('/')[0] - if (domain === 'add-template') { - return openWindow({ name: 'Templates', url: '/templates?action=add' }) + try { + if (domain === 'add-template') { + const payload: AddTemplatePayload = { + cmd: params.get('cmd')?.trim() || undefined, + name: params.get('name')?.trim() || undefined, + } + + // open_window only focuses an existing window (it never re-navigates), so a live + // Templates window gets the payload over the event bus instead of the URL. + const existing = await WebviewWindow.getByLabel(TEMPLATES_WINDOW) + if (existing) { + await emitTo(TEMPLATES_WINDOW, ADD_TEMPLATE, payload) + await openWindow({ name: TEMPLATES_WINDOW, url: '/templates' }) + return + } + + const search = new URLSearchParams({ action: 'add' }) + if (payload.cmd) search.set('cmd', payload.cmd) + if (payload.name) search.set('name', payload.name) + await openWindow({ name: TEMPLATES_WINDOW, url: `/templates?${search}` }) + } + } catch (error) { + console.error('[handleDeepLinkUrl] failed to handle deep link', error) } } diff --git a/lib/events.ts b/lib/events.ts index c9bfec7..090529a 100644 --- a/lib/events.ts +++ b/lib/events.ts @@ -21,6 +21,16 @@ export interface RestartRclonePayload { syncConfigLinkTarget?: string | null } +// Deep-link payload forwarded from the main window to an already-open 'Templates' window +// (lib/deep.ts → src/pages/Templates.tsx). Not part of AppEventPayload: it targets a specific +// window via emitTo, not the main-window listeners. +export const ADD_TEMPLATE = 'add-template' + +export interface AddTemplatePayload { + cmd?: string + name?: string +} + export type AppEventPayload = { [CLOSE_APP]: undefined [RELAUNCH_APP]: undefined diff --git a/src/components/TemplateAddDrawer.tsx b/src/components/TemplateAddDrawer.tsx index 860af57..c415359 100644 --- a/src/components/TemplateAddDrawer.tsx +++ b/src/components/TemplateAddDrawer.tsx @@ -32,6 +32,7 @@ import { import { startTransition, useEffect, useMemo, useState } from 'react' import { useDebounce } from 'use-debounce' import { formatErrorMessage } from '../../lib/errors' +import type { AddTemplatePayload } from '../../lib/events' import { FLAG_CATEGORIES, findFlagOption, @@ -64,9 +65,13 @@ function stripQuotes(value: string): string { export default function TemplateAddDrawer({ isOpen, onClose, + initialValues, }: { isOpen: boolean onClose: () => void + // Deep-link prefill (rclone://add-template?cmd=…). A fresh object arrives per link, so a + // repeated identical link still re-applies. + initialValues?: AddTemplatePayload | null }) { const { globalFlags, @@ -86,6 +91,12 @@ export default function TemplateAddDrawer({ const [name, setName] = useState('') const [tags, setTags] = useState([]) + useEffect(() => { + if (!isOpen || !initialValues) return + setImportString(initialValues.cmd ?? '') + setName(initialValues.name ?? '') + }, [isOpen, initialValues]) + const [configOptionsJson, setConfigOptionsJson] = useState('{}') const [copyOptionsJson, setCopyOptionsJson] = useState('{}') const [syncOptionsJson, setSyncOptionsJson] = useState('{}') @@ -152,6 +163,11 @@ export default function TemplateAddDrawer({ }, onSuccess: () => { onClose() + // Clearing the command matters: re-importing the same command later hits the cached + // parseFlags query (same `data` reference), so the section-populate effect would not + // re-fire against the freshly reset sections. + setImportString('') + setImportedCount(null) setName('') setTags([]) setMountOptionsJson('{}') @@ -324,6 +340,7 @@ export default function TemplateAddDrawer({ label="Import from command" labelPlacement="outside" placeholder="rclone copy --vfs-cache-mode writes ..." + value={importString} onValueChange={(value) => setImportString(value)} size="lg" data-focus-visible="false" @@ -364,6 +381,7 @@ export default function TemplateAddDrawer({ label="Name" labelPlacement="outside" placeholder="My Template" + value={name} onValueChange={(value) => setName(value)} size="lg" data-focus-visible="false" diff --git a/src/pages/Templates.tsx b/src/pages/Templates.tsx index 2010a69..493b013 100644 --- a/src/pages/Templates.tsx +++ b/src/pages/Templates.tsx @@ -12,6 +12,7 @@ import { useDisclosure, } from '@heroui/react' import { useMutation } from '@tanstack/react-query' +import { listen } from '@tauri-apps/api/event' import { ask, save } from '@tauri-apps/plugin-dialog' import { writeTextFile } from '@tauri-apps/plugin-fs' import { openUrl, revealItemInDir } from '@tauri-apps/plugin-opener' @@ -24,8 +25,9 @@ import { TrashIcon, XIcon, } from 'lucide-react' -import { startTransition, useEffect, useMemo, useState } from 'react' +import { startTransition, useCallback, useEffect, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' +import { ADD_TEMPLATE, type AddTemplatePayload } from '../../lib/events' import { usePersistedStore } from '../../store/persisted' import type { Template } from '../../types/template' import TemplateAddDrawer from '../components/TemplateAddDrawer' @@ -34,7 +36,13 @@ import TemplateEditDrawer from '../components/TemplateEditDrawer' export default function Templates() { const [searchParams] = useSearchParams() - const { isOpen, onOpen, onOpenChange } = useDisclosure() + const { isOpen, onOpen, onClose: onAddClose } = useDisclosure() + // Deep-link prefill for the add drawer (rclone://add-template?cmd=…), see lib/deep.ts. + const [addPayload, setAddPayload] = useState(null) + const handleAddClose = useCallback(() => { + onAddClose() + setAddPayload(null) + }, [onAddClose]) const { isOpen: isEditOpen, onOpen: onEditOpen, @@ -160,10 +168,28 @@ export default function Templates() { useEffect(() => { const action = searchParams.get('action') if (action === 'add') { + const cmd = searchParams.get('cmd')?.trim() || undefined + const name = searchParams.get('name')?.trim() || undefined + if (cmd || name) { + setAddPayload({ cmd, name }) + } onOpen() } }, [searchParams, onOpen]) + // A deep link arriving while this window is already open can't change its URL, so the main + // window forwards the payload over the event bus instead (lib/deep.ts). + useEffect(() => { + const unlisten = listen(ADD_TEMPLATE, (event) => { + const { cmd, name } = event.payload ?? {} + setAddPayload(cmd || name ? { cmd, name } : null) + onOpen() + }) + return () => { + unlisten.then((fn) => fn()) + } + }, [onOpen]) + return (
@@ -379,7 +405,11 @@ export default function Templates() { /> - + {selectedTemplate && (