diff --git a/lib/menu.ts b/lib/menu.ts index 35d8160..4764cab 100644 --- a/lib/menu.ts +++ b/lib/menu.ts @@ -188,11 +188,10 @@ async function parseRemotes(remotes: string[]) { }) } catch (error) { Sentry.captureException(error) - await ask('Could not open browse window. Please try again.', { + await message('Could not open browse window. Please try again.', { title: 'Error', kind: 'error', okLabel: 'OK', - cancelLabel: '', }) } }, diff --git a/lib/rclone/api.ts b/lib/rclone/api.ts index c04e475..9651571 100644 --- a/lib/rclone/api.ts +++ b/lib/rclone/api.ts @@ -19,6 +19,7 @@ const SUPPORTED_BACKENDS = [ 'box', 'webdav', 'onedrive', + 'http', ] function getAuthHeader() { diff --git a/lib/rclone/init.ts b/lib/rclone/init.ts index 385eecc..0e833ba 100644 --- a/lib/rclone/init.ts +++ b/lib/rclone/init.ts @@ -9,7 +9,8 @@ import { fetch } from '@tauri-apps/plugin-http' import { platform } from '@tauri-apps/plugin-os' import { exit } from '@tauri-apps/plugin-process' import { Command } from '@tauri-apps/plugin-shell' -import { usePersistedStore } from '../store' +import { usePersistedStore, useStore } from '../store' +import { openSmallWindow } from '../window' import { getConfigPath, getDefaultPath, @@ -25,6 +26,12 @@ export async function initRclone(args: string[]) { // rclone not available, let's download it if (!system && !internal) { + usePersistedStore.setState({ isFirstOpen: false }) + useStore.setState({ startupStatus: 'initializing' }) + await openSmallWindow({ + name: 'Startup', + url: '/startup', + }) const success = await provisionRclone() if (!success) { await new Promise((resolve) => setTimeout(resolve, 1000)) diff --git a/lib/store.ts b/lib/store.ts index 545bfef..843b747 100644 --- a/lib/store.ts +++ b/lib/store.ts @@ -40,6 +40,8 @@ interface State { setRemotes: (remotes: string[]) => void addRemote: (remote: string) => void removeRemote: (remote: string) => void + + startupStatus: null | 'initializing' | 'initialized' } interface PersistedState { @@ -136,6 +138,8 @@ export const useStore = create()( set((state) => ({ remotes: [...state.remotes, remote] })), removeRemote: (remote: string) => set((state) => ({ remotes: state.remotes.filter((r) => r !== remote) })), + + startupStatus: null, }), { name: 'shared-store' } ) diff --git a/lib/window.ts b/lib/window.ts index 7c6aeb8..e53675b 100644 --- a/lib/window.ts +++ b/lib/window.ts @@ -27,7 +27,7 @@ export async function openFullWindow({ name: string url: string }) { - console.log('[openFullWindow]') + console.log('[openFullWindow] ', name, url) const w = new WebviewWindow(name, { height: 0, @@ -41,6 +41,8 @@ export async function openFullWindow({ decorations: true, url: url, theme: 'dark', + // @ts-expect-error + backgroundThrottling: 'disabled', }) let monitor = await currentMonitor() @@ -57,8 +59,7 @@ export async function openFullWindow({ } if (platform() === 'windows') { - // windows merges the space for the taskbar - // subtract from the height to have it show + // subtract from the height to correct for the taskbar size.height -= 100 } @@ -81,7 +82,7 @@ export async function openWindow({ width?: number height?: number }) { - console.log('[openWindow]') + console.log('[openWindow] ', name, url) const isFirstWindow = useStore.getState().firstWindow @@ -98,12 +99,13 @@ export async function openWindow({ decorations: false, url: url, theme: 'dark', - // parent: 'main', + // @ts-expect-error + backgroundThrottling: 'disabled', }) await getMainTray().then((t) => t?.setVisible(false)) await getLoadingTray().then((t) => t?.setVisible(true)) - await new Promise((resolve) => setTimeout(resolve, isFirstWindow ? 1000 : 150)) + await new Promise((resolve) => setTimeout(resolve, isFirstWindow ? 900 : 150)) await getLoadingTray().then((t) => t?.setVisible(false)) await getMainTray().then((t) => t?.setVisible(true)) @@ -118,6 +120,45 @@ export async function openWindow({ return w } +export async function openSmallWindow({ + name, + url, +}: { + name: string + url: string +}) { + console.log('[openSmallWindow] ', name, url) + + const isFirstWindow = useStore.getState().firstWindow + + const w = new WebviewWindow(name, { + height: 0, + width: 0, + resizable: false, + visibleOnAllWorkspaces: false, + alwaysOnTop: true, + visible: false, + focus: true, + title: name, + decorations: false, + url: url, + theme: 'dark', + closable: false, + // @ts-expect-error + backgroundThrottling: 'disabled', + }) + + await new Promise((resolve) => setTimeout(resolve, isFirstWindow ? 900 : 150)) + + await w.setSize(new LogicalSize(800, 500)) + await w.center() + await w.show() + + useStore.setState({ firstWindow: false }) + + return w +} + export async function lockWindows(ids?: string[]) { const windows = await getAllWindows() const lockedWindows = ids ? windows.filter((w) => ids.includes(w.label)) : windows diff --git a/main.ts b/main.ts index bfa478b..d66bbeb 100644 --- a/main.ts +++ b/main.ts @@ -23,6 +23,7 @@ import { import { initRclone } from './lib/rclone/init' import { usePersistedStore, useStore } from './lib/store' import { initLoadingTray, initTray, rebuildTrayMenu } from './lib/tray' +import { openSmallWindow } from './lib/window' import type { ScheduledTask } from './types/task' try { @@ -73,13 +74,12 @@ async function validateInstance() { const isOnline = navigator.onLine if (!isOnline && platform() !== 'linux') { - await ask( + await message( 'You are not connected to the internet. Please check your connection and try again.', { title: 'Error', kind: 'error', okLabel: 'Exit', - cancelLabel: '', } ) return await exit(0) @@ -148,11 +148,10 @@ async function startRclone() { ]) } catch (error) { Sentry.captureException(error) - await ask(error.message || 'Failed to start rclone, please try again later.', { + await message(error.message || 'Failed to start rclone, please try again later.', { title: 'Error', kind: 'error', okLabel: 'Exit', - cancelLabel: '', }) return await exit(0) } @@ -282,10 +281,10 @@ async function startupMounts() { async function onboardUser() { const firstOpen = usePersistedStore.getState().isFirstOpen if (firstOpen) { - await message('Rclone has initialized, you can now find it in the tray menu!', { - title: 'Welcome to Rclone UI', - kind: 'info', - okLabel: 'Got it', + useStore.setState({ startupStatus: 'initialized' }) + await openSmallWindow({ + name: 'Startup', + url: '/startup', }) usePersistedStore.setState({ isFirstOpen: false }) } diff --git a/package-lock.json b/package-lock.json index bab07ef..52a0009 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "s-tray", - "version": "1.9.3", + "name": "rclone-ui", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "s-tray", - "version": "1.9.3", + "name": "rclone-ui", + "version": "2.0.0", "dependencies": { "@heroui/react": "^2.7.11", "@sentry/browser": "^10.8.0", diff --git a/package.json b/package.json index 8213ec0..ae348fd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "s-tray", + "name": "rclone-ui", "private": true, - "version": "1.9.3", + "version": "2.0.0", "type": "module", "scripts": { "dev": "node scripts/buildExternal.js && vite", diff --git a/public/banner.png b/public/banner.png new file mode 100644 index 0000000..c9ea6d5 Binary files /dev/null and b/public/banner.png differ diff --git a/public/icons/gphotos.png b/public/icons/gphotos.png new file mode 100644 index 0000000..bfca9ec Binary files /dev/null and b/public/icons/gphotos.png differ diff --git a/public/icons/http.png b/public/icons/http.png new file mode 100644 index 0000000..373d89c Binary files /dev/null and b/public/icons/http.png differ diff --git a/public/icons/netstorage.png b/public/icons/netstorage.png new file mode 100644 index 0000000..993de4c Binary files /dev/null and b/public/icons/netstorage.png differ diff --git a/public/icons/oos.png b/public/icons/oos.png new file mode 100644 index 0000000..a1cea28 Binary files /dev/null and b/public/icons/oos.png differ diff --git a/public/icons/swift.png b/public/icons/swift.png new file mode 100644 index 0000000..e8d84bf Binary files /dev/null and b/public/icons/swift.png differ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 38e6925..314b3e2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -103,7 +103,7 @@ checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "app" -version = "1.9.3" +version = "2.0.0" dependencies = [ "cocoa", "fix-path-env", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 211d839..0ca041d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "1.9.3" +version = "2.0.0" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index f68e12e..851d55c 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -13,9 +13,8 @@ "Jobs", "Cron", "Browse", - "Test", - "Test2", - "Test3" + "Startup", + "Test" ], "permissions": [ "core:default", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3c29316..4cce1f2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,8 +2,8 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "Rclone UI", "mainBinaryName": "Rclone UI", - "version": "1.9.3", - "identifier": "com.stray.app", + "version": "2.0.0", + "identifier": "com.rclone.ui", "build": { "frontendDist": "../dist", "devUrl": "http://localhost:1420", diff --git a/src/main.tsx b/src/main.tsx index a611c54..5325c7d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,6 +12,7 @@ import Jobs from './pages/Jobs' import Mount from './pages/Mount' import Move from './pages/Move' import Settings from './pages/Settings' +import Startup from './pages/Startup' import Sync from './pages/Sync' import Test from './pages/Test' @@ -39,6 +40,10 @@ const router = createBrowserRouter([ path: '/', element: , }, + { + path: '/startup', + element: , + }, { path: '/settings', element: , diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index ddb9571..4d76be0 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -4,6 +4,7 @@ import { CardBody, Checkbox, Chip, + Divider, Dropdown, DropdownItem, DropdownMenu, @@ -50,7 +51,12 @@ import { } from 'lucide-react' import { type DetailedHTMLProps, type HTMLAttributes, useEffect, useRef, useState } from 'react' import { revokeMachineLicense, validateLicense } from '../../lib/license' -import { deleteRemote, getVersion as getCliVersion, getVersion } from '../../lib/rclone/api' +import { + deleteRemote, + getVersion as getCliVersion, + getRemote, + getVersion, +} from '../../lib/rclone/api' import { getConfigPath, getDefaultPaths } from '../../lib/rclone/common' import { usePersistedStore, useStore } from '../../lib/store' import { triggerTrayRebuild } from '../../lib/tray' @@ -339,7 +345,7 @@ function GeneralSection() { title: 'Update', kind: 'info', okLabel: 'Restart', - cancelLabel: '', + cancelLabel: 'Later', }) if (!answer) { @@ -599,134 +605,124 @@ function LicenseSection() {
-
-
-

Activate

-
+
+ setLicenseKeyInput(e.target.value)} + size="lg" + isDisabled={!isLicenseEditable || isActivating} + autoCapitalize="none" + autoComplete="off" + autoCorrect="off" + spellCheck="false" + endContent={licenseValid && } + data-focus-visible="false" + fullWidth={true} + /> -
- setLicenseKeyInput(e.target.value)} + {!licenseValid && ( + + )} + {licenseValid && ( + - )} - {licenseValid && ( - - )} -
+ } + setIsRevoking(false) + + await message('Your license has been successfully deactivated.', { + title: 'License deactivated', + kind: 'info', + }) + }} + data-focus-visible="false" + > + Deactivate + + )}
+ +
{ - if (!licenseValid && remotes.length >= 3) { + if (!licenseValid && remotes.length >= 4) { await message( - 'Community version does not support adding more than 3 remotes.', + 'Community version does not support adding more than 4 remotes.', { title: 'Missing license', kind: 'error', @@ -802,69 +798,32 @@ function RemotesSection() { />
{remotes.map((remote) => ( - - -
- {remote} -
- - - -
-
-
-
+ await deleteRemote(remote) + removeRemote(remote) + await triggerTrayRebuild() + }} + /> ))}
@@ -906,6 +865,86 @@ function RemotesSection() { ) } +function RemoteCard({ + remote, + onDefaultsPress, + onConfigPress, + onDeletePress, +}: { + remote: string + onDefaultsPress: () => void + onConfigPress: () => void + onDeletePress: () => void +}) { + const [type, setType] = useState(null) + + const imageUrl = type ? `/icons/${type}.png` : undefined + + useEffect(() => { + const loadRemoteConfig = async () => { + try { + const remoteInfo = await getRemote(remote) + setType(remoteInfo.type) + } catch (error) { + console.error('[RemoteCard] Failed to load remote config:', error) + } + } + loadRemoteConfig() + }, [remote]) + + return ( + + +
+
+ {remote} +

{remote}

+
+
+ + + +
+
+
+
+ ) +} + function ConfigSection() { const licenseValid = usePersistedStore((state) => state.licenseValid) @@ -1076,11 +1115,10 @@ function ConfigSection() { } if (configFile.id === 'default') { - await ask('Default config cannot be deleted', { + await message('Default config cannot be deleted', { title: 'Error', kind: 'warning', okLabel: 'OK', - cancelLabel: '', }) return } diff --git a/src/pages/Startup.tsx b/src/pages/Startup.tsx new file mode 100644 index 0000000..8c85540 --- /dev/null +++ b/src/pages/Startup.tsx @@ -0,0 +1,78 @@ +import { Button, Divider } from '@heroui/react' + +import { useEffect, useState } from 'react' +import { useStore } from '../../lib/store' + +const GREETINGS = [ + 'Hello', + 'こんにちは', + 'Salut', + 'Cześć', + 'Hej', + 'Bonjour', + 'Olá', + 'Ciao', + '你好', + 'Hallo', + 'Merhaba', + 'مرحباً', +] + +export default function Startup() { + const [greetingIndex, setGreetingIndex] = useState(0) + + const startupStatus = useStore((state) => state.startupStatus) + + const isInitialized = startupStatus === 'initialized' + + useEffect(() => { + const intervalId = setInterval(() => { + setGreetingIndex((previousIndex) => (previousIndex + 1) % GREETINGS.length) + }, 2500) + return () => clearInterval(intervalId) + }, []) + + return ( +
+ Rclone UI + + + +
+
+ {isInitialized && ( +

+ Rclone has initialized, you can find it in the tray menu! +

+ )} + {!isInitialized && ( +

+ + {GREETINGS[greetingIndex]} + {' '} + 👋 +

+ )} +
+
+ {isInitialized ? ( + + ) : ( +

Rclone is initalizing

+ )} +
+
+
+ ) +} diff --git a/tailwind.config.js b/tailwind.config.js index 251900d..0f37f2d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -8,7 +8,18 @@ export default { './node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}', ], theme: { - extend: {}, + extend: { + keyframes: { + 'fade-in-up': { + '0%': { opacity: '0', transform: 'translateY(24px)' }, + '60%': { opacity: '1' }, + '100%': { opacity: '1', transform: 'translateY(0)' }, + }, + }, + animation: { + 'fade-in-up': 'fade-in-up 900ms cubic-bezier(0.22, 1, 0.36, 1)', + }, + }, }, plugins: [heroui()], }