build status: failing

This commit is contained in:
FTCHD
2025-01-22 14:47:24 +02:00
commit af044bca82
93 changed files with 18425 additions and 0 deletions
+28
View File
@@ -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])
}
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)))
}