SERVE command
Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com>
This commit is contained in:
+53
-1
@@ -8,7 +8,15 @@ import { openPath } from '@tauri-apps/plugin-opener'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { isDirectoryEmpty } from './fs'
|
||||
import notify from './notify'
|
||||
import { cleanupRemote, deleteRemote, listMounts, mountRemote, unmountRemote } from './rclone/api'
|
||||
import {
|
||||
cleanupRemote,
|
||||
deleteRemote,
|
||||
listMounts,
|
||||
listServes,
|
||||
mountRemote,
|
||||
stopServe,
|
||||
unmountRemote,
|
||||
} from './rclone/api'
|
||||
import { dialogGetMountPlugin, needsMountPlugin } from './rclone/mount'
|
||||
import { usePersistedStore, useStore } from './store'
|
||||
import { showDefaultTray, showLoadingTray } from './tray'
|
||||
@@ -22,6 +30,8 @@ async function parseRemotes(remotes: string[]) {
|
||||
console.log('[parseRemotes] listing mounts')
|
||||
const currentMounts = await listMounts()
|
||||
console.log('[parseRemotes] currentMounts', currentMounts)
|
||||
const currentServes = await listServes()
|
||||
console.log('[parseRemotes] currentServes', currentServes)
|
||||
|
||||
const parsedRemotes: Record<string, (MenuItem | Submenu | PredefinedMenuItem)[]> = {}
|
||||
|
||||
@@ -320,6 +330,34 @@ async function parseRemotes(remotes: string[]) {
|
||||
submenuItems.push(showLocationItem)
|
||||
}
|
||||
|
||||
for (const currentServe of currentServes) {
|
||||
// console.log('[parseRemotes] Adding Stop Serve (' + currentServe.Addr.split(':')[0] + ') for ', remote)
|
||||
const stopServeMenuItem = await MenuItem.new({
|
||||
id: `stop-serve-${currentServe.id}`,
|
||||
text: 'Stop Serve',
|
||||
action: async () => {
|
||||
try {
|
||||
await showLoadingTray()
|
||||
await stopServe(currentServe.id)
|
||||
|
||||
await message(`Successfully stopped serve ${currentServe.id}`, {
|
||||
title: 'Success',
|
||||
})
|
||||
} catch (error) {
|
||||
Sentry.captureException(error)
|
||||
console.error('Stop serve operation failed:', error)
|
||||
await message(`Failed to stop serve ${currentServe.id}: ${error}`, {
|
||||
kind: 'error',
|
||||
title: 'Stop Serve Error',
|
||||
})
|
||||
} finally {
|
||||
await showDefaultTray()
|
||||
}
|
||||
},
|
||||
})
|
||||
submenuItems.push(stopServeMenuItem)
|
||||
}
|
||||
|
||||
parsedRemotes[remote] = submenuItems
|
||||
}
|
||||
|
||||
@@ -436,6 +474,20 @@ export async function buildMenu() {
|
||||
menuItems.push(syncMenuItem)
|
||||
}
|
||||
|
||||
if (!persistedStoreState.disabledActions?.includes('tray-serve')) {
|
||||
const serveToMenuItem = await MenuItem.new({
|
||||
id: 'serve',
|
||||
text: 'Serve',
|
||||
action: async () => {
|
||||
await openWindow({
|
||||
name: 'Serve',
|
||||
url: '/serve',
|
||||
})
|
||||
},
|
||||
})
|
||||
menuItems.push(serveToMenuItem)
|
||||
}
|
||||
|
||||
if (!persistedStoreState.disabledActions?.includes('tray-purge')) {
|
||||
const purgeMenuItem = await MenuItem.new({
|
||||
id: 'purge',
|
||||
|
||||
+172
-2
@@ -1,6 +1,7 @@
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import type { FlagValue } from '../../types/rclone'
|
||||
import { useStore } from '../store'
|
||||
import { parseRcloneOptions } from './common'
|
||||
|
||||
@@ -859,7 +860,10 @@ export async function getSftpFlags() {
|
||||
headers: getAuthHeader(),
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
const sftpFlags = r.sftp
|
||||
const sftpFlags = r.sftp.map((flag: any) => ({
|
||||
...flag,
|
||||
FieldName: flag.Name,
|
||||
}))
|
||||
|
||||
return sftpFlags.sort((a: any, b: any) => a.Name.localeCompare(b.Name))
|
||||
}
|
||||
@@ -872,7 +876,173 @@ export async function getFtpFlags() {
|
||||
headers: getAuthHeader(),
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
const ftpFlags = r.ftp
|
||||
const ftpFlags = r.ftp.map((flag: any) => ({
|
||||
...flag,
|
||||
FieldName: flag.Name,
|
||||
}))
|
||||
|
||||
return ftpFlags.sort((a: any, b: any) => a.Name.localeCompare(b.Name))
|
||||
}
|
||||
|
||||
export async function getDlnaFlags() {
|
||||
console.log('[getDlnaFlags]')
|
||||
|
||||
const r = await fetch('http://localhost:5572/options/info', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeader(),
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
const dlnaFlags = r.dlna.map((flag: any) => ({
|
||||
...flag,
|
||||
FieldName: flag.Name,
|
||||
}))
|
||||
|
||||
return dlnaFlags.sort((a: any, b: any) => a.Name.localeCompare(b.Name))
|
||||
}
|
||||
|
||||
/* SERVE */
|
||||
export interface ServeListItem {
|
||||
id: string
|
||||
addr: string
|
||||
params: {
|
||||
fs: string
|
||||
type: string
|
||||
opt?: Record<string, unknown>
|
||||
vfsOpt?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export async function listServes() {
|
||||
console.log('[listServes]')
|
||||
|
||||
const r = await fetch('http://localhost:5572/serve/list', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeader(),
|
||||
})
|
||||
.then(
|
||||
(res) =>
|
||||
res.json() as Promise<{
|
||||
list: ServeListItem[]
|
||||
}>
|
||||
)
|
||||
.catch((e) => {
|
||||
Sentry.captureException(e)
|
||||
console.log('error', e)
|
||||
throw e
|
||||
})
|
||||
|
||||
if (!Array.isArray(r?.list)) {
|
||||
throw new Error('[listServes] Failed to get running serves')
|
||||
}
|
||||
|
||||
return r.list
|
||||
}
|
||||
|
||||
export async function startServe({
|
||||
// type,
|
||||
// fs,
|
||||
// addr,
|
||||
_filter,
|
||||
_config,
|
||||
...props
|
||||
}: {
|
||||
// type: string
|
||||
// fs: string
|
||||
// addr: string
|
||||
// options?: Record<string, string | number | boolean | string[]>
|
||||
_filter?: Record<string, FlagValue>
|
||||
_config?: Record<string, FlagValue>
|
||||
} & Record<string, FlagValue>) {
|
||||
console.log('[startServe] ', props)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
// params.set('type', type)
|
||||
// params.set('fs', fs)
|
||||
// params.set('user', 'usr')
|
||||
// params.set('pass', 'pas')
|
||||
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) params.append(key, String(v))
|
||||
} else if (value !== undefined) {
|
||||
params.set(key, String(value))
|
||||
}
|
||||
}
|
||||
|
||||
// if (options && Object.keys(options).length > 0) {
|
||||
// const parsed = parseRcloneOptions(options)
|
||||
// for (const [key, value] of Object.entries(parsed)) {
|
||||
// if (Array.isArray(value)) {
|
||||
// for (const v of value) params.append(key, String(v))
|
||||
// } else if (value !== undefined) {
|
||||
// params.set(key, String(value))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (_filter && Object.keys(_filter).length > 0) {
|
||||
params.set('_filter', JSON.stringify(parseRcloneOptions(_filter)))
|
||||
}
|
||||
|
||||
if (_config && Object.keys(_config).length > 0) {
|
||||
params.set('_config', JSON.stringify(parseRcloneOptions(_config)))
|
||||
}
|
||||
|
||||
const r = await fetch(`http://localhost:5572/serve/start?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeader(),
|
||||
})
|
||||
.then((res) => res.json() as Promise<{ addr: string; id: string } | { error: string }>)
|
||||
.catch((e) => {
|
||||
Sentry.captureException(e)
|
||||
console.log('error', e)
|
||||
throw e
|
||||
})
|
||||
|
||||
if ('error' in r) {
|
||||
throw new Error(r.error)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
export async function stopServe(id: string) {
|
||||
console.log('[stopServe] ', id)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
params.set('id', id)
|
||||
|
||||
const r = await fetch(`http://localhost:5572/serve/stop?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeader(),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
Sentry.captureException(e)
|
||||
console.log('error', e)
|
||||
throw e
|
||||
})
|
||||
|
||||
if ('error' in r) {
|
||||
throw new Error(r.error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopAllServes() {
|
||||
console.log('[stopAllServes]')
|
||||
|
||||
const r = await fetch('http://localhost:5572/serve/stopall', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeader(),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
Sentry.captureException(e)
|
||||
console.log('error', e)
|
||||
throw e
|
||||
})
|
||||
|
||||
if ('error' in r) {
|
||||
throw new Error(r.error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,6 @@ interface State {
|
||||
rcloneAuth: string
|
||||
rcloneAuthHeader: string
|
||||
|
||||
serveList: { pid: number; protocol: string; remote: string }[]
|
||||
setServeList: (serve: { pid: number; protocol: string; remote: string }) => void
|
||||
removeServeList: (pid: number) => void
|
||||
|
||||
remotes: string[]
|
||||
setRemotes: (remotes: string[]) => void
|
||||
addRemote: (remote: string) => void
|
||||
@@ -147,12 +143,6 @@ export const useStore = create<State>()(
|
||||
rcloneAuth: '',
|
||||
rcloneAuthHeader: '',
|
||||
|
||||
serveList: [],
|
||||
setServeList: (serve: { pid: number; protocol: string; remote: string }) =>
|
||||
set((state) => ({ serveList: [...state.serveList, serve] })),
|
||||
removeServeList: (pid: number) =>
|
||||
set((state) => ({ serveList: state.serveList.filter((s) => s.pid !== pid) })),
|
||||
|
||||
remotes: [],
|
||||
setRemotes: (remotes: string[]) => set((_) => ({ remotes })),
|
||||
addRemote: (remote: string) =>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"Settings",
|
||||
"Jobs",
|
||||
"Cron",
|
||||
"Serve",
|
||||
"Browse",
|
||||
"Startup",
|
||||
"Test"
|
||||
|
||||
@@ -12,6 +12,7 @@ import Jobs from './pages/Jobs'
|
||||
import Mount from './pages/Mount'
|
||||
import Move from './pages/Move'
|
||||
import Purge from './pages/Purge'
|
||||
import Serve from './pages/Serve'
|
||||
import Settings from './pages/Settings'
|
||||
import Startup from './pages/Startup'
|
||||
import Sync from './pages/Sync'
|
||||
@@ -69,6 +70,10 @@ const router = createBrowserRouter([
|
||||
path: '/purge',
|
||||
element: <Purge />,
|
||||
},
|
||||
{
|
||||
path: '/serve',
|
||||
element: <Serve />,
|
||||
},
|
||||
{
|
||||
path: '/mount',
|
||||
element: <Mount />,
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
import { Accordion, AccordionItem, Avatar, Button, Select, SelectItem } from '@heroui/react'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { message } from '@tauri-apps/plugin-dialog'
|
||||
import {
|
||||
AlertOctagonIcon,
|
||||
FilterIcon,
|
||||
FoldersIcon,
|
||||
PlayIcon,
|
||||
ServerCrashIcon,
|
||||
WavesLadderIcon,
|
||||
XIcon,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
getCurrentGlobalFlags,
|
||||
getDlnaFlags,
|
||||
getFilterFlags,
|
||||
getFtpFlags,
|
||||
getSftpFlags,
|
||||
getVfsFlags,
|
||||
startServe,
|
||||
} from '../../lib/rclone/api'
|
||||
import { RCLONE_VFS_DEFAULTS } from '../../lib/rclone/constants'
|
||||
import { usePersistedStore, useStore } from '../../lib/store'
|
||||
import { triggerTrayRebuild } from '../../lib/tray'
|
||||
import type { FlagValue } from '../../types/rclone'
|
||||
import OptionsSection from '../components/OptionsSection'
|
||||
|
||||
const SERVE_TYPES = ['dlna', 'ftp', 'sftp'] as const
|
||||
|
||||
export default function Serve() {
|
||||
const remotes = useStore((state) => state.remotes)
|
||||
const [remote, setRemote] = useState<string | undefined>(undefined)
|
||||
const [type, setType] = useState<(typeof SERVE_TYPES)[number] | undefined>(undefined)
|
||||
|
||||
const [isServing, setIsServing] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [jsonError, setJsonError] = useState<'mount' | 'vfs' | 'filter' | 'config' | null>(null)
|
||||
|
||||
const [serveOptionsLocked, setServeOptionsLocked] = useState(false)
|
||||
const [serveOptions, setServeOptions] = useState<Record<string, FlagValue>>({})
|
||||
const [serveOptionsJson, setServeOptionsJson] = useState<string>('{}')
|
||||
|
||||
const [vfsOptionsLocked, setVfsOptionsLocked] = useState(false)
|
||||
const [_, setVfsOptions] = useState<Record<string, FlagValue>>({})
|
||||
const [vfsOptionsJson, setVfsOptionsJson] = useState<string>('{}')
|
||||
|
||||
const [filterOptionsLocked, setFilterOptionsLocked] = useState(false)
|
||||
const [filterOptions, setFilterOptions] = useState<Record<string, FlagValue>>({})
|
||||
const [filterOptionsJson, setFilterOptionsJson] = useState<string>('{}')
|
||||
|
||||
// const [configOptionsLocked, setConfigOptionsLocked] = useState(false)
|
||||
// const [configOptions, setConfigOptions] = useState<Record<string, string>>({})
|
||||
// const [configOptionsJson, setConfigOptionsJson] = useState<string>('{}')
|
||||
|
||||
const [currentGlobalOptions, setCurrentGlobalOptions] = useState<any[]>([])
|
||||
|
||||
const getAvailableOptions =
|
||||
type === 'dlna'
|
||||
? getDlnaFlags
|
||||
: type === 'ftp'
|
||||
? getFtpFlags
|
||||
: type === 'sftp'
|
||||
? getSftpFlags
|
||||
: () => Promise.resolve([])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect
|
||||
useEffect(() => {
|
||||
const storeData = usePersistedStore.getState()
|
||||
|
||||
if (!remote) return
|
||||
|
||||
const remoteConfig = storeData.remoteConfigList?.[remote]
|
||||
|
||||
if (!vfsOptionsLocked) {
|
||||
if (remoteConfig?.vfsDefaults && Object.keys(remoteConfig.vfsDefaults).length > 0) {
|
||||
setVfsOptionsJson(JSON.stringify(remoteConfig.vfsDefaults, null, 2))
|
||||
} else {
|
||||
setVfsOptionsJson(JSON.stringify(RCLONE_VFS_DEFAULTS, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
remoteConfig?.filterDefaults &&
|
||||
Object.keys(remoteConfig.filterDefaults).length > 0 &&
|
||||
!filterOptionsLocked
|
||||
) {
|
||||
setFilterOptionsJson(JSON.stringify(remoteConfig.filterDefaults, null, 2))
|
||||
}
|
||||
|
||||
// if (!configOptionsLocked) {
|
||||
// if (
|
||||
// storeData.remoteConfigList?.[remote]?.configDefaults &&
|
||||
// Object.keys(storeData.remoteConfigList?.[remote]?.configDefaults).length > 0
|
||||
// ) {
|
||||
// setConfigOptionsJson(
|
||||
// JSON.stringify(storeData.remoteConfigList[remote].configDefaults, null, 2)
|
||||
// )
|
||||
// } else {
|
||||
// setConfigOptionsJson(JSON.stringify(RCLONE_CONFIG_DEFAULTS, null, 2))
|
||||
// }
|
||||
// }
|
||||
}, [remote])
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentGlobalFlags().then((flags) => setCurrentGlobalOptions(flags))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let step: 'mount' | 'vfs' | 'filter' | 'config' = 'mount'
|
||||
try {
|
||||
setServeOptions(JSON.parse(serveOptionsJson))
|
||||
|
||||
step = 'vfs'
|
||||
setVfsOptions(JSON.parse(vfsOptionsJson))
|
||||
|
||||
step = 'filter'
|
||||
setFilterOptions(JSON.parse(filterOptionsJson))
|
||||
|
||||
// step = 'config'
|
||||
// setConfigOptions(JSON.parse(configOptionsJson))
|
||||
|
||||
setJsonError(null)
|
||||
} catch (error) {
|
||||
setJsonError(step)
|
||||
console.error(`[Mount] Error parsing ${step} options:`, error)
|
||||
}
|
||||
}, [serveOptionsJson, vfsOptionsJson, filterOptionsJson])
|
||||
|
||||
async function handleStartServe() {
|
||||
if (!remote || !type) return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
await startServe({
|
||||
type,
|
||||
fs: `${remote}:/`,
|
||||
_filter: filterOptions as any,
|
||||
...serveOptions,
|
||||
})
|
||||
|
||||
setIsServing(true)
|
||||
|
||||
await triggerTrayRebuild()
|
||||
setIsLoading(false)
|
||||
} catch (err) {
|
||||
console.error('[Mount] Failed to start mount:', err)
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : 'Failed to start mount operation'
|
||||
await message(errorMessage, {
|
||||
title: 'Error',
|
||||
kind: 'error',
|
||||
})
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const buttonText = (() => {
|
||||
if (isLoading) return 'STARTING...'
|
||||
if (isServing) return 'STARTED'
|
||||
if (!remote) return 'Please select a remote'
|
||||
if (!type) return 'Please select a serve type'
|
||||
|
||||
if (jsonError) return 'Invalid JSON for ' + jsonError.toUpperCase() + ' options'
|
||||
|
||||
if (!('addr' in serveOptions)) return 'Specify a listen address in the Serve options'
|
||||
return 'START SERVE'
|
||||
})()
|
||||
|
||||
console.log('serveOptions', serveOptions)
|
||||
|
||||
const buttonIcon = (() => {
|
||||
if (isLoading || isServing) return
|
||||
if (!remote || !type) return <FoldersIcon className="w-5 h-5" />
|
||||
if (jsonError) return <AlertOctagonIcon className="w-4 h-4 mt-0.5" />
|
||||
if (!('addr' in serveOptions)) return <AlertOctagonIcon className="w-4 h-4 mt-0.5" />
|
||||
return <PlayIcon className="w-4 h-4 fill-current" />
|
||||
})()
|
||||
|
||||
console.log('type', type)
|
||||
return (
|
||||
<div className="flex flex-col h-screen gap-10 pt-10">
|
||||
<div className="flex flex-col flex-1 w-full max-w-3xl gap-6 mx-auto">
|
||||
<Select
|
||||
selectedKeys={remote ? [remote] : []}
|
||||
onSelectionChange={(keys) => {
|
||||
setRemote(keys.currentKey as string)
|
||||
}}
|
||||
size="lg"
|
||||
placeholder="Select a remote"
|
||||
label="Remote"
|
||||
labelPlacement="outside"
|
||||
classNames={{}}
|
||||
>
|
||||
{remotes.map((remote) => (
|
||||
<SelectItem key={remote} textValue={remote}>
|
||||
{remote}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
selectedKeys={type ? [type] : []}
|
||||
onSelectionChange={(keys) => {
|
||||
setType(keys.currentKey as (typeof SERVE_TYPES)[number])
|
||||
}}
|
||||
size="lg"
|
||||
placeholder="Select a serve type"
|
||||
label="Serve Type"
|
||||
labelPlacement="outside"
|
||||
>
|
||||
{SERVE_TYPES.map((type) => (
|
||||
<SelectItem key={type} textValue={type.toUpperCase()}>
|
||||
{type.toUpperCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Accordion>
|
||||
{type ? (
|
||||
<AccordionItem
|
||||
key="serve"
|
||||
startContent={
|
||||
<Avatar
|
||||
color="success"
|
||||
radius="lg"
|
||||
fallback={<ServerCrashIcon />}
|
||||
/>
|
||||
}
|
||||
indicator={<ServerCrashIcon />}
|
||||
subtitle="Tap to see Serve options for the current operation"
|
||||
title="Serve"
|
||||
>
|
||||
<OptionsSection
|
||||
optionsJson={serveOptionsJson}
|
||||
setOptionsJson={setServeOptionsJson}
|
||||
globalOptions={
|
||||
currentGlobalOptions[type as keyof typeof currentGlobalOptions]
|
||||
}
|
||||
getAvailableOptions={getAvailableOptions}
|
||||
isLocked={serveOptionsLocked}
|
||||
setIsLocked={setServeOptionsLocked}
|
||||
/>
|
||||
</AccordionItem>
|
||||
) : null}
|
||||
<AccordionItem
|
||||
key="vfs"
|
||||
startContent={
|
||||
<Avatar color="warning" radius="lg" fallback={<WavesLadderIcon />} />
|
||||
}
|
||||
indicator={<WavesLadderIcon />}
|
||||
subtitle="Tap to see VFS options for the current operation"
|
||||
title="VFS"
|
||||
>
|
||||
<OptionsSection
|
||||
optionsJson={vfsOptionsJson}
|
||||
setOptionsJson={setVfsOptionsJson}
|
||||
globalOptions={
|
||||
currentGlobalOptions['vfs' as keyof typeof currentGlobalOptions]
|
||||
}
|
||||
getAvailableOptions={getVfsFlags}
|
||||
isLocked={vfsOptionsLocked}
|
||||
setIsLocked={setVfsOptionsLocked}
|
||||
/>
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
key="filters"
|
||||
startContent={
|
||||
<Avatar color="danger" radius="lg" fallback={<FilterIcon />} />
|
||||
}
|
||||
indicator={<FilterIcon />}
|
||||
subtitle="Tap to toggle filtering options for this operation"
|
||||
title="Filters"
|
||||
>
|
||||
<OptionsSection
|
||||
globalOptions={
|
||||
currentGlobalOptions['filter' as keyof typeof currentGlobalOptions]
|
||||
}
|
||||
optionsJson={filterOptionsJson}
|
||||
setOptionsJson={setFilterOptionsJson}
|
||||
getAvailableOptions={getFilterFlags}
|
||||
isLocked={filterOptionsLocked}
|
||||
setIsLocked={setFilterOptionsLocked}
|
||||
/>
|
||||
</AccordionItem>
|
||||
{/* <AccordionItem
|
||||
key="config"
|
||||
startContent={
|
||||
<Avatar color="default" radius="lg" fallback={<WrenchIcon />} />
|
||||
}
|
||||
indicator={<WrenchIcon />}
|
||||
subtitle="Tap to toggle config options for this operation"
|
||||
title="Config"
|
||||
>
|
||||
<OptionsSection
|
||||
globalOptions={
|
||||
currentGlobalOptions['main' as keyof typeof currentGlobalOptions]
|
||||
}
|
||||
optionsJson={configOptionsJson}
|
||||
setOptionsJson={setConfigOptionsJson}
|
||||
getAvailableOptions={getConfigFlags}
|
||||
rows={10}
|
||||
isLocked={configOptionsLocked}
|
||||
setIsLocked={setConfigOptionsLocked}
|
||||
/>
|
||||
</AccordionItem> */}
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<div className="sticky bottom-0 z-50 flex items-center justify-center flex-none gap-5 p-4 border-t border-neutral-500/20 bg-neutral-900/50 backdrop-blur-lg">
|
||||
{isServing ? (
|
||||
<>
|
||||
<Button
|
||||
fullWidth={true}
|
||||
size="lg"
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
setRemote(undefined)
|
||||
setType(undefined)
|
||||
setIsServing(false)
|
||||
}}
|
||||
data-focus-visible="false"
|
||||
>
|
||||
RESET
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
isIconOnly={true}
|
||||
onPress={async () => {
|
||||
await getCurrentWindow().hide()
|
||||
await getCurrentWindow().destroy()
|
||||
}}
|
||||
data-focus-visible="false"
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onPress={handleStartServe}
|
||||
size="lg"
|
||||
fullWidth={true}
|
||||
color="primary"
|
||||
isDisabled={isLoading || !!jsonError || !remote || !type || isServing}
|
||||
isLoading={isLoading}
|
||||
endContent={buttonIcon}
|
||||
className="max-w-2xl gap-2"
|
||||
data-focus-visible="false"
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -555,6 +555,21 @@ function GeneralSection() {
|
||||
>
|
||||
Show <span className="font-mono text-blue-300">Move</span> option
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
isSelected={!disabledActions?.includes('tray-serve')}
|
||||
onValueChange={(value) => {
|
||||
if (value) {
|
||||
setDisabledActions(
|
||||
disabledActions?.filter((action) => action !== 'tray-serve') ||
|
||||
[]
|
||||
)
|
||||
} else {
|
||||
setDisabledActions([...(disabledActions || []), 'tray-serve'])
|
||||
}
|
||||
}}
|
||||
>
|
||||
Show <span className="font-mono text-blue-300">Serve</span> option
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
isSelected={!disabledActions?.includes('tray-purge')}
|
||||
onValueChange={(value) => {
|
||||
|
||||
Vendored
+2
@@ -24,3 +24,5 @@ export interface Backend {
|
||||
Options: BackendOption[]
|
||||
Prefix: string
|
||||
}
|
||||
|
||||
export type FlagValue = string | number | boolean | string[]
|
||||
|
||||
Reference in New Issue
Block a user