+1
-2
@@ -188,11 +188,10 @@ async function parseRemotes(remotes: string[]) {
|
|||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Sentry.captureException(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',
|
title: 'Error',
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
okLabel: 'OK',
|
okLabel: 'OK',
|
||||||
cancelLabel: '',
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const SUPPORTED_BACKENDS = [
|
|||||||
'box',
|
'box',
|
||||||
'webdav',
|
'webdav',
|
||||||
'onedrive',
|
'onedrive',
|
||||||
|
'http',
|
||||||
]
|
]
|
||||||
|
|
||||||
function getAuthHeader() {
|
function getAuthHeader() {
|
||||||
|
|||||||
+8
-1
@@ -9,7 +9,8 @@ import { fetch } from '@tauri-apps/plugin-http'
|
|||||||
import { platform } from '@tauri-apps/plugin-os'
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
import { exit } from '@tauri-apps/plugin-process'
|
import { exit } from '@tauri-apps/plugin-process'
|
||||||
import { Command } from '@tauri-apps/plugin-shell'
|
import { Command } from '@tauri-apps/plugin-shell'
|
||||||
import { usePersistedStore } from '../store'
|
import { usePersistedStore, useStore } from '../store'
|
||||||
|
import { openSmallWindow } from '../window'
|
||||||
import {
|
import {
|
||||||
getConfigPath,
|
getConfigPath,
|
||||||
getDefaultPath,
|
getDefaultPath,
|
||||||
@@ -25,6 +26,12 @@ export async function initRclone(args: string[]) {
|
|||||||
|
|
||||||
// rclone not available, let's download it
|
// rclone not available, let's download it
|
||||||
if (!system && !internal) {
|
if (!system && !internal) {
|
||||||
|
usePersistedStore.setState({ isFirstOpen: false })
|
||||||
|
useStore.setState({ startupStatus: 'initializing' })
|
||||||
|
await openSmallWindow({
|
||||||
|
name: 'Startup',
|
||||||
|
url: '/startup',
|
||||||
|
})
|
||||||
const success = await provisionRclone()
|
const success = await provisionRclone()
|
||||||
if (!success) {
|
if (!success) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ interface State {
|
|||||||
setRemotes: (remotes: string[]) => void
|
setRemotes: (remotes: string[]) => void
|
||||||
addRemote: (remote: string) => void
|
addRemote: (remote: string) => void
|
||||||
removeRemote: (remote: string) => void
|
removeRemote: (remote: string) => void
|
||||||
|
|
||||||
|
startupStatus: null | 'initializing' | 'initialized'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PersistedState {
|
interface PersistedState {
|
||||||
@@ -136,6 +138,8 @@ export const useStore = create<State>()(
|
|||||||
set((state) => ({ remotes: [...state.remotes, remote] })),
|
set((state) => ({ remotes: [...state.remotes, remote] })),
|
||||||
removeRemote: (remote: string) =>
|
removeRemote: (remote: string) =>
|
||||||
set((state) => ({ remotes: state.remotes.filter((r) => r !== remote) })),
|
set((state) => ({ remotes: state.remotes.filter((r) => r !== remote) })),
|
||||||
|
|
||||||
|
startupStatus: null,
|
||||||
}),
|
}),
|
||||||
{ name: 'shared-store' }
|
{ name: 'shared-store' }
|
||||||
)
|
)
|
||||||
|
|||||||
+47
-6
@@ -27,7 +27,7 @@ export async function openFullWindow({
|
|||||||
name: string
|
name: string
|
||||||
url: string
|
url: string
|
||||||
}) {
|
}) {
|
||||||
console.log('[openFullWindow]')
|
console.log('[openFullWindow] ', name, url)
|
||||||
|
|
||||||
const w = new WebviewWindow(name, {
|
const w = new WebviewWindow(name, {
|
||||||
height: 0,
|
height: 0,
|
||||||
@@ -41,6 +41,8 @@ export async function openFullWindow({
|
|||||||
decorations: true,
|
decorations: true,
|
||||||
url: url,
|
url: url,
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
|
// @ts-expect-error
|
||||||
|
backgroundThrottling: 'disabled',
|
||||||
})
|
})
|
||||||
|
|
||||||
let monitor = await currentMonitor()
|
let monitor = await currentMonitor()
|
||||||
@@ -57,8 +59,7 @@ export async function openFullWindow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (platform() === 'windows') {
|
if (platform() === 'windows') {
|
||||||
// windows merges the space for the taskbar
|
// subtract from the height to correct for the taskbar
|
||||||
// subtract from the height to have it show
|
|
||||||
size.height -= 100
|
size.height -= 100
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ export async function openWindow({
|
|||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
}) {
|
}) {
|
||||||
console.log('[openWindow]')
|
console.log('[openWindow] ', name, url)
|
||||||
|
|
||||||
const isFirstWindow = useStore.getState().firstWindow
|
const isFirstWindow = useStore.getState().firstWindow
|
||||||
|
|
||||||
@@ -98,12 +99,13 @@ export async function openWindow({
|
|||||||
decorations: false,
|
decorations: false,
|
||||||
url: url,
|
url: url,
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
// parent: 'main',
|
// @ts-expect-error
|
||||||
|
backgroundThrottling: 'disabled',
|
||||||
})
|
})
|
||||||
|
|
||||||
await getMainTray().then((t) => t?.setVisible(false))
|
await getMainTray().then((t) => t?.setVisible(false))
|
||||||
await getLoadingTray().then((t) => t?.setVisible(true))
|
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 getLoadingTray().then((t) => t?.setVisible(false))
|
||||||
await getMainTray().then((t) => t?.setVisible(true))
|
await getMainTray().then((t) => t?.setVisible(true))
|
||||||
|
|
||||||
@@ -118,6 +120,45 @@ export async function openWindow({
|
|||||||
return w
|
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[]) {
|
export async function lockWindows(ids?: string[]) {
|
||||||
const windows = await getAllWindows()
|
const windows = await getAllWindows()
|
||||||
const lockedWindows = ids ? windows.filter((w) => ids.includes(w.label)) : windows
|
const lockedWindows = ids ? windows.filter((w) => ids.includes(w.label)) : windows
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { initRclone } from './lib/rclone/init'
|
import { initRclone } from './lib/rclone/init'
|
||||||
import { usePersistedStore, useStore } from './lib/store'
|
import { usePersistedStore, useStore } from './lib/store'
|
||||||
import { initLoadingTray, initTray, rebuildTrayMenu } from './lib/tray'
|
import { initLoadingTray, initTray, rebuildTrayMenu } from './lib/tray'
|
||||||
|
import { openSmallWindow } from './lib/window'
|
||||||
import type { ScheduledTask } from './types/task'
|
import type { ScheduledTask } from './types/task'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -73,13 +74,12 @@ async function validateInstance() {
|
|||||||
const isOnline = navigator.onLine
|
const isOnline = navigator.onLine
|
||||||
|
|
||||||
if (!isOnline && platform() !== 'linux') {
|
if (!isOnline && platform() !== 'linux') {
|
||||||
await ask(
|
await message(
|
||||||
'You are not connected to the internet. Please check your connection and try again.',
|
'You are not connected to the internet. Please check your connection and try again.',
|
||||||
{
|
{
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
okLabel: 'Exit',
|
okLabel: 'Exit',
|
||||||
cancelLabel: '',
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return await exit(0)
|
return await exit(0)
|
||||||
@@ -148,11 +148,10 @@ async function startRclone() {
|
|||||||
])
|
])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Sentry.captureException(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',
|
title: 'Error',
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
okLabel: 'Exit',
|
okLabel: 'Exit',
|
||||||
cancelLabel: '',
|
|
||||||
})
|
})
|
||||||
return await exit(0)
|
return await exit(0)
|
||||||
}
|
}
|
||||||
@@ -282,10 +281,10 @@ async function startupMounts() {
|
|||||||
async function onboardUser() {
|
async function onboardUser() {
|
||||||
const firstOpen = usePersistedStore.getState().isFirstOpen
|
const firstOpen = usePersistedStore.getState().isFirstOpen
|
||||||
if (firstOpen) {
|
if (firstOpen) {
|
||||||
await message('Rclone has initialized, you can now find it in the tray menu!', {
|
useStore.setState({ startupStatus: 'initialized' })
|
||||||
title: 'Welcome to Rclone UI',
|
await openSmallWindow({
|
||||||
kind: 'info',
|
name: 'Startup',
|
||||||
okLabel: 'Got it',
|
url: '/startup',
|
||||||
})
|
})
|
||||||
usePersistedStore.setState({ isFirstOpen: false })
|
usePersistedStore.setState({ isFirstOpen: false })
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "s-tray",
|
"name": "rclone-ui",
|
||||||
"version": "1.9.3",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "s-tray",
|
"name": "rclone-ui",
|
||||||
"version": "1.9.3",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroui/react": "^2.7.11",
|
"@heroui/react": "^2.7.11",
|
||||||
"@sentry/browser": "^10.8.0",
|
"@sentry/browser": "^10.8.0",
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "s-tray",
|
"name": "rclone-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.9.3",
|
"version": "2.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/buildExternal.js && vite",
|
"dev": "node scripts/buildExternal.js && vite",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 530 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
Generated
+1
-1
@@ -103,7 +103,7 @@ checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "1.9.3"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"fix-path-env",
|
"fix-path-env",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "1.9.3"
|
version = "2.0.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@@ -13,9 +13,8 @@
|
|||||||
"Jobs",
|
"Jobs",
|
||||||
"Cron",
|
"Cron",
|
||||||
"Browse",
|
"Browse",
|
||||||
"Test",
|
"Startup",
|
||||||
"Test2",
|
"Test"
|
||||||
"Test3"
|
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Rclone UI",
|
"productName": "Rclone UI",
|
||||||
"mainBinaryName": "Rclone UI",
|
"mainBinaryName": "Rclone UI",
|
||||||
"version": "1.9.3",
|
"version": "2.0.0",
|
||||||
"identifier": "com.stray.app",
|
"identifier": "com.rclone.ui",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Jobs from './pages/Jobs'
|
|||||||
import Mount from './pages/Mount'
|
import Mount from './pages/Mount'
|
||||||
import Move from './pages/Move'
|
import Move from './pages/Move'
|
||||||
import Settings from './pages/Settings'
|
import Settings from './pages/Settings'
|
||||||
|
import Startup from './pages/Startup'
|
||||||
import Sync from './pages/Sync'
|
import Sync from './pages/Sync'
|
||||||
import Test from './pages/Test'
|
import Test from './pages/Test'
|
||||||
|
|
||||||
@@ -39,6 +40,10 @@ const router = createBrowserRouter([
|
|||||||
path: '/',
|
path: '/',
|
||||||
element: <Home />,
|
element: <Home />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/startup',
|
||||||
|
element: <Startup />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
element: <Settings />,
|
element: <Settings />,
|
||||||
|
|||||||
+216
-178
@@ -4,6 +4,7 @@ import {
|
|||||||
CardBody,
|
CardBody,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Chip,
|
Chip,
|
||||||
|
Divider,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -50,7 +51,12 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { type DetailedHTMLProps, type HTMLAttributes, useEffect, useRef, useState } from 'react'
|
import { type DetailedHTMLProps, type HTMLAttributes, useEffect, useRef, useState } from 'react'
|
||||||
import { revokeMachineLicense, validateLicense } from '../../lib/license'
|
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 { getConfigPath, getDefaultPaths } from '../../lib/rclone/common'
|
||||||
import { usePersistedStore, useStore } from '../../lib/store'
|
import { usePersistedStore, useStore } from '../../lib/store'
|
||||||
import { triggerTrayRebuild } from '../../lib/tray'
|
import { triggerTrayRebuild } from '../../lib/tray'
|
||||||
@@ -339,7 +345,7 @@ function GeneralSection() {
|
|||||||
title: 'Update',
|
title: 'Update',
|
||||||
kind: 'info',
|
kind: 'info',
|
||||||
okLabel: 'Restart',
|
okLabel: 'Restart',
|
||||||
cancelLabel: '',
|
cancelLabel: 'Later',
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!answer) {
|
if (!answer) {
|
||||||
@@ -599,134 +605,124 @@ function LicenseSection() {
|
|||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<BaseHeader title="License" />
|
<BaseHeader title="License" />
|
||||||
|
|
||||||
<div className="flex flex-row justify-center w-full gap-8 px-8">
|
<div className="flex flex-row justify-center w-full gap-2 px-8">
|
||||||
<div className="flex flex-col items-end w-2/6 gap-2 bg-transparent-500">
|
<Input
|
||||||
<h3 className="font-medium">Activate</h3>
|
placeholder="Enter license key"
|
||||||
</div>
|
value={licenseKeyInput}
|
||||||
|
onChange={(e) => setLicenseKeyInput(e.target.value)}
|
||||||
|
size="lg"
|
||||||
|
isDisabled={!isLicenseEditable || isActivating}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck="false"
|
||||||
|
endContent={licenseValid && <CheckIcon className="w-5 h-5 text-green-500" />}
|
||||||
|
data-focus-visible="false"
|
||||||
|
fullWidth={true}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col w-4/6 gap-2 bg-transparent-500">
|
{!licenseValid && (
|
||||||
<Input
|
<Button
|
||||||
placeholder="Enter license key"
|
isLoading={isActivating}
|
||||||
value={licenseKeyInput}
|
|
||||||
onChange={(e) => setLicenseKeyInput(e.target.value)}
|
|
||||||
size="lg"
|
size="lg"
|
||||||
isDisabled={!isLicenseEditable || isActivating}
|
onPress={async () => {
|
||||||
autoCapitalize="none"
|
if (!licenseKeyInput) {
|
||||||
autoComplete="off"
|
await message('Please enter a license key', {
|
||||||
autoCorrect="off"
|
title: 'Error',
|
||||||
spellCheck="false"
|
kind: 'error',
|
||||||
endContent={
|
})
|
||||||
licenseValid && <CheckIcon className="w-5 h-5 text-green-500" />
|
return
|
||||||
}
|
}
|
||||||
|
setIsActivating(true)
|
||||||
|
try {
|
||||||
|
await validateLicense(licenseKeyInput)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
await message(e.message, {
|
||||||
|
title: 'Error',
|
||||||
|
kind: 'error',
|
||||||
|
okLabel: 'Ok',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await message('An error occurred. Please try again.', {
|
||||||
|
title: 'Error',
|
||||||
|
kind: 'error',
|
||||||
|
okLabel: 'Ok',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setIsActivating(false)
|
||||||
|
|
||||||
|
await message('Your license has been successfully activated.', {
|
||||||
|
title: 'Congrats!',
|
||||||
|
kind: 'info',
|
||||||
|
})
|
||||||
|
}}
|
||||||
data-focus-visible="false"
|
data-focus-visible="false"
|
||||||
/>
|
>
|
||||||
|
Activate
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{licenseValid && (
|
||||||
|
<Button
|
||||||
|
isLoading={isRevoking}
|
||||||
|
color="danger"
|
||||||
|
variant="ghost"
|
||||||
|
onPress={async () => {
|
||||||
|
// usePersistedStore.setState({
|
||||||
|
// licenseKey: undefined,
|
||||||
|
// licenseValid: false,
|
||||||
|
// })
|
||||||
|
// return
|
||||||
|
|
||||||
{!licenseValid && (
|
const answer = await ask(
|
||||||
<Button
|
'Are you sure you want to deactivate your license? You can always activate it again later.',
|
||||||
fullWidth={true}
|
{
|
||||||
isLoading={isActivating}
|
title: 'Deactivate License',
|
||||||
onPress={async () => {
|
kind: 'warning',
|
||||||
if (!licenseKeyInput) {
|
|
||||||
await message('Please enter a license key', {
|
|
||||||
title: 'Error',
|
|
||||||
kind: 'error',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
setIsActivating(true)
|
)
|
||||||
try {
|
|
||||||
await validateLicense(licenseKeyInput)
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
await ask(e.message, {
|
|
||||||
title: 'Error',
|
|
||||||
kind: 'error',
|
|
||||||
okLabel: 'Ok',
|
|
||||||
cancelLabel: '',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await ask('An error occurred. Please try again.', {
|
if (!answer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRevoking(true)
|
||||||
|
try {
|
||||||
|
await revokeMachineLicense(licenseKeyInput)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
await message(e.message, {
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
okLabel: 'Ok',
|
okLabel: 'Ok',
|
||||||
cancelLabel: '',
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
setIsActivating(false)
|
|
||||||
|
|
||||||
await message('Your license has been successfully activated.', {
|
|
||||||
title: 'Congrats!',
|
|
||||||
kind: 'info',
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
data-focus-visible="false"
|
|
||||||
>
|
|
||||||
Activate
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{licenseValid && (
|
|
||||||
<Button
|
|
||||||
fullWidth={true}
|
|
||||||
isLoading={isRevoking}
|
|
||||||
color="danger"
|
|
||||||
variant="ghost"
|
|
||||||
onPress={async () => {
|
|
||||||
// usePersistedStore.setState({
|
|
||||||
// licenseKey: undefined,
|
|
||||||
// licenseValid: false,
|
|
||||||
// })
|
|
||||||
// return
|
|
||||||
|
|
||||||
const answer = await ask(
|
|
||||||
'Are you sure you want to deactivate your license? You can always activate it again later.',
|
|
||||||
{
|
|
||||||
title: 'Deactivate License',
|
|
||||||
kind: 'warning',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!answer) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsRevoking(true)
|
await message('An error occurred. Please try again.', {
|
||||||
try {
|
title: 'Error',
|
||||||
await revokeMachineLicense(licenseKeyInput)
|
kind: 'error',
|
||||||
} catch (e) {
|
okLabel: 'Ok',
|
||||||
if (e instanceof Error) {
|
|
||||||
await ask(e.message, {
|
|
||||||
title: 'Error',
|
|
||||||
kind: 'error',
|
|
||||||
okLabel: 'Ok',
|
|
||||||
cancelLabel: '',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await ask('An error occurred. Please try again.', {
|
|
||||||
title: 'Error',
|
|
||||||
kind: 'error',
|
|
||||||
okLabel: 'Ok',
|
|
||||||
cancelLabel: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setIsRevoking(false)
|
|
||||||
|
|
||||||
await message('Your license has been successfully deactivated.', {
|
|
||||||
title: 'License deactivated',
|
|
||||||
kind: 'info',
|
|
||||||
})
|
})
|
||||||
}}
|
}
|
||||||
data-focus-visible="false"
|
setIsRevoking(false)
|
||||||
>
|
|
||||||
Deactivate
|
await message('Your license has been successfully deactivated.', {
|
||||||
</Button>
|
title: 'License deactivated',
|
||||||
)}
|
kind: 'info',
|
||||||
</div>
|
})
|
||||||
|
}}
|
||||||
|
data-focus-visible="false"
|
||||||
|
>
|
||||||
|
Deactivate
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full overflow-hidden border-0 border-red-500 left-28 h-[470px] opacity-0 transition-opacity duration-300 ease-in-out',
|
'w-full overflow-hidden border-0 border-red-500 left-28 h-[470px] opacity-0 transition-opacity duration-300 ease-in-out',
|
||||||
@@ -777,9 +773,9 @@ function RemotesSection() {
|
|||||||
endContent={
|
endContent={
|
||||||
<Button
|
<Button
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
if (!licenseValid && remotes.length >= 3) {
|
if (!licenseValid && remotes.length >= 4) {
|
||||||
await message(
|
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',
|
title: 'Missing license',
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
@@ -802,69 +798,32 @@ function RemotesSection() {
|
|||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-2 p-4">
|
<div className="flex flex-col gap-2 p-4">
|
||||||
{remotes.map((remote) => (
|
{remotes.map((remote) => (
|
||||||
<Card
|
<RemoteCard
|
||||||
key={remote}
|
key={remote}
|
||||||
shadow="sm"
|
remote={remote}
|
||||||
isBlurred={true}
|
onDefaultsPress={() => {
|
||||||
className="border-none bg-background/60 dark:bg-default-100/60"
|
setPickedRemote(remote)
|
||||||
>
|
setDefaultsDrawerOpen(true)
|
||||||
<CardBody>
|
}}
|
||||||
<div className="flex items-center justify-between">
|
onConfigPress={() => {
|
||||||
<span>{remote}</span>
|
setPickedRemote(remote)
|
||||||
<div className="flex flex-row items-center gap-2">
|
setEditingDrawerOpen(true)
|
||||||
<Button
|
}}
|
||||||
onPress={() => {
|
onDeletePress={async () => {
|
||||||
setPickedRemote(remote)
|
const confirmation = await ask(
|
||||||
setDefaultsDrawerOpen(true)
|
`Are you sure you want to remove ${remote}? This action cannot be reverted.`,
|
||||||
}}
|
{ title: `Removing ${remote}`, kind: 'warning' }
|
||||||
// isIconOnly={true}
|
)
|
||||||
color="primary"
|
|
||||||
variant="flat"
|
|
||||||
data-focus-visible="false"
|
|
||||||
>
|
|
||||||
{/* <CableIcon className="w-4 h-4" /> */}
|
|
||||||
Edit Defaults
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onPress={() => {
|
|
||||||
setPickedRemote(remote)
|
|
||||||
setEditingDrawerOpen(true)
|
|
||||||
}}
|
|
||||||
// isIconOnly={true}
|
|
||||||
// color="primary"
|
|
||||||
variant="faded"
|
|
||||||
data-focus-visible="false"
|
|
||||||
>
|
|
||||||
{/* <PencilIcon className="w-4 h-4" /> */}
|
|
||||||
Config
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isIconOnly={true}
|
|
||||||
color="danger"
|
|
||||||
variant="light"
|
|
||||||
size="sm"
|
|
||||||
onPress={async () => {
|
|
||||||
const confirmation = await ask(
|
|
||||||
`Are you sure you want to remove ${remote}? This action cannot be reverted.`,
|
|
||||||
{ title: `Removing ${remote}`, kind: 'warning' }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!confirmation) {
|
if (!confirmation) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteRemote(remote)
|
await deleteRemote(remote)
|
||||||
removeRemote(remote)
|
removeRemote(remote)
|
||||||
await triggerTrayRebuild()
|
await triggerTrayRebuild()
|
||||||
}}
|
}}
|
||||||
data-focus-visible="false"
|
/>
|
||||||
>
|
|
||||||
<Trash2Icon className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -906,6 +865,86 @@ function RemotesSection() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RemoteCard({
|
||||||
|
remote,
|
||||||
|
onDefaultsPress,
|
||||||
|
onConfigPress,
|
||||||
|
onDeletePress,
|
||||||
|
}: {
|
||||||
|
remote: string
|
||||||
|
onDefaultsPress: () => void
|
||||||
|
onConfigPress: () => void
|
||||||
|
onDeletePress: () => void
|
||||||
|
}) {
|
||||||
|
const [type, setType] = useState<string | null>(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 (
|
||||||
|
<Card
|
||||||
|
key={remote}
|
||||||
|
shadow="sm"
|
||||||
|
isBlurred={true}
|
||||||
|
className="border-none bg-background/60 dark:bg-default-100/60"
|
||||||
|
isPressable={true}
|
||||||
|
onPress={onConfigPress}
|
||||||
|
>
|
||||||
|
<CardBody>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img src={imageUrl} className="object-contain w-10 h-10" alt={remote} />
|
||||||
|
<p className="text-large">{remote}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<Button
|
||||||
|
onPress={onDefaultsPress}
|
||||||
|
// isIconOnly={true}
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
data-focus-visible="false"
|
||||||
|
>
|
||||||
|
{/* <CableIcon className="w-4 h-4" /> */}
|
||||||
|
Edit Defaults
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={onConfigPress}
|
||||||
|
// isIconOnly={true}
|
||||||
|
// color="primary"
|
||||||
|
variant="faded"
|
||||||
|
data-focus-visible="false"
|
||||||
|
>
|
||||||
|
{/* <PencilIcon className="w-4 h-4" /> */}
|
||||||
|
Config
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isIconOnly={true}
|
||||||
|
color="danger"
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
onPress={onDeletePress}
|
||||||
|
data-focus-visible="false"
|
||||||
|
>
|
||||||
|
<Trash2Icon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function ConfigSection() {
|
function ConfigSection() {
|
||||||
const licenseValid = usePersistedStore((state) => state.licenseValid)
|
const licenseValid = usePersistedStore((state) => state.licenseValid)
|
||||||
|
|
||||||
@@ -1076,11 +1115,10 @@ function ConfigSection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (configFile.id === 'default') {
|
if (configFile.id === 'default') {
|
||||||
await ask('Default config cannot be deleted', {
|
await message('Default config cannot be deleted', {
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
kind: 'warning',
|
kind: 'warning',
|
||||||
okLabel: 'OK',
|
okLabel: 'OK',
|
||||||
cancelLabel: '',
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="flex flex-col h-screen rounded-lg">
|
||||||
|
<img src="/banner.png" alt="Rclone UI" className="w-full h-auto p-5" />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div className="flex flex-col w-full h-full justify-evenly">
|
||||||
|
<div className="flex flex-col items-center w-full gap-8 overflow-visible">
|
||||||
|
{isInitialized && (
|
||||||
|
<p className="ml-2 text-2xl">
|
||||||
|
Rclone has initialized, you can find it in the tray menu!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!isInitialized && (
|
||||||
|
<p className="ml-2 text-3xl">
|
||||||
|
<span
|
||||||
|
key={greetingIndex}
|
||||||
|
className="inline-block align-middle animate-fade-in-up"
|
||||||
|
>
|
||||||
|
{GREETINGS[greetingIndex]}
|
||||||
|
</span>{' '}
|
||||||
|
<span className="inline-block align-middle">👋</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center w-full bg-red-500/0">
|
||||||
|
{isInitialized ? (
|
||||||
|
<Button
|
||||||
|
className="w-full max-w-md py-8 text-large"
|
||||||
|
variant="shadow"
|
||||||
|
color="primary"
|
||||||
|
size="lg"
|
||||||
|
onPress={() => {}}
|
||||||
|
>
|
||||||
|
START
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<p className="uppercase text-small animate-pulse">Rclone is initalizing</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+12
-1
@@ -8,7 +8,18 @@ export default {
|
|||||||
'./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}',
|
'./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}',
|
||||||
],
|
],
|
||||||
theme: {
|
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()],
|
plugins: [heroui()],
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user