+1
-2
@@ -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: '',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ const SUPPORTED_BACKENDS = [
|
||||
'box',
|
||||
'webdav',
|
||||
'onedrive',
|
||||
'http',
|
||||
]
|
||||
|
||||
function getAuthHeader() {
|
||||
|
||||
+8
-1
@@ -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))
|
||||
|
||||
@@ -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<State>()(
|
||||
set((state) => ({ remotes: [...state.remotes, remote] })),
|
||||
removeRemote: (remote: string) =>
|
||||
set((state) => ({ remotes: state.remotes.filter((r) => r !== remote) })),
|
||||
|
||||
startupStatus: null,
|
||||
}),
|
||||
{ name: 'shared-store' }
|
||||
)
|
||||
|
||||
+47
-6
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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",
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
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]]
|
||||
name = "app"
|
||||
version = "1.9.3"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"fix-path-env",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "1.9.3"
|
||||
version = "2.0.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
|
||||
@@ -13,9 +13,8 @@
|
||||
"Jobs",
|
||||
"Cron",
|
||||
"Browse",
|
||||
"Test",
|
||||
"Test2",
|
||||
"Test3"
|
||||
"Startup",
|
||||
"Test"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: <Home />,
|
||||
},
|
||||
{
|
||||
path: '/startup',
|
||||
element: <Startup />,
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
element: <Settings />,
|
||||
|
||||
+216
-178
@@ -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() {
|
||||
<div className="flex flex-col gap-8">
|
||||
<BaseHeader title="License" />
|
||||
|
||||
<div className="flex flex-row justify-center w-full gap-8 px-8">
|
||||
<div className="flex flex-col items-end w-2/6 gap-2 bg-transparent-500">
|
||||
<h3 className="font-medium">Activate</h3>
|
||||
</div>
|
||||
<div className="flex flex-row justify-center w-full gap-2 px-8">
|
||||
<Input
|
||||
placeholder="Enter license key"
|
||||
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">
|
||||
<Input
|
||||
placeholder="Enter license key"
|
||||
value={licenseKeyInput}
|
||||
onChange={(e) => setLicenseKeyInput(e.target.value)}
|
||||
{!licenseValid && (
|
||||
<Button
|
||||
isLoading={isActivating}
|
||||
size="lg"
|
||||
isDisabled={!isLicenseEditable || isActivating}
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
endContent={
|
||||
licenseValid && <CheckIcon className="w-5 h-5 text-green-500" />
|
||||
}
|
||||
onPress={async () => {
|
||||
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 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"
|
||||
/>
|
||||
>
|
||||
Activate
|
||||
</Button>
|
||||
)}
|
||||
{licenseValid && (
|
||||
<Button
|
||||
isLoading={isRevoking}
|
||||
color="danger"
|
||||
variant="ghost"
|
||||
onPress={async () => {
|
||||
// usePersistedStore.setState({
|
||||
// licenseKey: undefined,
|
||||
// licenseValid: false,
|
||||
// })
|
||||
// return
|
||||
|
||||
{!licenseValid && (
|
||||
<Button
|
||||
fullWidth={true}
|
||||
isLoading={isActivating}
|
||||
onPress={async () => {
|
||||
if (!licenseKeyInput) {
|
||||
await message('Please enter a license key', {
|
||||
title: 'Error',
|
||||
kind: 'error',
|
||||
})
|
||||
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',
|
||||
}
|
||||
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',
|
||||
kind: 'error',
|
||||
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
|
||||
}
|
||||
|
||||
setIsRevoking(true)
|
||||
try {
|
||||
await revokeMachineLicense(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.', {
|
||||
title: 'Error',
|
||||
kind: 'error',
|
||||
okLabel: 'Ok',
|
||||
cancelLabel: '',
|
||||
})
|
||||
}
|
||||
setIsRevoking(false)
|
||||
|
||||
await message('Your license has been successfully deactivated.', {
|
||||
title: 'License deactivated',
|
||||
kind: 'info',
|
||||
await message('An error occurred. Please try again.', {
|
||||
title: 'Error',
|
||||
kind: 'error',
|
||||
okLabel: 'Ok',
|
||||
})
|
||||
}}
|
||||
data-focus-visible="false"
|
||||
>
|
||||
Deactivate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
setIsRevoking(false)
|
||||
|
||||
await message('Your license has been successfully deactivated.', {
|
||||
title: 'License deactivated',
|
||||
kind: 'info',
|
||||
})
|
||||
}}
|
||||
data-focus-visible="false"
|
||||
>
|
||||
Deactivate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'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={
|
||||
<Button
|
||||
onPress={async () => {
|
||||
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() {
|
||||
/>
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
{remotes.map((remote) => (
|
||||
<Card
|
||||
<RemoteCard
|
||||
key={remote}
|
||||
shadow="sm"
|
||||
isBlurred={true}
|
||||
className="border-none bg-background/60 dark:bg-default-100/60"
|
||||
>
|
||||
<CardBody>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{remote}</span>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Button
|
||||
onPress={() => {
|
||||
setPickedRemote(remote)
|
||||
setDefaultsDrawerOpen(true)
|
||||
}}
|
||||
// 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' }
|
||||
)
|
||||
remote={remote}
|
||||
onDefaultsPress={() => {
|
||||
setPickedRemote(remote)
|
||||
setDefaultsDrawerOpen(true)
|
||||
}}
|
||||
onConfigPress={() => {
|
||||
setPickedRemote(remote)
|
||||
setEditingDrawerOpen(true)
|
||||
}}
|
||||
onDeletePress={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) {
|
||||
return
|
||||
}
|
||||
if (!confirmation) {
|
||||
return
|
||||
}
|
||||
|
||||
await deleteRemote(remote)
|
||||
removeRemote(remote)
|
||||
await triggerTrayRebuild()
|
||||
}}
|
||||
data-focus-visible="false"
|
||||
>
|
||||
<Trash2Icon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
await deleteRemote(remote)
|
||||
removeRemote(remote)
|
||||
await triggerTrayRebuild()
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</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() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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}',
|
||||
],
|
||||
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()],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user