Files
rclone-ui/lib/hooks.ts
2026-08-20 00:43:05 +03:00

149 lines
5.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import type { RcloneFeatures, RcloneFsInfo } from '../types/rclone'
import { UserCancelledError } from './errors'
import { sortByName } from './flags'
import rclone from './rclone/client'
import { SERVE_TYPES } from './rclone/constants'
// Wall-clock tick for values derived from "now" (relative timestamps, next cron occurrences).
// Memoizing such values without a time dep freezes them at their last dep change. Pass null to
// pause (e.g. while a drawer is closed); re-arming refreshes immediately.
export function useNow(intervalMs: number | null = 30_000): number {
const [now, setNow] = useState(() => Date.now())
useEffect(() => {
if (intervalMs === null) {
return
}
setNow(Date.now())
const id = setInterval(() => setNow(Date.now()), intervalMs)
return () => clearInterval(id)
}, [intervalMs])
return now
}
// Shared query options for a remote's `/config/get`. No default staleTime: most consumers rely on
// staleTime-0 refetch-on-mount for cross-window freshness (each webview has its own QueryClient);
// the handful that want caching spread `staleTime` per-site.
export function remoteConfigQueryOptions(remote: string | undefined | null) {
return {
queryKey: ['remote', remote, 'config'] as const,
queryFn: () => rclone('/config/get', { params: { query: { name: remote! } } }),
enabled: !!remote && remote !== 'UI_LOCAL_FS' && remote !== 'UI_FAVORITES',
}
}
export function useRemoteConfig(remote: string | undefined | null) {
return useQuery(remoteConfigQueryOptions(remote))
}
// Shared query options for a remote's `operations/fsinfo` — the authoritative, per-remote backend
// capability set (correct even for wrapping backends like crypt/alias/union, which the old static
// type lists could not express). fsinfo instantiates/connects the remote, so it's cached hard:
// capabilities are ~immutable per config and only change on a remote edit (explicit invalidation)
// or an rclone upgrade (self-heals on the 24h staleTime / next cold launch). retry: 1 overrides the
// app-wide 3-retry default so an unreachable remote costs one attempt, then error-caches.
export function fsInfoQueryOptions(remote: string | undefined | null) {
return {
queryKey: ['remote', remote, 'fsinfo'] as const,
queryFn: () => rclone('/operations/fsinfo', { params: { query: { fs: `${remote}:` } } }),
enabled: !!remote && remote !== 'UI_LOCAL_FS' && remote !== 'UI_FAVORITES',
staleTime: 1000 * 60 * 60 * 24,
// Cap retries low (dead remotes shouldn't retry 3× like the default) but keep the default's
// UserCancelledError guard: a plain `retry: 1` would override it, and since fsinfo connects
// to the remote, retrying a dismissed reconnect prompt re-prompts the user (client.ts).
retry: (failureCount: number, error: unknown) =>
!(error instanceof UserCancelledError) && failureCount < 1,
}
}
export function useFsInfo(remote: string | undefined | null) {
return useQuery(fsInfoQueryOptions(remote))
}
// undefined while loading/errored → false. Callers that need an optimistic default (e.g. create
// folder, which historically assumed "supported" until proven otherwise) handle that explicitly.
export function hasFeature(
fsInfo: RcloneFsInfo | undefined,
feature: keyof RcloneFeatures
): boolean {
return !!fsInfo?.Features?.[feature]
}
export function useFlags() {
const allFlagsQuery = useQuery({
queryKey: ['options', 'all'],
queryFn: async () => await rclone('/options/info'),
})
const globalFlagsQuery = useQuery({
queryKey: ['options', 'global'],
queryFn: async () => await rclone('/options/get'),
})
const globalFlags = globalFlagsQuery.data
const allFlags = allFlagsQuery.data
const filterFlags = allFlags?.filter
?.filter((flag) => !flag.Groups?.includes('Metadata'))
.sort(sortByName)
const configFlags = allFlags?.main
?.filter(
(flag) =>
flag.Groups?.includes('Performance') ||
flag.Groups?.includes('Listing') ||
flag.Groups?.includes('Networking') ||
flag.Groups?.includes('Check') ||
flag.Name === 'use_server_modtime'
)
.sort(sortByName)
const mountFlags = allFlags?.mount
?.filter((flag) => !flag.Groups?.includes('Metadata'))
.sort(sortByName)
const vfsFlags = allFlags?.vfs
?.filter((flag) => !flag.Groups?.includes('Metadata'))
.sort(sortByName)
const copyFlags = allFlags?.main
?.filter((flag) => flag.Groups?.includes('Copy'))
.sort(sortByName)
const syncFlags = allFlags?.main
?.filter((flag) => flag.Groups?.includes('Copy') || flag.Groups?.includes('Sync'))
.sort(sortByName)
const serveFlags = SERVE_TYPES.reduce(
(acc, type) => {
acc[type] = (allFlags?.[type] || [])
.map((flag: any) => ({
...flag,
FieldName: flag.Name,
DefaultStr:
flag.Name === 'addr'
? flag.DefaultStr.replace('[', '').replace(']', '')
: flag.DefaultStr,
}))
.sort((a: any, b: any) => a.Name.localeCompare(b.Name))
return acc
},
{} as Record<(typeof SERVE_TYPES)[number], any[]>
)
return {
allFlags,
globalFlags,
filterFlags,
configFlags,
mountFlags,
vfsFlags,
copyFlags,
syncFlags,
serveFlags,
}
}