Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com>
This commit is contained in:
FTCHD
2025-09-15 11:00:17 +02:00
parent 81b0b3da79
commit 4d3dcf1632
22 changed files with 391 additions and 209 deletions
+1 -2
View File
@@ -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: '',
}) })
} }
}, },
+1
View File
@@ -19,6 +19,7 @@ const SUPPORTED_BACKENDS = [
'box', 'box',
'webdav', 'webdav',
'onedrive', 'onedrive',
'http',
] ]
function getAuthHeader() { function getAuthHeader() {
+8 -1
View File
@@ -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))
+4
View File
@@ -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
View File
@@ -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
+7 -8
View File
@@ -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 })
} }
+4 -4
View File
@@ -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
View File
@@ -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",
BIN
View File
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

+1 -1
View File
@@ -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 -1
View File
@@ -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 = ""
+2 -3
View File
@@ -13,9 +13,8 @@
"Jobs", "Jobs",
"Cron", "Cron",
"Browse", "Browse",
"Test", "Startup",
"Test2", "Test"
"Test3"
], ],
"permissions": [ "permissions": [
"core:default", "core:default",
+2 -2
View File
@@ -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",
+5
View File
@@ -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
View File
@@ -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
} }
+78
View File
@@ -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
View File
@@ -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()],
} }