build status: failing
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
export function formatBytes(bytes: number) {
|
||||
// format bytes in to a readable format like (MB, GB, etc), depending on how big the number is
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`
|
||||
}
|
||||
|
||||
if (bytes < 1024 * 1024) {
|
||||
return `${(bytes / 1024).toFixed(2)} KB`
|
||||
}
|
||||
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
||||
}
|
||||
|
||||
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
export function replaceSmartQuotes(value: string) {
|
||||
const replacements: { [key: string]: string } = {
|
||||
'‘': "'",
|
||||
'’': "'",
|
||||
'‚': "'",
|
||||
'“': '"',
|
||||
'”': '"',
|
||||
'„': '"',
|
||||
}
|
||||
return value.replace(/[‘’‚“”„]/g, (match) => replacements[match])
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { readDir } from '@tauri-apps/plugin-fs'
|
||||
|
||||
export async function isDirectoryEmpty(path: string): Promise<boolean> {
|
||||
try {
|
||||
const entries = await readDir(path)
|
||||
return entries.length === 0
|
||||
} catch (err) {
|
||||
console.error('Error checking directory:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function isRemotePath(path: string): boolean {
|
||||
return path.includes(':/') && !path.startsWith('/')
|
||||
}
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
import { Menu, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'
|
||||
import { ask, confirm, message, open } from '@tauri-apps/plugin-dialog'
|
||||
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification'
|
||||
import { sendNotification } from '@tauri-apps/plugin-notification'
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener'
|
||||
import { exit } from '@tauri-apps/plugin-process'
|
||||
import { isDirectoryEmpty } from './fs'
|
||||
import { deleteRemote, mountRemote, unmountRemote } from './rclone'
|
||||
import { usePersistedStore, useStore } from './store'
|
||||
import { getLoadingTray, getMainTray } from './tray'
|
||||
import { lockWindows, openFullWindow, openTrayWindow, openWindow, unlockWindows } from './window'
|
||||
|
||||
// Function to rebuild and update the menu
|
||||
export async function buildMenu() {
|
||||
const storeState = useStore.getState()
|
||||
|
||||
const persistedStoreState = usePersistedStore.getState()
|
||||
|
||||
const remotes = storeState.remotes
|
||||
|
||||
const menuItems: (MenuItem | Submenu | PredefinedMenuItem)[] = []
|
||||
|
||||
// Add remote submenus
|
||||
for (const remote of remotes) {
|
||||
const remoteConfig = persistedStoreState.remoteConfigList?.[remote]
|
||||
if (remoteConfig?.hideTray) {
|
||||
continue
|
||||
}
|
||||
|
||||
const submenuItems: (MenuItem | Submenu | PredefinedMenuItem)[] = []
|
||||
|
||||
const alreadyMounted = storeState.mountedRemotes[remote]
|
||||
|
||||
if (alreadyMounted) {
|
||||
const unmountMenuItem = await MenuItem.new({
|
||||
id: `unmount-${remote}`,
|
||||
text: 'Unmount',
|
||||
action: async () => {
|
||||
try {
|
||||
const mountPoint = storeState.mountedRemotes[remote]
|
||||
if (!mountPoint) {
|
||||
console.error(`No mount point found for remote ${remote}`)
|
||||
return
|
||||
}
|
||||
await unmountRemote(mountPoint)
|
||||
delete storeState.mountedRemotes[remote]
|
||||
// await rebuildTrayMenu()
|
||||
await message(`Successfully unmounted ${remote} from ${mountPoint}`, {
|
||||
title: 'Unmount Success',
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Unmount operation failed:', err)
|
||||
await message(`Failed to unmount ${remote}: ${err}`, {
|
||||
title: 'Unmount Error',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
submenuItems.push(unmountMenuItem)
|
||||
|
||||
// Add "Open in Finder" option for mounted remotes
|
||||
const mountPoint = storeState.mountedRemotes[remote]
|
||||
console.log('Adding Open in Finder', mountPoint)
|
||||
const openInFinderItem = await MenuItem.new({
|
||||
id: `open-${remote}`,
|
||||
|
||||
text: 'Open in Finder',
|
||||
action: async () => {
|
||||
console.log('Open in Finder')
|
||||
console.log(mountPoint)
|
||||
if (mountPoint) {
|
||||
console.log('Opening in Finder')
|
||||
await revealItemInDir(mountPoint)
|
||||
console.log('Opened in Finder')
|
||||
}
|
||||
},
|
||||
})
|
||||
submenuItems.push(openInFinderItem)
|
||||
}
|
||||
|
||||
if (!alreadyMounted && !remoteConfig?.disabledActions?.includes('mount')) {
|
||||
const mountMenuItem = await MenuItem.new({
|
||||
id: `mount-${remote}`,
|
||||
text: 'Quick Mount',
|
||||
action: async () => {
|
||||
await getMainTray().then((t) => t?.setVisible(false))
|
||||
|
||||
await getLoadingTray().then((t) => t?.setVisible(true))
|
||||
|
||||
try {
|
||||
await lockWindows()
|
||||
|
||||
let selectedPath = remoteConfig.defaultMountPoint || null
|
||||
|
||||
if (!selectedPath) {
|
||||
selectedPath = await open({
|
||||
title: `Select a directory to mount "${remote}"`,
|
||||
multiple: false,
|
||||
directory: true,
|
||||
})
|
||||
}
|
||||
|
||||
if (!selectedPath) {
|
||||
// await resetMainWindow()
|
||||
return
|
||||
}
|
||||
|
||||
console.log('selectedPath', selectedPath)
|
||||
|
||||
// Check if directory is empty
|
||||
const isEmpty = await isDirectoryEmpty(selectedPath)
|
||||
if (!isEmpty) {
|
||||
// await resetMainWindow()
|
||||
|
||||
await message(
|
||||
'The selected directory must be empty to mount a remote.',
|
||||
{
|
||||
title: 'Mount Error',
|
||||
kind: 'error',
|
||||
}
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Mount the remote
|
||||
await mountRemote({
|
||||
remotePath: `${remote}:${remoteConfig.defaultRemotePath || ''}`,
|
||||
mountPoint: selectedPath,
|
||||
mountOptions: remoteConfig.mountDefaults,
|
||||
vfsOptions: remoteConfig.vfsDefaults,
|
||||
})
|
||||
storeState.mountedRemotes[remote] = selectedPath
|
||||
|
||||
// await rebuildTrayMenu()
|
||||
|
||||
let permissionGranted = await isPermissionGranted()
|
||||
|
||||
if (!permissionGranted) {
|
||||
const permission = await requestPermission()
|
||||
permissionGranted = permission === 'granted'
|
||||
}
|
||||
|
||||
if (permissionGranted) {
|
||||
sendNotification({
|
||||
title: 'Mounted',
|
||||
body: `Successfully mounted ${remote} to ${selectedPath}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (!remoteConfig.defaultMountPoint) {
|
||||
const answer = await ask(
|
||||
`Mount successful! Do you want to set ${selectedPath} as the default mount point for ${remote}? You can always change it later in Remote settings.`,
|
||||
{
|
||||
title: 'Set Default?',
|
||||
okLabel: 'Set',
|
||||
cancelLabel: 'Cancel',
|
||||
}
|
||||
)
|
||||
if (answer) {
|
||||
usePersistedStore.setState((state) => ({
|
||||
remoteConfigList: {
|
||||
...state.remoteConfigList,
|
||||
[remote]: {
|
||||
...state.remoteConfigList[remote],
|
||||
defaultMountPoint: selectedPath,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// await resetMainWindow()
|
||||
console.error('Mount operation failed:', err)
|
||||
await message(`Failed to mount ${remote}: ${err}`, {
|
||||
title: 'Mount Error',
|
||||
})
|
||||
} finally {
|
||||
await unlockWindows()
|
||||
await getLoadingTray().then((t) => t?.setVisible(false))
|
||||
await getMainTray().then((t) => t?.setVisible(true))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
submenuItems.push(mountMenuItem)
|
||||
}
|
||||
|
||||
if (!remoteConfig?.disabledActions?.includes('browse')) {
|
||||
const browseMenuItem = await MenuItem.new({
|
||||
id: `browse-${remote}`,
|
||||
text: 'Browse',
|
||||
action: async () => {
|
||||
// await openBrowser(`http://localhost:5572/[${remote}:]/`)
|
||||
await openFullWindow({
|
||||
name: 'Browse',
|
||||
// url: 'browse.html?url=https%3A%2F%2Fwww.google.com%2F',
|
||||
url:
|
||||
'browse.html?url=' +
|
||||
encodeURIComponent(`http://localhost:5572/[${remote}:]/`),
|
||||
})
|
||||
},
|
||||
})
|
||||
submenuItems.push(browseMenuItem)
|
||||
}
|
||||
|
||||
if (!remoteConfig?.disabledActions?.includes('remove')) {
|
||||
const removeMenuItem = await MenuItem.new({
|
||||
id: `remove-${remote}`,
|
||||
text: 'Remove',
|
||||
action: async () => {
|
||||
const confirmation = await confirm(
|
||||
`Are you sure you want to remove ${remote}? This action cannot be reverted.`,
|
||||
{ title: `Removing ${remote}`, kind: 'warning' }
|
||||
)
|
||||
|
||||
if (!confirmation) {
|
||||
return
|
||||
}
|
||||
|
||||
await deleteRemote(remote)
|
||||
// await rebuildTrayMenu()
|
||||
},
|
||||
})
|
||||
submenuItems.push(removeMenuItem)
|
||||
}
|
||||
|
||||
const sub = await Submenu.new({
|
||||
items: submenuItems,
|
||||
text: remote,
|
||||
})
|
||||
|
||||
menuItems.push(sub)
|
||||
}
|
||||
|
||||
await PredefinedMenuItem.new({
|
||||
item: 'Separator',
|
||||
}).then((item) => {
|
||||
menuItems.push(item)
|
||||
})
|
||||
|
||||
if (!persistedStoreState.disabledActions?.includes('mount')) {
|
||||
const mountToMenuItem = await MenuItem.new({
|
||||
id: 'mount',
|
||||
text: 'Mount',
|
||||
action: async () => {
|
||||
await openWindow({
|
||||
name: 'Mount',
|
||||
url: '/mount',
|
||||
})
|
||||
},
|
||||
})
|
||||
menuItems.push(mountToMenuItem)
|
||||
}
|
||||
|
||||
if (!persistedStoreState.disabledActions?.includes('copy')) {
|
||||
const copyMenuItem = await MenuItem.new({
|
||||
id: 'copy',
|
||||
text: 'Copy',
|
||||
action: async () => {
|
||||
await openWindow({
|
||||
name: 'Copy',
|
||||
url: '/copy',
|
||||
})
|
||||
},
|
||||
})
|
||||
menuItems.push(copyMenuItem)
|
||||
}
|
||||
|
||||
if (!persistedStoreState.disabledActions?.includes('sync')) {
|
||||
const syncMenuItem = await MenuItem.new({
|
||||
id: 'sync',
|
||||
text: 'Sync',
|
||||
action: async () => {
|
||||
await openWindow({
|
||||
name: 'Sync',
|
||||
url: '/sync',
|
||||
})
|
||||
},
|
||||
})
|
||||
menuItems.push(syncMenuItem)
|
||||
}
|
||||
|
||||
const jobsMenuItem = await MenuItem.new({
|
||||
id: 'jobs',
|
||||
text: 'Jobs',
|
||||
action: async () => {
|
||||
await openTrayWindow({
|
||||
name: 'Jobs',
|
||||
url: '/jobs',
|
||||
})
|
||||
},
|
||||
})
|
||||
menuItems.push(jobsMenuItem)
|
||||
|
||||
await PredefinedMenuItem.new({
|
||||
item: 'Separator',
|
||||
}).then((item) => {
|
||||
menuItems.push(item)
|
||||
})
|
||||
|
||||
const settingsItem = await MenuItem.new({
|
||||
id: 'settings',
|
||||
text: 'Settings',
|
||||
action: async () => {
|
||||
await openWindow({
|
||||
name: 'Settings',
|
||||
url: '/settings',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
menuItems.push(settingsItem)
|
||||
|
||||
const quitItem = await MenuItem.new({
|
||||
id: 'quit',
|
||||
text: 'Quit',
|
||||
action: async () => {
|
||||
await exit(0)
|
||||
},
|
||||
})
|
||||
|
||||
menuItems.push(quitItem)
|
||||
|
||||
const testItem = await MenuItem.new({
|
||||
id: 'test',
|
||||
text: 'Test',
|
||||
action: async () => {
|
||||
await openWindow({
|
||||
name: 'Test',
|
||||
url: '/test',
|
||||
width: 400,
|
||||
height: 400,
|
||||
})
|
||||
},
|
||||
})
|
||||
menuItems.push(testItem)
|
||||
|
||||
const test2Item = await MenuItem.new({
|
||||
id: 'test2',
|
||||
text: 'Test2',
|
||||
action: async () => {
|
||||
await openWindow({
|
||||
name: 'Test2',
|
||||
url: '/test',
|
||||
width: 400,
|
||||
height: 400,
|
||||
})
|
||||
},
|
||||
})
|
||||
menuItems.push(test2Item)
|
||||
|
||||
return await Menu.new({
|
||||
id: 'main-menu',
|
||||
|
||||
items: menuItems,
|
||||
})
|
||||
}
|
||||
+514
@@ -0,0 +1,514 @@
|
||||
import { ask } from '@tauri-apps/plugin-dialog'
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
import { Command } from '@tauri-apps/plugin-shell'
|
||||
|
||||
/* DATA */
|
||||
|
||||
export async function listRemotes() {
|
||||
const r = await fetch('http://localhost:5572/config/listremotes', {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<{ remotes: string[] }>)
|
||||
// .catch((e) => {
|
||||
// console.log("error", e);
|
||||
// throw e;
|
||||
// });
|
||||
|
||||
if (typeof r?.remotes === 'undefined') {
|
||||
throw new Error('Failed to fetch remotes')
|
||||
}
|
||||
|
||||
return r.remotes
|
||||
}
|
||||
|
||||
export async function getRemote(remote: string) {
|
||||
const r = await fetch(`http://localhost:5572/config/get?name=${remote}`, {
|
||||
method: 'POST',
|
||||
}).then(
|
||||
(res) => res.json() as Promise<{ type: string } & Record<string, string | number | boolean>>
|
||||
)
|
||||
// .catch((e) => {
|
||||
// console.log("error", e);
|
||||
// throw e;
|
||||
// });
|
||||
|
||||
console.log(JSON.stringify(r, null, 2))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
export async function updateRemote(
|
||||
remote: string,
|
||||
parameters: Record<string, string | number | boolean>
|
||||
) {
|
||||
console.log('updateRemote', remote, parameters)
|
||||
|
||||
const options = new URLSearchParams()
|
||||
options.set('name', remote)
|
||||
options.set('parameters', JSON.stringify(parameters))
|
||||
|
||||
await fetch(`http://localhost:5572/config/update?${options.toString()}`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
// console.log(JSON.stringify(r, null, 2))
|
||||
}
|
||||
|
||||
export async function createRemote(
|
||||
name: string,
|
||||
type: string,
|
||||
parameters: Record<string, string | number | boolean>
|
||||
) {
|
||||
console.log('createRemote', name, type, parameters)
|
||||
|
||||
const options = new URLSearchParams()
|
||||
options.set('name', name)
|
||||
options.set('type', type)
|
||||
options.set('parameters', JSON.stringify(parameters))
|
||||
|
||||
await fetch(`http://localhost:5572/config/create?${options.toString()}`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
// console.log(JSON.stringify(r, null, 2))
|
||||
}
|
||||
|
||||
export async function deleteRemote(remote: string) {
|
||||
await fetch(`http://localhost:5572/config/delete?name=${remote}`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
// console.log(JSON.stringify(r, null, 2))
|
||||
}
|
||||
|
||||
export async function getMountPoints() {
|
||||
const r = await fetch('http://localhost:5572/mount/listmounts', {
|
||||
method: 'POST',
|
||||
}).then(
|
||||
(res) =>
|
||||
res.json() as Promise<{
|
||||
mountPoints: string[]
|
||||
}>
|
||||
)
|
||||
|
||||
// console.log('Mount points:', r)
|
||||
|
||||
if (!Array.isArray(r?.mountPoints)) {
|
||||
throw new Error('Failed to get mount points')
|
||||
}
|
||||
|
||||
return r.mountPoints
|
||||
}
|
||||
|
||||
const SUPPORRTED_BACKENDS = ['sftp', 's3', 'b2', 'drive']
|
||||
|
||||
export async function getBackends() {
|
||||
const providers = await fetch('http://localhost:5572/config/providers', {
|
||||
method: 'POST',
|
||||
})
|
||||
.then((res) => res.json() as Promise<any>)
|
||||
.then((r) => r.providers)
|
||||
|
||||
return providers.filter((b: any) => SUPPORRTED_BACKENDS.includes(b.Name))
|
||||
}
|
||||
|
||||
export interface ListOptions {
|
||||
recurse?: boolean
|
||||
noModTime?: boolean
|
||||
showEncrypted?: boolean
|
||||
showOrigIDs?: boolean
|
||||
showHash?: boolean
|
||||
noMimeType?: boolean
|
||||
dirsOnly?: boolean
|
||||
filesOnly?: boolean
|
||||
metadata?: boolean
|
||||
hashTypes?: string[]
|
||||
}
|
||||
|
||||
export async function listPath(remote: string, path: string = '', options: ListOptions = {}) {
|
||||
const params = new URLSearchParams()
|
||||
params.set('fs', `${remote}:`)
|
||||
params.set('remote', path)
|
||||
|
||||
// Add optional parameters
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) {
|
||||
params.append(key, v)
|
||||
}
|
||||
} else if (value !== undefined) {
|
||||
params.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`http://localhost:5572/operations/list?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
}).then(
|
||||
(res) =>
|
||||
res.json() as Promise<{
|
||||
list: {
|
||||
Hashes?: Record<string, string>
|
||||
ID?: string
|
||||
OrigID?: string
|
||||
IsBucket?: boolean
|
||||
IsDir: boolean
|
||||
MimeType?: string
|
||||
ModTime?: string
|
||||
Name: string
|
||||
Encrypted?: string
|
||||
EncryptedPath?: string
|
||||
Path: string
|
||||
Size?: number
|
||||
Tier?: string
|
||||
}[]
|
||||
}>
|
||||
)
|
||||
|
||||
return response?.list || []
|
||||
}
|
||||
/* JOBS */
|
||||
|
||||
export async function listJobs() {
|
||||
const allStats = await fetch('http://localhost:5572/core/stats', {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as any)
|
||||
|
||||
const transferring = allStats?.transferring
|
||||
|
||||
const transferredStats = await fetch('http://localhost:5572/core/transferred', {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as any)
|
||||
|
||||
const transferred = transferredStats?.transferred
|
||||
|
||||
const jobs = {
|
||||
active: [] as any[],
|
||||
inactive: [] as any[],
|
||||
}
|
||||
|
||||
const activeJobIds = new Set(
|
||||
transferring
|
||||
?.filter((t: any) => t.group.startsWith('job/'))
|
||||
.map((t: any) => Number(t.group.split('/')[1]))
|
||||
.sort((a: number, b: number) => a - b)
|
||||
)
|
||||
|
||||
for (const jobId of activeJobIds) {
|
||||
const job = await fetch(`http://localhost:5572/core/stats?group=job/${jobId}`, {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
jobs.active.push({
|
||||
id: jobId,
|
||||
bytes: job.bytes,
|
||||
totalBytes: job.totalBytes,
|
||||
speed: job.speed,
|
||||
|
||||
done: job.bytes === job.totalBytes,
|
||||
progress: Math.round((job.bytes / job.totalBytes) * 100),
|
||||
fatal: job.fatalError,
|
||||
|
||||
srcFs: transferred.find((t: any) => t.group === `job/${jobId}`)?.srcFs,
|
||||
dstFs: transferred.find((t: any) => t.group === `job/${jobId}`)?.dstFs,
|
||||
})
|
||||
}
|
||||
|
||||
const inactiveJobIds = new Set(
|
||||
transferred
|
||||
?.filter((t: any) => t.group.startsWith('job/'))
|
||||
.map((t: any) => Number(t.group.split('/')[1]))
|
||||
.filter((id: number) => !activeJobIds.has(id))
|
||||
.sort((a: number, b: number) => a - b)
|
||||
)
|
||||
|
||||
for (const jobId of inactiveJobIds) {
|
||||
const job = await fetch(`http://localhost:5572/core/stats?group=job/${jobId}`, {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
jobs.inactive.push({
|
||||
id: jobId,
|
||||
bytes: job.bytes,
|
||||
totalBytes: job.totalBytes,
|
||||
speed: 0,
|
||||
|
||||
done: job.bytes === job.totalBytes,
|
||||
progress: Math.round((job.bytes / job.totalBytes) * 100),
|
||||
fatal: job.fatalError,
|
||||
|
||||
srcFs: transferred.find((t: any) => t.group === `job/${jobId}`)?.srcFs,
|
||||
dstFs: transferred.find((t: any) => t.group === `job/${jobId}`)?.dstFs,
|
||||
})
|
||||
}
|
||||
|
||||
return jobs
|
||||
}
|
||||
|
||||
export async function stopJob(jobId: number) {
|
||||
await fetch(`http://localhost:5572/job/stopgroup?group=job/${jobId}`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
/* OPERATIONS */
|
||||
|
||||
export async function mountRemote({
|
||||
remotePath,
|
||||
mountPoint,
|
||||
mountOptions,
|
||||
vfsOptions,
|
||||
}: {
|
||||
remotePath: string
|
||||
mountPoint: string
|
||||
mountOptions?: Record<string, string | number | boolean>
|
||||
vfsOptions?: Record<string, string | number | boolean>
|
||||
}) {
|
||||
const options = new URLSearchParams()
|
||||
options.set('fs', remotePath)
|
||||
options.set('mountPoint', mountPoint)
|
||||
|
||||
if (mountOptions && Object.keys(mountOptions).length > 0) {
|
||||
options.set('mountOpt', JSON.stringify(mountOptions))
|
||||
}
|
||||
|
||||
if (vfsOptions && Object.keys(vfsOptions).length > 0) {
|
||||
options.set('vfsOpt', JSON.stringify(vfsOptions))
|
||||
}
|
||||
|
||||
const r = await fetch(`http://localhost:5572/mount/mount?${options.toString()}`, {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<{ remotes: string[] } | Promise<{ error: string }>>)
|
||||
|
||||
if ('error' in r) {
|
||||
throw new Error(r.error)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
export async function unmountRemote(mountPoint: string, force = false) {
|
||||
const command = Command.create('umount', [force ? '-f' : '', mountPoint])
|
||||
|
||||
const output = await command.execute()
|
||||
|
||||
// console.log('output')
|
||||
// console.log(JSON.stringify(output, null, 2))
|
||||
|
||||
if (output.code !== 0) {
|
||||
if (output.stderr.toLowerCase().includes('busy')) {
|
||||
const answer = await ask('This resource is busy, do you want to force unmount?', {
|
||||
title: 'Could not unmount',
|
||||
kind: 'warning',
|
||||
})
|
||||
if (answer) {
|
||||
return await unmountRemote(mountPoint, true)
|
||||
}
|
||||
throw new Error(output.stderr)
|
||||
}
|
||||
|
||||
if (output.stderr.toLowerCase().includes('not currently mounted')) {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(output.stderr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
export async function startCopy({
|
||||
source,
|
||||
dest,
|
||||
copyOptions,
|
||||
filterOptions,
|
||||
}: {
|
||||
source: string
|
||||
dest: string
|
||||
copyOptions: Record<string, string | number | boolean> | undefined
|
||||
filterOptions: Record<string, string | number | boolean> | undefined
|
||||
}) {
|
||||
const params = new URLSearchParams()
|
||||
params.set('srcFs', source)
|
||||
params.set('dstFs', dest)
|
||||
// params.set('b2_disable_checksum', 'true')
|
||||
params.set('_async', 'true')
|
||||
|
||||
console.log('params', params.toString())
|
||||
|
||||
if (copyOptions && Object.keys(copyOptions).length > 0) {
|
||||
params.set('_config', JSON.stringify(copyOptions))
|
||||
}
|
||||
console.log('copyOptions', copyOptions)
|
||||
|
||||
if (filterOptions && Object.keys(filterOptions).length > 0) {
|
||||
params.set('_filter', JSON.stringify(filterOptions))
|
||||
}
|
||||
console.log('filterOptions', filterOptions)
|
||||
|
||||
const r = await fetch(`http://localhost:5572/sync/copy?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<{ jobid: string }>)
|
||||
|
||||
console.log('Copy operation started:', r)
|
||||
return
|
||||
|
||||
// if (!r.jobid) {
|
||||
// throw new Error("Failed to start copy job");
|
||||
// }
|
||||
|
||||
// Monitor job status
|
||||
// while (true) {
|
||||
// const status = await fetch(
|
||||
// `http://localhost:5572/job/status/${r.jobid}`,
|
||||
// {
|
||||
// method: "POST",
|
||||
// },
|
||||
// ).then((res) => res.json() as Promise<RcloneJobStatus>);
|
||||
|
||||
// if (status.finished) {
|
||||
// return status.success;
|
||||
// }
|
||||
|
||||
// // Wait a bit before checking again
|
||||
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
// }
|
||||
}
|
||||
|
||||
export async function startSync({
|
||||
source,
|
||||
dest,
|
||||
syncOptions,
|
||||
filterOptions,
|
||||
}: {
|
||||
source: string
|
||||
dest: string
|
||||
syncOptions: Record<string, string | number | boolean> | undefined
|
||||
filterOptions: Record<string, string | number | boolean> | undefined
|
||||
}) {
|
||||
const params = new URLSearchParams()
|
||||
params.set('srcFs', source)
|
||||
params.set('dstFs', dest)
|
||||
// params.set('b2_disable_checksum', 'true')
|
||||
params.set('_async', 'true')
|
||||
|
||||
console.log('params', params.toString())
|
||||
|
||||
if (syncOptions && Object.keys(syncOptions).length > 0) {
|
||||
params.set('_config', JSON.stringify(syncOptions))
|
||||
}
|
||||
console.log('syncOptions', syncOptions)
|
||||
|
||||
if (filterOptions && Object.keys(filterOptions).length > 0) {
|
||||
params.set('_filter', JSON.stringify(filterOptions))
|
||||
}
|
||||
console.log('filterOptions', filterOptions)
|
||||
|
||||
const r = await fetch(`http://localhost:5572/sync/sync?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<{ jobid: string }>)
|
||||
|
||||
console.log('Sync operation started:', r)
|
||||
return
|
||||
|
||||
// if (!r.jobid) {
|
||||
// throw new Error("Failed to start copy job");
|
||||
// }
|
||||
|
||||
// Monitor job status
|
||||
// while (true) {
|
||||
// const status = await fetch(
|
||||
// `http://localhost:5572/job/status/${r.jobid}`,
|
||||
// {
|
||||
// method: "POST",
|
||||
// },
|
||||
// ).then((res) => res.json() as Promise<RcloneJobStatus>);
|
||||
|
||||
// if (status.finished) {
|
||||
// return status.success;
|
||||
// }
|
||||
|
||||
// // Wait a bit before checking again
|
||||
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
// }
|
||||
}
|
||||
|
||||
/* FLAGS */
|
||||
|
||||
export async function getGlobalFlags() {
|
||||
const r = await fetch('http://localhost:5572/options/get', {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
export async function getCopyFlags() {
|
||||
const r = await fetch('http://localhost:5572/options/info', {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
const mainFlags = r.main
|
||||
|
||||
const copyFlags = mainFlags.filter(
|
||||
(flag: any) => flag?.Groups?.includes('Copy') || flag?.Groups?.includes('Performance')
|
||||
)
|
||||
|
||||
return copyFlags
|
||||
}
|
||||
|
||||
export async function getSyncFlags() {
|
||||
const r = await fetch('http://localhost:5572/options/info', {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
const mainFlags = r.main
|
||||
|
||||
const syncFlags = mainFlags.filter(
|
||||
(flag: any) =>
|
||||
flag?.Groups?.includes('Copy') ||
|
||||
flag?.Groups?.includes('Sync') ||
|
||||
flag?.Groups?.includes('Performance')
|
||||
)
|
||||
|
||||
return syncFlags
|
||||
}
|
||||
|
||||
export async function getFilterFlags() {
|
||||
const r = await fetch('http://localhost:5572/options/info', {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
const filterFlags = r.filter
|
||||
|
||||
// ignore "Metadata" fields as they have the same FieldNames as the normal non-metadata filters
|
||||
const filteredFlags = filterFlags.filter((flag: any) => !flag.Groups.includes('Metadata'))
|
||||
|
||||
return filteredFlags
|
||||
}
|
||||
|
||||
export async function getVfsFlags() {
|
||||
const r = await fetch('http://localhost:5572/options/info', {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
const vfsFlags = r.vfs
|
||||
|
||||
const IGNORED_FLAGS = ['NONE']
|
||||
|
||||
const filteredFlags = vfsFlags.filter((flag: any) => !IGNORED_FLAGS.includes(flag.Name))
|
||||
|
||||
return filteredFlags
|
||||
}
|
||||
|
||||
export async function getMountFlags() {
|
||||
const r = await fetch('http://localhost:5572/options/info', {
|
||||
method: 'POST',
|
||||
}).then((res) => res.json() as Promise<any>)
|
||||
|
||||
const mountFlags = r.mount
|
||||
|
||||
const IGNORED_FLAGS = ['debug_fuse', 'daemon', 'daemon_timeout']
|
||||
|
||||
const filteredFlags = mountFlags.filter((flag: any) => !IGNORED_FLAGS.includes(flag.Name))
|
||||
|
||||
return filteredFlags
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { LazyStore } from '@tauri-apps/plugin-store'
|
||||
import { shared } from 'use-broadcast-ts'
|
||||
import { create } from 'zustand'
|
||||
import { type StateStorage, createJSONStorage, persist } from 'zustand/middleware'
|
||||
|
||||
// const { LazyStore } = window.__TAURI__.store
|
||||
const store = new LazyStore('store.json')
|
||||
|
||||
export interface RemoteConfig {
|
||||
hideTray?: boolean
|
||||
|
||||
disabledActions?: ('mount' | 'browse' | 'remove')[]
|
||||
|
||||
defaultRemotePath?: string
|
||||
defaultMountPoint?: string
|
||||
mountOnStart?: boolean
|
||||
|
||||
mountDefaults?: Record<string, any>
|
||||
vfsDefaults?: Record<string, any>
|
||||
filterDefaults?: Record<string, any>
|
||||
copyDefaults?: Record<string, any>
|
||||
syncDefaults?: Record<string, any>
|
||||
}
|
||||
|
||||
interface State {
|
||||
count: number
|
||||
anotherCount: number
|
||||
increment: () => void
|
||||
|
||||
rcloneLoaded: boolean
|
||||
mountedRemotes: Record<string, 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
|
||||
removeRemote: (remote: string) => void
|
||||
}
|
||||
|
||||
interface PersistedState {
|
||||
remoteConfigList: Record<string, RemoteConfig>
|
||||
setRemoteConfig: (remote: string, config: RemoteConfig) => void
|
||||
mergeRemoteConfig: (remote: string, config: RemoteConfig) => void
|
||||
|
||||
disabledActions: ('mount' | 'sync' | 'copy' | 'serve')[]
|
||||
}
|
||||
|
||||
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 useStore = create<State>()(
|
||||
shared(
|
||||
(set) => ({
|
||||
count: 0,
|
||||
anotherCount: 0,
|
||||
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||
|
||||
rcloneLoaded: false,
|
||||
mountedRemotes: {},
|
||||
|
||||
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) =>
|
||||
set((state) => ({ remotes: [...state.remotes, remote] })),
|
||||
removeRemote: (remote: string) =>
|
||||
set((state) => ({ remotes: state.remotes.filter((r) => r !== remote) })),
|
||||
}),
|
||||
{ name: 'shared-store' }
|
||||
)
|
||||
)
|
||||
|
||||
export const usePersistedStore = create<PersistedState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
remoteConfigList: {},
|
||||
setRemoteConfig: (remote: string, config: Record<string, any>) =>
|
||||
set((state) => ({
|
||||
remoteConfigList: { ...state.remoteConfigList, [remote]: config },
|
||||
})),
|
||||
mergeRemoteConfig: (remote: string, config: Record<string, any>) =>
|
||||
set((state) => ({
|
||||
remoteConfigList: {
|
||||
...state.remoteConfigList,
|
||||
[remote]: { ...state.remoteConfigList[remote], ...config },
|
||||
},
|
||||
})),
|
||||
|
||||
disabledActions: [],
|
||||
}),
|
||||
{
|
||||
name: 'store',
|
||||
storage: createJSONStorage(() => getStorage(store)),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// useStore.persist.onFinishHydration(() => {
|
||||
// console.log('onFinishHydration')
|
||||
// })
|
||||
|
||||
store.onKeyChange('store', async (_) => {
|
||||
await usePersistedStore.persist.rehydrate()
|
||||
})
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import type { TrayIconEvent } from '@tauri-apps/api/tray'
|
||||
import { TrayIcon } from '@tauri-apps/api/tray'
|
||||
import { getAllWindows } from '@tauri-apps/api/window'
|
||||
import { handleIconState } from '@tauri-apps/plugin-positioner'
|
||||
import { buildMenu } from './menu'
|
||||
import { resetMainWindow } from './window'
|
||||
|
||||
export async function getMainTray() {
|
||||
return await TrayIcon.getById('main-tray')
|
||||
}
|
||||
|
||||
export async function getLoadingTray() {
|
||||
return await TrayIcon.getById('loading-tray')
|
||||
}
|
||||
|
||||
export async function triggerTrayRebuild() {
|
||||
return getAllWindows().then((windows) => {
|
||||
windows.find((w) => w.label === 'main')?.emit('rebuild-tray')
|
||||
})
|
||||
}
|
||||
|
||||
// Function to update the tray menu
|
||||
export async function rebuildTrayMenu() {
|
||||
const tray = await getMainTray()
|
||||
if (!tray) {
|
||||
return
|
||||
}
|
||||
const newMenu = await buildMenu()
|
||||
await tray.setMenu(newMenu)
|
||||
}
|
||||
|
||||
async function onTrayAction(event: TrayIconEvent) {
|
||||
await handleIconState(event)
|
||||
|
||||
if (event.type === 'Click') {
|
||||
console.log('Tray clicked:', event)
|
||||
|
||||
await resetMainWindow()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the tray
|
||||
export async function initTray(): Promise<void> {
|
||||
try {
|
||||
console.log('initTray')
|
||||
const menu = await buildMenu()
|
||||
console.log('built menu')
|
||||
|
||||
await TrayIcon.getById('loading-tray').then((t) => t?.setVisible(false))
|
||||
console.log('set loading tray to false')
|
||||
|
||||
await TrayIcon.new({
|
||||
id: 'main-tray',
|
||||
icon: 'icons/icon.png',
|
||||
tooltip: 'S-Tray App',
|
||||
menu,
|
||||
menuOnLeftClick: true,
|
||||
action: onTrayAction,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to create tray:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function initLoadingTray() {
|
||||
const loadingTray = await TrayIcon.new({
|
||||
id: 'loading-tray',
|
||||
icon: 'icons/favicon/frame_00_delay-0.1s.png',
|
||||
})
|
||||
|
||||
let currentIcon = 1
|
||||
|
||||
setInterval(async () => {
|
||||
if (currentIcon > 17) {
|
||||
currentIcon = 1
|
||||
}
|
||||
await loadingTray?.setIcon(
|
||||
`icons/favicon/frame_${currentIcon < 10 ? '0' : ''}${currentIcon}_delay-0.1s.png`
|
||||
)
|
||||
currentIcon = currentIcon + 1
|
||||
}, 200)
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { LogicalSize, PhysicalSize, currentMonitor, getAllWindows } from '@tauri-apps/api/window'
|
||||
import { Position, moveWindow } from '@tauri-apps/plugin-positioner'
|
||||
|
||||
export async function resetMainWindow() {
|
||||
const window = await getAllWindows().then((w) => w.find((w) => w.label === 'main'))
|
||||
if (!window) return
|
||||
|
||||
await window.setSize(new PhysicalSize(0, 0))
|
||||
await window.center()
|
||||
await window.hide()
|
||||
await window.setAlwaysOnTop(true)
|
||||
}
|
||||
|
||||
export async function openFullWindow({
|
||||
name,
|
||||
url,
|
||||
}: {
|
||||
name: string
|
||||
url: string
|
||||
}) {
|
||||
const w = new WebviewWindow(name, {
|
||||
height: 0,
|
||||
width: 0,
|
||||
visibleOnAllWorkspaces: false,
|
||||
alwaysOnTop: false,
|
||||
resizable: true,
|
||||
visible: true,
|
||||
focus: true,
|
||||
title: name,
|
||||
decorations: true,
|
||||
url: url,
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const size = await currentMonitor().then((m) => m?.size)
|
||||
|
||||
if (!size) return
|
||||
|
||||
await w.hide()
|
||||
await w.setSize(size)
|
||||
await w.center()
|
||||
await w.show()
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
export async function openWindow({
|
||||
name,
|
||||
url,
|
||||
width = 740,
|
||||
height = 600,
|
||||
}: {
|
||||
name: string
|
||||
url: string
|
||||
width?: number
|
||||
height?: number
|
||||
}) {
|
||||
const w = new WebviewWindow(name, {
|
||||
height: 0,
|
||||
width: 0,
|
||||
resizable: false,
|
||||
visibleOnAllWorkspaces: false,
|
||||
alwaysOnTop: true,
|
||||
visible: true,
|
||||
focus: true,
|
||||
title: name,
|
||||
decorations: true,
|
||||
url: url,
|
||||
// parent: 'main',
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
await w.hide()
|
||||
await w.setSize(new LogicalSize(width, height))
|
||||
await w.center()
|
||||
await w.show()
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
export async function openTrayWindow({
|
||||
name,
|
||||
url,
|
||||
}: {
|
||||
name: string
|
||||
url: string
|
||||
}) {
|
||||
const w = new WebviewWindow(name, {
|
||||
height: 0,
|
||||
width: 0,
|
||||
resizable: false,
|
||||
visibleOnAllWorkspaces: true,
|
||||
alwaysOnTop: true,
|
||||
visible: true,
|
||||
focus: true,
|
||||
title: name,
|
||||
decorations: false,
|
||||
url: url,
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
await w.hide()
|
||||
await w.setSize(new LogicalSize(400, 600))
|
||||
// await w.center()
|
||||
await w.show()
|
||||
await moveWindow(Position.TopRight)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
export async function lockWindows(ids?: string[]) {
|
||||
const windows = await getAllWindows()
|
||||
const lockedWindows = ids ? windows.filter((w) => ids.includes(w.label)) : windows
|
||||
await Promise.all(lockedWindows.map((w) => w.setClosable(false)))
|
||||
}
|
||||
|
||||
export async function unlockWindows(ids?: string[]) {
|
||||
const windows = await getAllWindows()
|
||||
const unlockedWindows = ids ? windows.filter((w) => ids.includes(w.label)) : windows
|
||||
await Promise.all(unlockedWindows.map((w) => w.setClosable(true)))
|
||||
}
|
||||
Reference in New Issue
Block a user