import template with deep link
This commit is contained in:
+35
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !initialValues) return
|
||||
setImportString(initialValues.cmd ?? '')
|
||||
setName(initialValues.name ?? '')
|
||||
}, [isOpen, initialValues])
|
||||
|
||||
const [configOptionsJson, setConfigOptionsJson] = useState<string>('{}')
|
||||
const [copyOptionsJson, setCopyOptionsJson] = useState<string>('{}')
|
||||
const [syncOptionsJson, setSyncOptionsJson] = useState<string>('{}')
|
||||
@@ -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"
|
||||
|
||||
+33
-3
@@ -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<AddTemplatePayload | null>(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<AddTemplatePayload>(ADD_TEMPLATE, (event) => {
|
||||
const { cmd, name } = event.payload ?? {}
|
||||
setAddPayload(cmd || name ? { cmd, name } : null)
|
||||
onOpen()
|
||||
})
|
||||
return () => {
|
||||
unlisten.then((fn) => fn())
|
||||
}
|
||||
}, [onOpen])
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col h-screen', platform() === 'macos' && 'pt-7')}>
|
||||
<div className="flex flex-row items-center justify-between w-full px-6 py-4">
|
||||
@@ -379,7 +405,11 @@ export default function Templates() {
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<TemplateAddDrawer isOpen={isOpen} onClose={onOpenChange} />
|
||||
<TemplateAddDrawer
|
||||
isOpen={isOpen}
|
||||
onClose={handleAddClose}
|
||||
initialValues={addPayload}
|
||||
/>
|
||||
{selectedTemplate && (
|
||||
<TemplateEditDrawer
|
||||
isOpen={isEditOpen}
|
||||
|
||||
Reference in New Issue
Block a user