1307 lines
45 KiB
TypeScript
1307 lines
45 KiB
TypeScript
import { captureException } from '@sentry/browser'
|
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
|
import { ask, message } from '@tauri-apps/plugin-dialog'
|
|
import { openUrl, revealItemInDir } from '@tauri-apps/plugin-opener'
|
|
import notify from '../lib/notify'
|
|
import queryClient from '../lib/query'
|
|
import type { fetchMountList, fetchServeList } from '../lib/rclone/api'
|
|
import rclone from '../lib/rclone/client'
|
|
import { SERVE_TYPES, SUPPORTS_CLEANUP, SUPPORTS_PURGE } from '../lib/rclone/constants'
|
|
import { openFullWindow } from '../lib/window'
|
|
import { usePersistedStore } from '../store/persisted'
|
|
import { COMMAND_CONFIG, COMMAND_DESCRIPTIONS, COMMAND_KEYWORDS } from './constants'
|
|
import type {
|
|
ToolbarActionArgs,
|
|
ToolbarActionDefinition,
|
|
ToolbarActionOnPressContext,
|
|
ToolbarActionPath,
|
|
ToolbarActionResult,
|
|
ToolbarCommandId,
|
|
} from './types'
|
|
import { formatMountLabel, formatServeInfo, formatServeLabel } from './utils'
|
|
|
|
const WHITESPACE_SPLIT = /\s+/
|
|
const TOKEN_TRIM_REGEX = /^[\"'`]+|[\"'`.,;!?]+$/g
|
|
const SIMPLE_URL_REGEX = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}([/:?][^\s]*)?$/
|
|
const TRAILING_SLASH_REGEX = /\/+$/
|
|
|
|
const actions: ToolbarActionDefinition[] = [
|
|
{
|
|
id: 'copy',
|
|
label: 'Copy',
|
|
description: COMMAND_DESCRIPTIONS.copy,
|
|
keywords: COMMAND_KEYWORDS.copy,
|
|
getDefaultResult: () => createBaseResult('Copy', COMMAND_DESCRIPTIONS.copy, {}, 50),
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.copy)) {
|
|
return []
|
|
}
|
|
|
|
if (paths.length === 0) {
|
|
return [createBaseResult('Copy', COMMAND_DESCRIPTIONS.copy, {}, 50)]
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
|
|
if (paths.length === 2) {
|
|
const [source, destination] = paths
|
|
results.push(
|
|
createBaseResult(
|
|
`Copy ${source.readable} → ${destination.readable}`,
|
|
COMMAND_DESCRIPTIONS.copy,
|
|
{
|
|
initialSource: normalizePathForArgs(source),
|
|
initialDestination: normalizePathForArgs(destination),
|
|
},
|
|
200
|
|
)
|
|
)
|
|
}
|
|
|
|
for (let index = 0; index < paths.length; index += 1) {
|
|
const path = paths[index]
|
|
const isDestination = paths.length > 1 && index === paths.length - 1
|
|
const args: ToolbarActionArgs = isDestination
|
|
? { initialDestination: normalizePathForArgs(path) }
|
|
: { initialSource: normalizePathForArgs(path) }
|
|
const score = path.isLocal ? 140 : isDestination ? 155 : 160
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`Copy ${path.readable}`,
|
|
COMMAND_DESCRIPTIONS.copy,
|
|
args,
|
|
score
|
|
)
|
|
)
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('copy', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'move',
|
|
label: 'Move',
|
|
description: COMMAND_DESCRIPTIONS.move,
|
|
keywords: COMMAND_KEYWORDS.move,
|
|
getDefaultResult: () => createBaseResult('Move', COMMAND_DESCRIPTIONS.move, {}, 48),
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.move)) {
|
|
return []
|
|
}
|
|
|
|
if (paths.length === 0) {
|
|
return [createBaseResult('Move', COMMAND_DESCRIPTIONS.move, {}, 48)]
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
|
|
if (paths.length === 2) {
|
|
const [source, destination] = paths
|
|
results.push(
|
|
createBaseResult(
|
|
`Move ${source.readable} → ${destination.readable}`,
|
|
COMMAND_DESCRIPTIONS.move,
|
|
{
|
|
initialSource: normalizePathForArgs(source),
|
|
initialDestination: normalizePathForArgs(destination),
|
|
},
|
|
200
|
|
)
|
|
)
|
|
}
|
|
|
|
for (let index = 0; index < paths.length; index += 1) {
|
|
const path = paths[index]
|
|
const isDestination = paths.length > 1 && index === paths.length - 1
|
|
const args: ToolbarActionArgs = isDestination
|
|
? { initialDestination: normalizePathForArgs(path) }
|
|
: { initialSource: normalizePathForArgs(path) }
|
|
const score = path.isLocal ? 140 : isDestination ? 155 : 160
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`Move ${path.readable}`,
|
|
COMMAND_DESCRIPTIONS.move,
|
|
args,
|
|
score
|
|
)
|
|
)
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('move', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'sync',
|
|
label: 'Sync',
|
|
description: COMMAND_DESCRIPTIONS.sync,
|
|
keywords: COMMAND_KEYWORDS.sync,
|
|
getDefaultResult: () => createBaseResult('Sync', COMMAND_DESCRIPTIONS.sync, {}, 46),
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.sync)) {
|
|
return []
|
|
}
|
|
|
|
if (paths.length === 0) {
|
|
return [createBaseResult('Sync', COMMAND_DESCRIPTIONS.sync, {}, 46)]
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
|
|
if (paths.length === 2) {
|
|
const [source, destination] = paths
|
|
results.push(
|
|
createBaseResult(
|
|
`Sync ${source.readable} ↔ ${destination.readable}`,
|
|
COMMAND_DESCRIPTIONS.sync,
|
|
{
|
|
initialSource: normalizePathForArgs(source),
|
|
initialDestination: normalizePathForArgs(destination),
|
|
},
|
|
200
|
|
)
|
|
)
|
|
}
|
|
|
|
for (let index = 0; index < paths.length; index += 1) {
|
|
const path = paths[index]
|
|
const isDestination = paths.length > 1 && index === paths.length - 1
|
|
const args: ToolbarActionArgs = isDestination
|
|
? { initialDestination: normalizePathForArgs(path) }
|
|
: { initialSource: normalizePathForArgs(path) }
|
|
const score = path.isLocal ? 140 : isDestination ? 155 : 160
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`Sync ${path.readable}`,
|
|
COMMAND_DESCRIPTIONS.sync,
|
|
args,
|
|
score
|
|
)
|
|
)
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('sync', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'bisync',
|
|
label: 'Bisync',
|
|
description: COMMAND_DESCRIPTIONS.bisync,
|
|
keywords: COMMAND_KEYWORDS.bisync,
|
|
getDefaultResult: () => createBaseResult('Bisync', COMMAND_DESCRIPTIONS.bisync, {}, 44),
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.bisync)) {
|
|
return []
|
|
}
|
|
|
|
if (paths.length === 0) {
|
|
return [createBaseResult('Bisync', COMMAND_DESCRIPTIONS.bisync, {}, 44)]
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
|
|
if (paths.length === 2) {
|
|
const [source, destination] = paths
|
|
results.push(
|
|
createBaseResult(
|
|
`Bisync ${source.readable} ↔ ${destination.readable}`,
|
|
COMMAND_DESCRIPTIONS.bisync,
|
|
{
|
|
initialSource: normalizePathForArgs(source),
|
|
initialDestination: normalizePathForArgs(destination),
|
|
},
|
|
200
|
|
)
|
|
)
|
|
}
|
|
|
|
for (let index = 0; index < paths.length; index += 1) {
|
|
const path = paths[index]
|
|
const isDestination = paths.length > 1 && index === paths.length - 1
|
|
const args: ToolbarActionArgs = isDestination
|
|
? { initialDestination: normalizePathForArgs(path) }
|
|
: { initialSource: normalizePathForArgs(path) }
|
|
const score = path.isLocal ? 140 : isDestination ? 155 : 160
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`Bisync ${path.readable}`,
|
|
COMMAND_DESCRIPTIONS.bisync,
|
|
args,
|
|
score
|
|
)
|
|
)
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('bisync', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'mount',
|
|
label: 'Mount',
|
|
description: COMMAND_DESCRIPTIONS.mount,
|
|
keywords: COMMAND_KEYWORDS.mount,
|
|
getDefaultResult: () => createBaseResult('Mount', COMMAND_DESCRIPTIONS.mount, {}, 42),
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.mount)) {
|
|
return []
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
|
|
const activeMounts = queryClient.getQueryData(['mount', 'list']) as
|
|
| Awaited<ReturnType<typeof fetchMountList>>
|
|
| undefined
|
|
|
|
if (activeMounts && activeMounts.length > 0) {
|
|
for (const mount of activeMounts) {
|
|
const mountLabel = formatMountLabel(mount)
|
|
|
|
if (usePersistedStore.getState().currentHost?.id === 'local') {
|
|
results.push(
|
|
createBaseResult(
|
|
`Open ${mountLabel}`,
|
|
'Open mount point in file explorer',
|
|
{ _action: 'open', _mountPoint: mount.MountPoint },
|
|
180
|
|
)
|
|
)
|
|
} else {
|
|
results.push(
|
|
createBaseResult(
|
|
`Copy ${mountLabel}`,
|
|
'Press Enter to copy mount details to clipboard',
|
|
{ _action: 'copy_info', _mountPoint: mount.MountPoint },
|
|
180
|
|
)
|
|
)
|
|
}
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`Stop ${mountLabel}`,
|
|
'Unmount this path',
|
|
{ _action: 'stop', _mountPoint: mount.MountPoint },
|
|
175
|
|
)
|
|
)
|
|
}
|
|
|
|
if (activeMounts.length >= 2) {
|
|
results.push(
|
|
createBaseResult(
|
|
`Stop All Mounts (${activeMounts.length} active)`,
|
|
'Unmount all active mounts',
|
|
{ _action: 'stop_all' },
|
|
170
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
if (paths.length === 0) {
|
|
results.push(createBaseResult('Mount', COMMAND_DESCRIPTIONS.mount, {}, 42))
|
|
} else {
|
|
for (const path of paths) {
|
|
const score = path.isLocal ? 140 : 160
|
|
results.push(
|
|
createBaseResult(
|
|
`Mount ${path.readable}`,
|
|
COMMAND_DESCRIPTIONS.mount,
|
|
{ initialSource: normalizePathForArgs(path) },
|
|
score
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args, context) => {
|
|
if (args._action === 'open') {
|
|
const mountPoint = args._mountPoint as string
|
|
try {
|
|
await revealItemInDir(mountPoint)
|
|
} catch (error) {
|
|
console.error('[toolbar] failed to open mount', error)
|
|
await message(
|
|
error instanceof Error ? error.message : 'Failed to open mount point',
|
|
{
|
|
title: 'Open Mount',
|
|
kind: 'error',
|
|
}
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (args._action === 'copy_info') {
|
|
const mountPoint = args._mountPoint as string
|
|
await writeText(mountPoint)
|
|
await notify({
|
|
title: 'Copied!',
|
|
body: 'Mount point copied to clipboard',
|
|
})
|
|
return
|
|
}
|
|
|
|
if (args._action === 'stop') {
|
|
const mountPoint = args._mountPoint as string
|
|
|
|
const confirmed = await ask('Are you sure you want to unmount this path?', {
|
|
title: 'Confirm Unmount',
|
|
kind: 'warning',
|
|
})
|
|
|
|
if (!confirmed) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await rclone('/mount/unmount', {
|
|
params: {
|
|
query: {
|
|
mountPoint: mountPoint,
|
|
},
|
|
},
|
|
})
|
|
await notify({
|
|
title: 'Mount Stopped',
|
|
body: `Mount point ${mountPoint} has been unmounted`,
|
|
})
|
|
queryClient.setQueryData(
|
|
['mount', 'list'],
|
|
(old: Awaited<ReturnType<typeof fetchMountList>> | undefined) =>
|
|
old?.filter((m) => m.MountPoint !== mountPoint) ?? []
|
|
)
|
|
} catch (error) {
|
|
console.error('[toolbar] failed to stop mount', error)
|
|
await message(
|
|
error instanceof Error ? error.message : 'Failed to stop mount instance',
|
|
{
|
|
title: 'Stop Mount',
|
|
kind: 'error',
|
|
}
|
|
)
|
|
await queryClient.resetQueries({ queryKey: ['mount', 'list'] })
|
|
}
|
|
return
|
|
}
|
|
|
|
if (args._action === 'stop_all') {
|
|
const confirmed = await ask('Are you sure you want to unmount ALL paths?', {
|
|
title: 'Confirm Stop All',
|
|
kind: 'warning',
|
|
})
|
|
|
|
if (!confirmed) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await rclone('/mount/unmountall')
|
|
await notify({
|
|
title: 'All Mounts Stopped',
|
|
body: 'All mount instances have been unmounted',
|
|
})
|
|
queryClient.setQueryData(['mount', 'list'], [])
|
|
} catch (error) {
|
|
console.error('[toolbar] failed to stop all mounts', error)
|
|
await message(
|
|
error instanceof Error
|
|
? error.message
|
|
: 'Failed to stop all mount instances',
|
|
{
|
|
title: 'Stop All Mounts',
|
|
kind: 'error',
|
|
}
|
|
)
|
|
await queryClient.resetQueries({ queryKey: ['mount', 'list'] })
|
|
}
|
|
return
|
|
}
|
|
|
|
await openCommandWindow('mount', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'serve',
|
|
label: 'Serve',
|
|
description: COMMAND_DESCRIPTIONS.serve,
|
|
keywords: COMMAND_KEYWORDS.serve,
|
|
getDefaultResult: () => createBaseResult('Serve', COMMAND_DESCRIPTIONS.serve, {}, 40),
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.serve)) {
|
|
return []
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
|
|
const activeServes = queryClient.getQueryData(['serve', 'list']) as
|
|
| Awaited<ReturnType<typeof fetchServeList>>
|
|
| undefined
|
|
|
|
if (activeServes && activeServes.length > 0) {
|
|
for (const serve of activeServes) {
|
|
const serveLabel = formatServeLabel(serve)
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`📋 ${serveLabel}`,
|
|
'Press Enter to copy serve details to clipboard',
|
|
{ _action: 'copy_info', _serveId: serve.id },
|
|
180
|
|
)
|
|
)
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`⏹ Stop ${serveLabel}`,
|
|
'Stop this serve instance',
|
|
{ _action: 'stop', _serveId: serve.id },
|
|
175
|
|
)
|
|
)
|
|
}
|
|
|
|
if (activeServes.length >= 2) {
|
|
results.push(
|
|
createBaseResult(
|
|
`⏹ Stop All Serves (${activeServes.length} active)`,
|
|
'Stop all running serve instances',
|
|
{ _action: 'stop_all' },
|
|
170
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
if (paths.length === 0) {
|
|
results.push(createBaseResult('Serve', COMMAND_DESCRIPTIONS.serve, {}, 40))
|
|
} else {
|
|
const protocol = findServeType(query)
|
|
|
|
for (const path of paths) {
|
|
const args: ToolbarActionArgs = protocol
|
|
? { initialSource: normalizePathForArgs(path), initialType: protocol }
|
|
: { initialSource: normalizePathForArgs(path) }
|
|
const protocolLabel = protocol ? `${protocol} ` : ''
|
|
const score = path.isLocal ? 140 : 160
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`Serve ${protocolLabel}${path.readable}`.trim(),
|
|
COMMAND_DESCRIPTIONS.serve,
|
|
args,
|
|
score
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args, context) => {
|
|
if (args._action === 'copy_info') {
|
|
const serveId = args._serveId as string
|
|
const serves = queryClient.getQueryData(['serve', 'list']) as
|
|
| Awaited<ReturnType<typeof fetchServeList>>
|
|
| undefined
|
|
const serve = serves?.find((s) => s.id === serveId)
|
|
|
|
if (serve) {
|
|
const info = formatServeInfo(serve)
|
|
await writeText(info)
|
|
await message('Serve details copied to clipboard', {
|
|
title: 'Serve Info',
|
|
kind: 'info',
|
|
})
|
|
} else {
|
|
await message('Serve instance not found. It may have been stopped.', {
|
|
title: 'Serve Info',
|
|
kind: 'error',
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
if (args._action === 'stop') {
|
|
const serveId = args._serveId as string
|
|
try {
|
|
await rclone('/serve/stop', {
|
|
params: {
|
|
query: {
|
|
id: serveId,
|
|
},
|
|
},
|
|
})
|
|
await notify({
|
|
title: 'Serve Stopped',
|
|
body: `Serve instance ${serveId} has been stopped`,
|
|
})
|
|
queryClient.setQueryData(
|
|
['serve', 'list'],
|
|
(old: Awaited<ReturnType<typeof fetchServeList>> | undefined) =>
|
|
old?.filter((s) => s.id !== serveId) ?? []
|
|
)
|
|
} catch (error) {
|
|
console.error('[toolbar] failed to stop serve', error)
|
|
await message(
|
|
error instanceof Error ? error.message : 'Failed to stop serve instance',
|
|
{
|
|
title: 'Stop Serve',
|
|
kind: 'error',
|
|
}
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (args._action === 'stop_all') {
|
|
try {
|
|
await rclone('/serve/stopall')
|
|
await notify({
|
|
title: 'All Serves Stopped',
|
|
body: 'All serve instances have been stopped',
|
|
})
|
|
queryClient.setQueryData(['serve', 'list'], [])
|
|
} catch (error) {
|
|
console.error('[toolbar] failed to stop all serves', error)
|
|
await message(
|
|
error instanceof Error
|
|
? error.message
|
|
: 'Failed to stop all serve instances',
|
|
{
|
|
title: 'Stop All Serves',
|
|
kind: 'error',
|
|
}
|
|
)
|
|
await queryClient.resetQueries({ queryKey: ['serve', 'list'] })
|
|
}
|
|
return
|
|
}
|
|
|
|
await openCommandWindow('serve', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'download',
|
|
label: 'Download',
|
|
description: COMMAND_DESCRIPTIONS.download,
|
|
keywords: COMMAND_KEYWORDS.download,
|
|
getDefaultResult: () => createBaseResult('Download', COMMAND_DESCRIPTIONS.download, {}, 45),
|
|
getResults: ({ query, paths }) => {
|
|
const url = findFirstUrl(query)
|
|
const urlLabel = url ? formatUrlLabel(url) : undefined
|
|
|
|
if (!url && query && !matchesKeyword(query, COMMAND_KEYWORDS.download)) {
|
|
return []
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
|
|
if (paths.length > 0) {
|
|
for (const path of paths) {
|
|
const destinationLabel = formatDestinationLabel(path)
|
|
const label = urlLabel
|
|
? `Download ${urlLabel} to ${destinationLabel}`
|
|
: `Download to ${destinationLabel}`
|
|
const args: ToolbarActionArgs = url
|
|
? { initialDestination: normalizePathForArgs(path), initialUrl: url }
|
|
: { initialDestination: normalizePathForArgs(path) }
|
|
const score = url ? (path.isLocal ? 190 : 200) : path.isLocal ? 150 : 160
|
|
|
|
results.push(
|
|
createBaseResult(label, COMMAND_DESCRIPTIONS.download, args, score)
|
|
)
|
|
}
|
|
} else if (url) {
|
|
results.push(
|
|
createBaseResult(
|
|
`Download ${urlLabel}`,
|
|
COMMAND_DESCRIPTIONS.download,
|
|
{ initialUrl: url },
|
|
170
|
|
)
|
|
)
|
|
} else {
|
|
results.push(createBaseResult('Download', COMMAND_DESCRIPTIONS.download, {}, 45))
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('download', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'cleanup',
|
|
label: 'Cleanup',
|
|
description: COMMAND_DESCRIPTIONS.cleanup,
|
|
keywords: COMMAND_KEYWORDS.cleanup,
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.cleanup)) {
|
|
return []
|
|
}
|
|
|
|
const supportedRemotePaths = paths.filter(
|
|
(path) => path.remoteType && SUPPORTS_CLEANUP.includes(path.remoteType)
|
|
)
|
|
|
|
if (supportedRemotePaths.length === 0) {
|
|
return [createBaseResult('Cleanup', COMMAND_DESCRIPTIONS.cleanup, {}, 36)]
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
const seenRemotes = new Set<string>()
|
|
|
|
for (const path of supportedRemotePaths) {
|
|
const remote = formatDestinationLabel(path)
|
|
if (!remote || seenRemotes.has(remote)) {
|
|
continue
|
|
}
|
|
seenRemotes.add(remote)
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`Cleanup ${remote}`,
|
|
COMMAND_DESCRIPTIONS.cleanup,
|
|
{ remote },
|
|
150
|
|
)
|
|
)
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args) => {
|
|
const remote =
|
|
typeof args.remote === 'string' && args.remote.length > 0 ? args.remote : undefined
|
|
|
|
if (!remote) {
|
|
await notify({
|
|
title: 'Error',
|
|
body: 'Please enter a remote name to cleanup',
|
|
})
|
|
return
|
|
}
|
|
|
|
try {
|
|
await rclone('/operations/cleanup', {
|
|
params: {
|
|
query: {
|
|
fs: remote,
|
|
_async: true,
|
|
},
|
|
},
|
|
})
|
|
await notify({
|
|
title: 'Cleanup Started',
|
|
body: `Cleanup started for ${remote}`,
|
|
})
|
|
} catch (error) {
|
|
console.error('[toolbar] failed to start cleanup task', error)
|
|
await notify({
|
|
title: 'Cleanup Failed',
|
|
body: `Could not start cleanup for ${remote}`,
|
|
})
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: 'browse',
|
|
label: 'Browse',
|
|
description: COMMAND_DESCRIPTIONS.browse,
|
|
keywords: COMMAND_KEYWORDS.browse,
|
|
getDefaultResult: () =>
|
|
createBaseResult('Browse', 'Specify a remote to browse its files', {}, 37),
|
|
getResults: ({ query, paths, remotes }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.browse)) {
|
|
return []
|
|
}
|
|
|
|
const remotePaths = paths.filter((path) => !path.isLocal)
|
|
|
|
if (remotePaths.length === 0) {
|
|
if (remotes.length === 0) {
|
|
return [
|
|
createBaseResult('Browse', 'Specify a remote to browse its files', {}, 37),
|
|
]
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
for (const remote of remotes) {
|
|
results.push(
|
|
createBaseResult(
|
|
`Browse ${remote}`,
|
|
COMMAND_DESCRIPTIONS.browse,
|
|
{ remote },
|
|
140
|
|
)
|
|
)
|
|
}
|
|
return results
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
const seenRemotes = new Set<string>()
|
|
|
|
for (const path of remotePaths) {
|
|
const remote = path.remoteName
|
|
if (!remote || seenRemotes.has(remote)) {
|
|
continue
|
|
}
|
|
seenRemotes.add(remote)
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`Browse ${remote}`,
|
|
COMMAND_DESCRIPTIONS.browse,
|
|
{ remote },
|
|
150
|
|
)
|
|
)
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args) => {
|
|
const remote =
|
|
typeof args.remote === 'string' && args.remote.length > 0 ? args.remote : undefined
|
|
|
|
if (!remote) {
|
|
await notify({
|
|
title: 'Error',
|
|
body: 'Please specify a remote to browse',
|
|
})
|
|
return
|
|
}
|
|
|
|
const persistedStoreState = usePersistedStore.getState()
|
|
const hostUrl = persistedStoreState.currentHost?.url
|
|
|
|
if (!hostUrl) {
|
|
await notify({
|
|
title: 'Error',
|
|
body: 'No host URL found',
|
|
})
|
|
return
|
|
}
|
|
|
|
try {
|
|
let auth: string | undefined
|
|
const authUser = persistedStoreState.currentHost?.authUser
|
|
|
|
if (authUser) {
|
|
const authPassword = persistedStoreState.currentHost?.authPassword
|
|
auth = btoa(`${authUser}:${authPassword ?? ''}`)
|
|
}
|
|
|
|
const normalizedHostUrl = hostUrl.replace(TRAILING_SLASH_REGEX, '')
|
|
const browseTargetUrl = `${normalizedHostUrl}/[${remote}:]/`
|
|
let fullUrl = `browse.html?url=${encodeURIComponent(browseTargetUrl)}`
|
|
|
|
if (auth) {
|
|
fullUrl += `&auth=${encodeURIComponent(auth)}`
|
|
}
|
|
|
|
await openFullWindow({
|
|
name: 'Browse',
|
|
url: fullUrl,
|
|
})
|
|
} catch (error) {
|
|
captureException(error)
|
|
await message('Could not open browse window. Please try again.', {
|
|
title: 'Error',
|
|
kind: 'error',
|
|
okLabel: 'OK',
|
|
})
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: 'delete',
|
|
label: 'Delete',
|
|
description: COMMAND_DESCRIPTIONS.delete,
|
|
keywords: COMMAND_KEYWORDS.delete,
|
|
getDefaultResult: () => createBaseResult('Delete', COMMAND_DESCRIPTIONS.delete, {}, 38),
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.delete)) {
|
|
return []
|
|
}
|
|
|
|
if (paths.length === 0) {
|
|
return [createBaseResult('Delete', COMMAND_DESCRIPTIONS.delete, {}, 38)]
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
|
|
for (const path of paths) {
|
|
const score = path.isLocal ? 140 : 150
|
|
results.push(
|
|
createBaseResult(
|
|
`Delete ${path.readable}`,
|
|
COMMAND_DESCRIPTIONS.delete,
|
|
{ initialSource: normalizePathForArgs(path) },
|
|
score
|
|
)
|
|
)
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('delete', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'purge',
|
|
label: 'Purge',
|
|
description: COMMAND_DESCRIPTIONS.purge,
|
|
keywords: COMMAND_KEYWORDS.purge,
|
|
getDefaultResult: () => createBaseResult('Purge', COMMAND_DESCRIPTIONS.purge, {}, 36),
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.purge)) {
|
|
return []
|
|
}
|
|
|
|
const supportedPaths = paths.filter(
|
|
(path) => path.remoteType && SUPPORTS_PURGE.includes(path.remoteType)
|
|
)
|
|
|
|
if (supportedPaths.length === 0) {
|
|
return [createBaseResult('Purge', COMMAND_DESCRIPTIONS.purge, {}, 36)]
|
|
}
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
|
|
for (const path of supportedPaths) {
|
|
const score = path.isLocal ? 140 : 150
|
|
results.push(
|
|
createBaseResult(
|
|
`Purge ${path.readable}`,
|
|
COMMAND_DESCRIPTIONS.purge,
|
|
{ initialSource: normalizePathForArgs(path) },
|
|
score
|
|
)
|
|
)
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('purge', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'settings',
|
|
label: 'Settings',
|
|
description: COMMAND_DESCRIPTIONS.settings,
|
|
keywords: COMMAND_KEYWORDS.settings,
|
|
getDefaultResult: () => createBaseResult('Settings', COMMAND_DESCRIPTIONS.settings, {}, 34),
|
|
getResults: ({ query }) => {
|
|
if (query && matchesKeyword(query, COMMAND_KEYWORDS.settings)) {
|
|
return [createBaseResult('Settings', COMMAND_DESCRIPTIONS.settings, {}, 34)]
|
|
}
|
|
return []
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('settings', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'github',
|
|
label: 'GitHub',
|
|
description: COMMAND_DESCRIPTIONS.github,
|
|
keywords: COMMAND_KEYWORDS.github,
|
|
getDefaultResult: () => createBaseResult('GitHub', COMMAND_DESCRIPTIONS.github, {}, 32),
|
|
getResults: ({ query }) => {
|
|
if (query && matchesKeyword(query, COMMAND_KEYWORDS.github)) {
|
|
return [createBaseResult('GitHub', COMMAND_DESCRIPTIONS.github, {}, 32)]
|
|
}
|
|
return []
|
|
},
|
|
onPress: async () => {
|
|
await openUrl('https://github.com/rclone-ui/rclone-ui')
|
|
},
|
|
},
|
|
{
|
|
id: 'transfers',
|
|
label: 'Transfers',
|
|
description: COMMAND_DESCRIPTIONS.transfers,
|
|
keywords: COMMAND_KEYWORDS.transfers,
|
|
getDefaultResult: () =>
|
|
createBaseResult('Transfers', COMMAND_DESCRIPTIONS.transfers, {}, 30),
|
|
getResults: ({ query }) => {
|
|
if (query && matchesKeyword(query, COMMAND_KEYWORDS.transfers)) {
|
|
return [createBaseResult('Transfers', COMMAND_DESCRIPTIONS.transfers, {}, 30)]
|
|
}
|
|
return []
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('transfers', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'schedules',
|
|
label: 'Schedules',
|
|
description: COMMAND_DESCRIPTIONS.schedules,
|
|
keywords: COMMAND_KEYWORDS.schedules,
|
|
getResults: ({ query }) => {
|
|
if (query && matchesKeyword(query, COMMAND_KEYWORDS.schedules)) {
|
|
return [createBaseResult('Schedules', COMMAND_DESCRIPTIONS.schedules, {}, 28)]
|
|
}
|
|
return []
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('schedules', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'templates',
|
|
label: 'Templates',
|
|
description: COMMAND_DESCRIPTIONS.templates,
|
|
keywords: COMMAND_KEYWORDS.templates,
|
|
getDefaultResult: () =>
|
|
createBaseResult('Templates', COMMAND_DESCRIPTIONS.templates, {}, 28),
|
|
getResults: ({ query }) => {
|
|
if (query && matchesKeyword(query, COMMAND_KEYWORDS.templates)) {
|
|
return [createBaseResult('Templates', COMMAND_DESCRIPTIONS.templates, {}, 28)]
|
|
}
|
|
return []
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('templates', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'remoteCreate',
|
|
label: 'New Remote',
|
|
description: COMMAND_DESCRIPTIONS.remoteCreate,
|
|
keywords: COMMAND_KEYWORDS.remoteCreate,
|
|
getDefaultResult: () =>
|
|
createBaseResult(
|
|
'New Remote',
|
|
COMMAND_DESCRIPTIONS.remoteCreate,
|
|
{
|
|
tab: 'remotes',
|
|
action: 'create',
|
|
},
|
|
28
|
|
),
|
|
getResults: ({ query }) => {
|
|
if (query && matchesKeyword(query, COMMAND_KEYWORDS.remoteCreate)) {
|
|
return [
|
|
createBaseResult(
|
|
'New Remote',
|
|
COMMAND_DESCRIPTIONS.remoteCreate,
|
|
{
|
|
tab: 'remotes',
|
|
action: 'create',
|
|
},
|
|
28
|
|
),
|
|
]
|
|
}
|
|
return []
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('settings', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'remoteEdit',
|
|
label: 'Edit Remote',
|
|
description: COMMAND_DESCRIPTIONS.remoteEdit,
|
|
keywords: COMMAND_KEYWORDS.remoteEdit,
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.remoteEdit)) {
|
|
return []
|
|
}
|
|
|
|
const remotePaths = paths.filter((path) => !path.isLocal)
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
const seenRemotes = new Set<string>()
|
|
|
|
for (const path of remotePaths) {
|
|
const remote = path.remoteName
|
|
if (!remote || seenRemotes.has(remote)) {
|
|
continue
|
|
}
|
|
seenRemotes.add(remote)
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`Edit ${remote}`,
|
|
COMMAND_DESCRIPTIONS.remoteEdit,
|
|
{ tab: 'remotes', action: 'edit', remote },
|
|
140
|
|
)
|
|
)
|
|
}
|
|
|
|
return results
|
|
},
|
|
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('settings', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'remoteAutoMount',
|
|
label: 'Auto Mount',
|
|
description: COMMAND_DESCRIPTIONS.remoteAutoMount,
|
|
keywords: COMMAND_KEYWORDS.remoteAutoMount,
|
|
getResults: ({ query, paths }) => {
|
|
if (query && !matchesKeyword(query, COMMAND_KEYWORDS.remoteAutoMount)) {
|
|
return []
|
|
}
|
|
|
|
const remotePaths = paths.filter((path) => !path.isLocal)
|
|
|
|
const results: ToolbarActionResult[] = []
|
|
const seenRemotes = new Set<string>()
|
|
|
|
for (const path of remotePaths) {
|
|
const remote = path.remoteName
|
|
if (!remote || seenRemotes.has(remote)) {
|
|
continue
|
|
}
|
|
seenRemotes.add(remote)
|
|
|
|
results.push(
|
|
createBaseResult(
|
|
`Configure auto mount for ${remote}`,
|
|
COMMAND_DESCRIPTIONS.remoteAutoMount,
|
|
{ tab: 'remotes', action: 'auto-mount', remote },
|
|
140
|
|
)
|
|
)
|
|
}
|
|
|
|
return results
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('settings', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'remoteList',
|
|
label: 'Show Remotes',
|
|
description: COMMAND_DESCRIPTIONS.remoteList,
|
|
keywords: COMMAND_KEYWORDS.remoteList,
|
|
getDefaultResult: () =>
|
|
createBaseResult(
|
|
'Show Remotes',
|
|
COMMAND_DESCRIPTIONS.remoteList,
|
|
{ tab: 'remotes' },
|
|
32
|
|
),
|
|
getResults: ({ query }) => {
|
|
if (query && matchesKeyword(query, COMMAND_KEYWORDS.remoteList)) {
|
|
return [
|
|
createBaseResult(
|
|
'Show Remotes',
|
|
COMMAND_DESCRIPTIONS.remoteList,
|
|
{ tab: 'remotes' },
|
|
32
|
|
),
|
|
]
|
|
}
|
|
return []
|
|
},
|
|
onPress: async (args, context) => {
|
|
await openCommandWindow('settings', args, context)
|
|
},
|
|
},
|
|
{
|
|
id: 'quit',
|
|
label: 'Quit',
|
|
description: COMMAND_DESCRIPTIONS.quit,
|
|
keywords: COMMAND_KEYWORDS.quit,
|
|
getDefaultResult: () => createBaseResult('Quit', COMMAND_DESCRIPTIONS.quit, {}, 20),
|
|
getResults: ({ query }) => {
|
|
if (query && matchesKeyword(query, COMMAND_KEYWORDS.quit)) {
|
|
return [createBaseResult('Quit', COMMAND_DESCRIPTIONS.quit, {}, 20)]
|
|
}
|
|
return []
|
|
},
|
|
onPress: async () => {
|
|
await getCurrentWindow().emit('close-app')
|
|
},
|
|
},
|
|
]
|
|
|
|
export function getToolbarActions(): ToolbarActionDefinition[] {
|
|
return actions
|
|
}
|
|
|
|
export function getToolbarAction(id: ToolbarCommandId): ToolbarActionDefinition {
|
|
const action = actions.find((item) => item.id === id)
|
|
if (!action) {
|
|
throw new Error(`Unknown toolbar action: ${id}`)
|
|
}
|
|
return action
|
|
}
|
|
|
|
type CommandConfig = typeof COMMAND_CONFIG
|
|
type ConfiguredCommandId = {
|
|
[K in keyof CommandConfig]: CommandConfig[K] extends { route: string; windowLabel: string }
|
|
? K
|
|
: never
|
|
}[keyof CommandConfig]
|
|
|
|
async function openCommandWindow(
|
|
id: ConfiguredCommandId,
|
|
args: ToolbarActionArgs,
|
|
context: ToolbarActionOnPressContext
|
|
): Promise<void> {
|
|
console.log('openCommandWindow', id, args)
|
|
|
|
const config = COMMAND_CONFIG[id]
|
|
|
|
const commandUrl = buildCommandUrl(id, args)
|
|
console.log('commandUrl', commandUrl)
|
|
|
|
await context.openWindow({
|
|
name: config.windowLabel,
|
|
url: commandUrl,
|
|
})
|
|
}
|
|
|
|
function buildCommandUrl(id: ConfiguredCommandId, args: ToolbarActionArgs): string {
|
|
const config = COMMAND_CONFIG[id]
|
|
const params = buildCommandParams(id, args)
|
|
const search = params.toString()
|
|
return search ? `${config.route}?${search}` : config.route
|
|
}
|
|
|
|
function buildCommandParams(id: ConfiguredCommandId, args: ToolbarActionArgs): URLSearchParams {
|
|
const params = new URLSearchParams()
|
|
const setParam = (key: string, value?: string) => {
|
|
if (typeof value === 'string' && value.length > 0) {
|
|
params.set(key, value)
|
|
}
|
|
}
|
|
|
|
switch (id) {
|
|
case 'copy':
|
|
case 'move':
|
|
case 'sync':
|
|
case 'bisync':
|
|
case 'mount':
|
|
case 'serve':
|
|
case 'download':
|
|
case 'delete':
|
|
case 'purge':
|
|
case 'settings': {
|
|
for (const key in args) {
|
|
if (args[key]) {
|
|
setParam(key, args[key])
|
|
}
|
|
}
|
|
break
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
return params
|
|
}
|
|
|
|
function createBaseResult(
|
|
label: string,
|
|
description: string | undefined,
|
|
args: ToolbarActionArgs,
|
|
score: number
|
|
): ToolbarActionResult {
|
|
return {
|
|
label,
|
|
description,
|
|
args,
|
|
score,
|
|
}
|
|
}
|
|
|
|
function matchesKeyword(query: string, keywords: string[]): boolean {
|
|
const [first = ''] = query.split(WHITESPACE_SPLIT).filter(Boolean)
|
|
const normalized = first.toLowerCase()
|
|
if (!normalized) return false
|
|
|
|
return keywords.some((keyword) => {
|
|
const lowerKeyword = keyword.toLowerCase()
|
|
return lowerKeyword.includes(normalized) || normalized.includes(lowerKeyword)
|
|
})
|
|
}
|
|
|
|
function findServeType(query: string): string | undefined {
|
|
const lower = query.toLowerCase()
|
|
return SERVE_TYPES.find((type) => lower.includes(type))
|
|
}
|
|
|
|
function findFirstUrl(query: string): string | undefined {
|
|
const tokens = query.split(WHITESPACE_SPLIT).filter(Boolean)
|
|
for (const raw of tokens) {
|
|
const token = raw.replace(TOKEN_TRIM_REGEX, '')
|
|
if (!token) continue
|
|
if (token.includes('://')) {
|
|
return token
|
|
}
|
|
if (SIMPLE_URL_REGEX.test(token)) {
|
|
return token.startsWith('http') ? token : `https://${token}`
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function formatUrlLabel(raw: string): string {
|
|
try {
|
|
const parsed = new URL(raw)
|
|
const pathname = parsed.pathname.replace(TRAILING_SLASH_REGEX, '')
|
|
if (pathname) {
|
|
const segments = pathname.split('/').filter(Boolean)
|
|
if (segments.length > 0) {
|
|
return segments[segments.length - 1]
|
|
}
|
|
}
|
|
return parsed.hostname
|
|
} catch {
|
|
const cleaned = raw.replace(TRAILING_SLASH_REGEX, '')
|
|
const parts = cleaned.split('/').filter(Boolean)
|
|
return parts.length > 0 ? parts[parts.length - 1] : cleaned
|
|
}
|
|
}
|
|
|
|
function normalizePathForArgs(path: ToolbarActionPath): string {
|
|
if (!path.isLocal && !path.full.includes(':/')) {
|
|
return `${path.full}:/`
|
|
}
|
|
return path.full
|
|
}
|
|
|
|
function formatDestinationLabel(path: ToolbarActionPath): string {
|
|
if (!path.isLocal) {
|
|
const colonIndex = path.full.indexOf(':')
|
|
if (colonIndex > 0) {
|
|
return path.full.slice(0, colonIndex)
|
|
}
|
|
}
|
|
return path.readable
|
|
}
|