402 lines
15 KiB
TypeScript
402 lines
15 KiB
TypeScript
import { invoke } from '@tauri-apps/api/core'
|
|
import { ask } from '@tauri-apps/plugin-dialog'
|
|
import { exit } from '@tauri-apps/plugin-process'
|
|
import { LazyStore } from '@tauri-apps/plugin-store'
|
|
import { create } from 'zustand'
|
|
import { type StateStorage, createJSONStorage, persist } from 'zustand/middleware'
|
|
import type { Host } from '../lib/hosts'
|
|
import type { SERVE_TYPES } from '../lib/rclone/constants'
|
|
import type { ConfigFile } from '../types/config'
|
|
import type { ScheduledTask } from '../types/schedules'
|
|
import type { Template } from '../types/template'
|
|
import type { RemoteConfig as HostRemoteConfig } from './host'
|
|
|
|
const store = new LazyStore('store.json')
|
|
|
|
interface RemoteConfigV1 {
|
|
disabledActions?: ('tray' | 'tray-mount' | 'tray-browse' | 'tray-remove' | 'tray-cleanup')[]
|
|
|
|
defaultRemotePath?: string
|
|
defaultMountPoint?: string
|
|
mountOnStart?: boolean
|
|
|
|
mountDefaults?: Record<string, any>
|
|
vfsDefaults?: Record<string, any>
|
|
filterDefaults?: Record<string, any>
|
|
copyDefaults?: Record<string, any>
|
|
moveDefaults?: Record<string, any>
|
|
syncDefaults?: Record<string, any>
|
|
configDefaults?: Record<string, any>
|
|
serveDefaults?: Record<(typeof SERVE_TYPES)[number], Record<string, any>>
|
|
bisyncDefaults?: Record<string, any>
|
|
remoteDefaults?: Record<string, any>
|
|
}
|
|
|
|
type SupportedAction =
|
|
| 'tray-mount'
|
|
| 'tray-sync'
|
|
| 'tray-copy'
|
|
| 'tray-serve'
|
|
| 'tray-move'
|
|
| 'tray-bisync'
|
|
| 'tray-delete'
|
|
| 'tray-purge'
|
|
| 'tray-download'
|
|
|
|
interface TemplateV1 {
|
|
id: string
|
|
name: string
|
|
operation?: 'copy' | 'sync' | 'move' | 'delete' | 'purge' | 'serve' | 'mount' | 'bisync'
|
|
options: Record<string, any>
|
|
}
|
|
|
|
interface PersistedStateV1 {
|
|
remoteConfigList: Record<string, RemoteConfigV1>
|
|
setRemoteConfig: (remote: string, config: RemoteConfigV1) => void
|
|
mergeRemoteConfig: (remote: string, config: RemoteConfigV1) => void
|
|
|
|
disabledActions: SupportedAction[]
|
|
|
|
setDisabledActions: (actions: SupportedAction[]) => void
|
|
|
|
proxy:
|
|
| {
|
|
url: string
|
|
ignoredHosts: string[]
|
|
}
|
|
| undefined
|
|
|
|
favoritePaths: { remote: string; path: string; added: number }[]
|
|
|
|
settingsPass: string | undefined
|
|
setSettingsPass: (pass: string | undefined) => void
|
|
|
|
licenseKey: string | undefined
|
|
setLicenseKey: (key: string | undefined) => void
|
|
licenseValid: boolean
|
|
setLicenseValid: (valid: boolean) => void
|
|
|
|
startOnBoot: boolean
|
|
setStartOnBoot: (startOnBoot: boolean) => void
|
|
|
|
scheduledTasks: ScheduledTask[]
|
|
addScheduledTask: (
|
|
task: Omit<
|
|
ScheduledTask,
|
|
'id' | 'isRunning' | 'currentRunId' | 'lastRun' | 'configId' | 'isEnabled'
|
|
>
|
|
) => void
|
|
removeScheduledTask: (id: string) => void
|
|
updateScheduledTask: (id: string, task: Partial<ScheduledTask>) => void
|
|
|
|
templates: TemplateV1[]
|
|
|
|
configFiles: ConfigFile[]
|
|
addConfigFile: (configFile: ConfigFile) => void
|
|
removeConfigFile: (id: string) => void
|
|
activeConfigFile: ConfigFile | null
|
|
setActiveConfigFile: (configFile: string) => void
|
|
updateConfigFile: (id: string, configFile: Partial<ConfigFile>) => void
|
|
|
|
lastSkippedVersion: string | undefined
|
|
|
|
hideStartup: boolean
|
|
|
|
themeV2: {
|
|
tray: 'light' | 'dark' | undefined
|
|
}
|
|
}
|
|
|
|
interface PersistedStateV2 {
|
|
settingsPass: string | undefined
|
|
setSettingsPass: (pass: string | undefined) => void
|
|
|
|
licenseKey: string | undefined
|
|
setLicenseKey: (key: string | undefined) => void
|
|
licenseValid: boolean
|
|
setLicenseValid: (valid: boolean) => void
|
|
|
|
startOnBoot: boolean
|
|
setStartOnBoot: (startOnBoot: boolean) => void
|
|
|
|
toolbarShortcut: string | undefined
|
|
setToolbarShortcut: (shortcut: string | undefined) => Promise<void>
|
|
|
|
templates: Template[]
|
|
|
|
hosts: Host[]
|
|
currentHost: Host | null
|
|
updateHost: (id: Host['id'], host: Partial<Host>) => void
|
|
setCurrentHost: (id: Host['id']) => void
|
|
|
|
hideStartup: boolean
|
|
|
|
acknowledgements: string[]
|
|
|
|
appearance: {
|
|
tray: 'light' | 'dark' | 'system'
|
|
app: 'light' | 'dark' | 'system'
|
|
}
|
|
}
|
|
|
|
const getStorage = (store: LazyStore): StateStorage => ({
|
|
getItem: async (name: string): Promise<string | null> => {
|
|
console.log('getItem', { name })
|
|
return (await store.get(name)) ?? null
|
|
},
|
|
setItem: async (name: string, value: string): Promise<void> => {
|
|
console.log('setItem', { name, value })
|
|
await store.set(name, value)
|
|
await store.save()
|
|
},
|
|
removeItem: async (name: string): Promise<void> => {
|
|
console.log('removeItem', { name })
|
|
await store.delete(name)
|
|
await store.save()
|
|
},
|
|
})
|
|
|
|
export const usePersistedStore = create<PersistedStateV2>()(
|
|
persist(
|
|
(set) => ({
|
|
settingsPass: undefined,
|
|
setSettingsPass: (pass: string | undefined) => set((_) => ({ settingsPass: pass })),
|
|
|
|
licenseKey: undefined,
|
|
setLicenseKey: (key: string | undefined) => set((_) => ({ licenseKey: key })),
|
|
licenseValid: false,
|
|
setLicenseValid: (valid: boolean) => set((_) => ({ licenseValid: valid })),
|
|
|
|
startOnBoot: false,
|
|
setStartOnBoot: (startOnBoot: boolean) => set((_) => ({ startOnBoot })),
|
|
|
|
toolbarShortcut: undefined,
|
|
setToolbarShortcut: async (shortcut: string | undefined) => {
|
|
await invoke('update_toolbar_shortcut', { shortcut })
|
|
set((_) => ({ toolbarShortcut: shortcut }))
|
|
},
|
|
|
|
templates: [],
|
|
|
|
hosts: [],
|
|
currentHost: null,
|
|
updateHost: (id: Host['id'], host: Partial<Host>) =>
|
|
set((state) => {
|
|
if (!state.hosts.some((h) => h.id === id)) {
|
|
return {}
|
|
}
|
|
|
|
const hosts = state.hosts.map((h) =>
|
|
h.id === id ? { ...h, ...host, id: h.id } : h
|
|
)
|
|
|
|
const currentHost =
|
|
state.currentHost?.id === id
|
|
? (hosts.find((h) => h.id === id) ?? state.currentHost)
|
|
: state.currentHost
|
|
|
|
return { hosts, currentHost }
|
|
}),
|
|
setCurrentHost: (id: Host['id']) =>
|
|
set((state) => {
|
|
const host = state.hosts.find((h) => h.id === id)
|
|
if (!host) {
|
|
return {}
|
|
}
|
|
|
|
return { currentHost: host }
|
|
}),
|
|
|
|
hideStartup: false,
|
|
|
|
acknowledgements: [],
|
|
|
|
appearance: {
|
|
tray: 'system',
|
|
app: 'dark',
|
|
},
|
|
}),
|
|
{
|
|
name: 'store',
|
|
storage: createJSONStorage(() => getStorage(store)),
|
|
version: 2,
|
|
migrate: async (persistedState, version) => {
|
|
if (!persistedState) {
|
|
return persistedState as PersistedStateV2
|
|
}
|
|
|
|
if (version < 2) {
|
|
const legacyState = persistedState as PersistedStateV1
|
|
|
|
console.log('[Migration] Migrating from V1 to V2')
|
|
|
|
const localHostStore = new LazyStore('hosts/local/store.json')
|
|
|
|
const newRemoteConfigs: Record<string, HostRemoteConfig> = {}
|
|
const newTemplates: Template[] = legacyState.templates
|
|
? [
|
|
...legacyState.templates.map((template) => ({
|
|
...template,
|
|
tags: (template as unknown as TemplateV1).operation
|
|
? [(template as unknown as TemplateV1).operation!]
|
|
: [],
|
|
})),
|
|
]
|
|
: []
|
|
|
|
if (legacyState.remoteConfigList) {
|
|
for (const [key, config] of Object.entries(legacyState.remoteConfigList)) {
|
|
newRemoteConfigs[key] = {
|
|
mountOnStart: {
|
|
enabled: config.mountOnStart || false,
|
|
remotePath: config.defaultRemotePath || '',
|
|
mountPoint: config.defaultMountPoint || '',
|
|
mountOptions: config.mountDefaults || {},
|
|
vfsOptions: config.vfsDefaults || {},
|
|
filterOptions: config.filterDefaults || {},
|
|
configOptions: config.configDefaults || {},
|
|
},
|
|
}
|
|
|
|
// migrate defaults to templates
|
|
const mergedOptions: Record<string, any> = {
|
|
sources: [],
|
|
dest: '',
|
|
}
|
|
|
|
if (
|
|
config.copyDefaults &&
|
|
Object.keys(config.copyDefaults).length > 0
|
|
) {
|
|
mergedOptions.copyOptions = config.copyDefaults
|
|
}
|
|
if (config.vfsDefaults && Object.keys(config.vfsDefaults).length > 0) {
|
|
mergedOptions.vfsOptions = config.vfsDefaults
|
|
}
|
|
if (
|
|
config.filterDefaults &&
|
|
Object.keys(config.filterDefaults).length > 0
|
|
) {
|
|
mergedOptions.filterOptions = config.filterDefaults
|
|
}
|
|
if (
|
|
config.mountDefaults &&
|
|
Object.keys(config.mountDefaults).length > 0
|
|
) {
|
|
mergedOptions.mountOptions = config.mountDefaults
|
|
}
|
|
if (
|
|
config.configDefaults &&
|
|
Object.keys(config.configDefaults).length > 0
|
|
) {
|
|
mergedOptions.configOptions = config.configDefaults
|
|
}
|
|
if (
|
|
config.syncDefaults &&
|
|
Object.keys(config.syncDefaults).length > 0
|
|
) {
|
|
mergedOptions.syncOptions = config.syncDefaults
|
|
}
|
|
if (
|
|
config.moveDefaults &&
|
|
Object.keys(config.moveDefaults).length > 0
|
|
) {
|
|
mergedOptions.moveOptions = config.moveDefaults
|
|
}
|
|
if (
|
|
config.bisyncDefaults &&
|
|
Object.keys(config.bisyncDefaults).length > 0
|
|
) {
|
|
mergedOptions.bisyncOptions = config.bisyncDefaults
|
|
}
|
|
|
|
// check if any options have keys
|
|
const hasOptions = Object.values(mergedOptions).some(
|
|
(opt) =>
|
|
typeof opt === 'object' &&
|
|
opt !== null &&
|
|
Object.keys(opt).length > 0
|
|
)
|
|
|
|
if (hasOptions) {
|
|
newTemplates.push({
|
|
id: crypto.randomUUID(),
|
|
name: `${key} (Defaults)`,
|
|
tags: [
|
|
'copy',
|
|
'sync',
|
|
'move',
|
|
'delete',
|
|
'purge',
|
|
'serve',
|
|
'mount',
|
|
'bisync',
|
|
],
|
|
options: mergedOptions,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const hostState = {
|
|
state: {
|
|
remoteConfigs: newRemoteConfigs,
|
|
proxy: legacyState.proxy,
|
|
favoritePaths: legacyState.favoritePaths || [],
|
|
scheduledTasks: [],
|
|
configFiles: legacyState.configFiles,
|
|
activeConfigFile: legacyState.activeConfigFile,
|
|
lastSkippedVersion: legacyState.lastSkippedVersion,
|
|
},
|
|
version: 1,
|
|
}
|
|
|
|
try {
|
|
await localHostStore.set('host-store', JSON.stringify(hostState))
|
|
await localHostStore.save()
|
|
console.log(
|
|
'[Migration] Moved host-specific state to hosts/local/store.json'
|
|
)
|
|
} catch (e) {
|
|
console.error('[Migration] Failed to save host store', e)
|
|
await ask(
|
|
'Old data could not be migrated to V3. Please reinstall.\n\nYou can make a backup of the "store.json" file located in the app\'s directory before reinstalling.',
|
|
{
|
|
title: 'Fatal Error',
|
|
kind: 'error',
|
|
}
|
|
)
|
|
|
|
await exit()
|
|
}
|
|
|
|
return {
|
|
settingsPass: legacyState.settingsPass || undefined,
|
|
licenseKey: legacyState.licenseKey || undefined,
|
|
licenseValid: legacyState.licenseValid || false,
|
|
startOnBoot: legacyState.startOnBoot || false,
|
|
templates: newTemplates,
|
|
hideStartup: legacyState.hideStartup || false,
|
|
appearance: {
|
|
tray: 'system',
|
|
app: 'dark',
|
|
},
|
|
} as unknown as PersistedStateV2
|
|
}
|
|
|
|
return persistedState as PersistedStateV2
|
|
},
|
|
}
|
|
)
|
|
)
|
|
|
|
usePersistedStore.persist.onFinishHydration((state) => {
|
|
if (state.toolbarShortcut) {
|
|
invoke('update_toolbar_shortcut', { shortcut: state.toolbarShortcut })
|
|
}
|
|
})
|
|
|
|
store.onKeyChange('store', async (_) => {
|
|
await usePersistedStore.persist.rehydrate()
|
|
})
|