Revert "Fix Linux first-launch startup feedback when provisioning bundled rclone"

This reverts commit 3201b0306c.
This commit is contained in:
FTCHD
2026-05-10 21:52:01 +03:00
parent 3201b0306c
commit d749844073
7 changed files with 86 additions and 416 deletions
+12 -277
View File
@@ -25,198 +25,6 @@ import {
shouldUpdateRclone, shouldUpdateRclone,
} from './common' } from './common'
function setStartupMessage(startupMessage: string | null) {
useStore.setState({ startupMessage })
}
function resetStartupDownloadState() {
useStore.setState({
startupIsDownloading: false,
startupDownloadedBytes: 0,
startupTotalBytes: null,
startupDownloadSpeed: null,
})
}
function setStartupDownloadState({
downloadedBytes,
totalBytes,
speedBytesPerSecond,
}: {
downloadedBytes: number
totalBytes: number | null
speedBytesPerSecond: number | null
}) {
useStore.setState({
startupIsDownloading: true,
startupDownloadedBytes: downloadedBytes,
startupTotalBytes: totalBytes,
startupDownloadSpeed: speedBytesPerSecond,
})
}
async function withTimeout<T>(operation: Promise<T>, timeoutMs: number, errorMessage: string) {
let timeoutId: ReturnType<typeof setTimeout> | undefined
const timeout = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(errorMessage))
}, timeoutMs)
})
try {
return await Promise.race([operation, timeout])
} finally {
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}
async function fetchTextWithTimeout(url: string, timeoutMs: number) {
const response = await withTimeout(
fetch(url),
timeoutMs,
`Request timed out after ${Math.round(timeoutMs / 1000)} seconds.`
)
if (!response.ok) {
throw new Error(`Request failed (${response.status}).`)
}
return await withTimeout(
response.text(),
timeoutMs,
`Reading response timed out after ${Math.round(timeoutMs / 1000)} seconds.`
)
}
async function downloadArrayBufferWithProgress(
url: string,
{
connectTimeoutMs = 30_000,
stallTimeoutMs = 45_000,
onProgress,
}: {
connectTimeoutMs?: number
stallTimeoutMs?: number
onProgress?: (progress: {
downloadedBytes: number
totalBytes: number | null
speedBytesPerSecond: number | null
}) => void
} = {}
) {
const controller = new AbortController()
let stalled = false
let stallTimeoutId: ReturnType<typeof setTimeout> | undefined
const resetStallTimeout = () => {
if (stallTimeoutId) {
clearTimeout(stallTimeoutId)
}
stallTimeoutId = setTimeout(() => {
stalled = true
controller.abort()
}, stallTimeoutMs)
}
const response = await fetch(url, {
connectTimeout: connectTimeoutMs,
signal: controller.signal,
})
if (!response.ok) {
throw new Error(`Failed to download rclone (${response.status}).`)
}
const totalHeader = response.headers.get('content-length')
const parsedTotal = totalHeader ? Number.parseInt(totalHeader, 10) : Number.NaN
const totalBytes = Number.isFinite(parsedTotal) ? parsedTotal : null
onProgress?.({
downloadedBytes: 0,
totalBytes,
speedBytesPerSecond: null,
})
if (!response.body) {
const buffer = await response.arrayBuffer()
onProgress?.({
downloadedBytes: buffer.byteLength,
totalBytes: totalBytes ?? buffer.byteLength,
speedBytesPerSecond: null,
})
return buffer
}
const reader = response.body.getReader()
const chunks: Uint8Array[] = totalBytes === null ? [] : []
const preallocated = totalBytes !== null ? new Uint8Array(totalBytes) : null
const startedAt = Date.now()
let downloadedBytes = 0
resetStallTimeout()
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
if (!value) {
continue
}
resetStallTimeout()
if (preallocated) {
preallocated.set(value, downloadedBytes)
} else {
chunks.push(value)
}
downloadedBytes += value.byteLength
const elapsedSeconds = Math.max((Date.now() - startedAt) / 1000, 0.001)
onProgress?.({
downloadedBytes,
totalBytes,
speedBytesPerSecond: downloadedBytes / elapsedSeconds,
})
}
} catch (error) {
if (controller.signal.aborted && stalled) {
throw new Error(
`Downloading rclone stalled for ${Math.round(stallTimeoutMs / 1000)} seconds.`
)
}
throw error
} finally {
if (stallTimeoutId) {
clearTimeout(stallTimeoutId)
}
reader.releaseLock()
}
if (preallocated) {
return preallocated.buffer
}
const merged = new Uint8Array(downloadedBytes)
let offset = 0
for (const chunk of chunks) {
merged.set(chunk, offset)
offset += chunk.byteLength
}
return merged.buffer
}
export async function initRclone(args: string[]) { export async function initRclone(args: string[]) {
console.log('[initRclone] starting with args:', args) console.log('[initRclone] starting with args:', args)
@@ -228,30 +36,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) {
console.log('[initRclone] no rclone installation found, provisioning...') console.log('[initRclone] no rclone installation found, provisioning...')
resetStartupDownloadState() useStore.setState({ startupDisplayed: true, startupStatus: 'initializing' })
useStore.setState({
startupDisplayed: true,
startupStatus: 'initializing',
startupMessage: 'Preparing rclone for first launch',
})
await openSmallWindow({ await openSmallWindow({
name: 'Startup', name: 'Startup',
url: '/startup', url: '/startup',
transparent: platform() !== 'linux',
})
const success = await provisionRclone().catch((error) => {
console.error(
'[initRclone] provision failed with error',
error instanceof Error ? error.message : error
)
Sentry.captureException(error)
setStartupMessage(
error instanceof Error
? error.message
: 'Failed to prepare rclone. Please try again later.'
)
return false
}) })
const success = await provisionRclone()
console.log('[initRclone] provision rclone result:', success) console.log('[initRclone] provision rclone result:', success)
if (!success) { if (!success) {
console.error('[initRclone] provision failed, setting fatal status') console.error('[initRclone] provision failed, setting fatal status')
@@ -260,8 +50,7 @@ export async function initRclone(args: string[]) {
} }
console.log('[initRclone] provision succeeded') console.log('[initRclone] provision succeeded')
resetStartupDownloadState() useStore.setState({ startupStatus: 'initialized' })
useStore.setState({ startupStatus: 'initialized', startupMessage: null })
if (!['windows', 'macos'].includes(platform())) { if (!['windows', 'macos'].includes(platform())) {
usePersistedStore.setState({ hideStartup: true }) usePersistedStore.setState({ hideStartup: true })
@@ -276,16 +65,11 @@ export async function initRclone(args: string[]) {
if (shouldUpdateRclone(rcloneVersion)) { if (shouldUpdateRclone(rcloneVersion)) {
console.log('[initRclone] needs update') console.log('[initRclone] needs update')
resetStartupDownloadState() useStore.setState({ startupStatus: 'updating' })
useStore.setState({
startupStatus: 'updating',
startupMessage: 'Updating rclone',
})
await openSmallWindow({ await openSmallWindow({
name: 'Startup', name: 'Startup',
url: '/startup', url: '/startup',
transparent: platform() !== 'linux',
}) })
try { try {
@@ -298,10 +82,7 @@ export async function initRclone(args: string[]) {
'[initRclone] system rclone update failed or was cancelled by user, code:', '[initRclone] system rclone update failed or was cancelled by user, code:',
code code
) )
useStore.setState({ useStore.setState({ startupStatus: 'error' })
startupStatus: 'error',
startupMessage: 'Could not update the system rclone installation.',
})
const skipping = await ask( const skipping = await ask(
'You are running an outdated version of the CLI that could not be updated.\n\nPlease update manually and restart Rclone UI.', 'You are running an outdated version of the CLI that could not be updated.\n\nPlease update manually and restart Rclone UI.',
{ {
@@ -318,13 +99,11 @@ export async function initRclone(args: string[]) {
} }
} else { } else {
console.log('[initRclone] system rclone updated successfully') console.log('[initRclone] system rclone updated successfully')
resetStartupDownloadState() useStore.setState({ startupStatus: 'updated' })
useStore.setState({ startupStatus: 'updated', startupMessage: null })
} }
} }
if (internal) { if (internal) {
console.log('[initRclone] updating internal rclone') console.log('[initRclone] updating internal rclone')
setStartupMessage('Updating the bundled rclone binary')
const instance = Command.create('rclone-internal', ['selfupdate']) const instance = Command.create('rclone-internal', ['selfupdate'])
const updateResult = await instance.execute() const updateResult = await instance.execute()
console.log('[initRclone] updateResult', JSON.stringify(updateResult, null, 2)) console.log('[initRclone] updateResult', JSON.stringify(updateResult, null, 2))
@@ -333,23 +112,15 @@ export async function initRclone(args: string[]) {
'[initRclone] internal rclone update failed, code:', '[initRclone] internal rclone update failed, code:',
updateResult.code updateResult.code
) )
useStore.setState({ useStore.setState({ startupStatus: 'error' })
startupStatus: 'error',
startupMessage: 'Could not update the bundled rclone binary.',
})
} else { } else {
console.log('[initRclone] internal rclone updated successfully') console.log('[initRclone] internal rclone updated successfully')
resetStartupDownloadState() useStore.setState({ startupStatus: 'updated' })
useStore.setState({ startupStatus: 'updated', startupMessage: null })
} }
} }
} catch (error) { } catch (error) {
console.error('[initRclone] failed to update rclone', error) console.error('[initRclone] failed to update rclone', error)
useStore.setState({ useStore.setState({ startupStatus: 'error' })
startupStatus: 'error',
startupMessage:
error instanceof Error ? error.message : 'Failed to update rclone.',
})
} }
await new Promise((resolve) => setTimeout(resolve, 1000)) await new Promise((resolve) => setTimeout(resolve, 1000))
@@ -614,13 +385,10 @@ export async function initRclone(args: string[]) {
*/ */
export async function provisionRclone() { export async function provisionRclone() {
console.log('[provisionRclone] starting provisioning process') console.log('[provisionRclone] starting provisioning process')
resetStartupDownloadState()
setStartupMessage('Checking the latest rclone version')
console.log('[provisionRclone] fetching latest version info') console.log('[provisionRclone] fetching latest version info')
const currentVersionString = await fetchTextWithTimeout( const currentVersionString = await fetch('https://downloads.rclone.org/version.txt').then(
'https://downloads.rclone.org/version.txt', (res) => res.text()
30_000
) )
console.log('[provisionRclone] currentVersionString', currentVersionString) console.log('[provisionRclone] currentVersionString', currentVersionString)
@@ -632,7 +400,6 @@ export async function provisionRclone() {
return false return false
} }
console.log('[provisionRclone] currentVersion', currentVersion) console.log('[provisionRclone] currentVersion', currentVersion)
setStartupMessage(`Preparing rclone ${currentVersion}`)
const currentPlatform = platform() const currentPlatform = platform()
console.log('[provisionRclone] currentPlatform', currentPlatform) console.log('[provisionRclone] currentPlatform', currentPlatform)
@@ -648,7 +415,6 @@ export async function provisionRclone() {
console.log('[provisionRclone] tempDirPath', tempDirPath) console.log('[provisionRclone] tempDirPath', tempDirPath)
console.log('[provisionRclone] detecting system architecture') console.log('[provisionRclone] detecting system architecture')
setStartupMessage('Detecting system architecture')
const arch = (await invoke('get_arch')) as 'arm64' | 'amd64' | '386' | 'unknown' const arch = (await invoke('get_arch')) as 'arm64' | 'amd64' | '386' | 'unknown'
console.log('[provisionRclone] arch', arch) console.log('[provisionRclone] arch', arch)
@@ -662,34 +428,10 @@ export async function provisionRclone() {
console.log('[provisionRclone] downloadUrl', downloadUrl) console.log('[provisionRclone] downloadUrl', downloadUrl)
console.log('[provisionRclone] downloading rclone binary') console.log('[provisionRclone] downloading rclone binary')
setStartupMessage(`Downloading rclone ${currentVersion}`) const downloadedFile = await fetch(downloadUrl).then((res) => res.arrayBuffer())
let lastProgressUpdateAt = 0
const downloadedFile = await downloadArrayBufferWithProgress(downloadUrl, {
connectTimeoutMs: 30_000,
stallTimeoutMs: 45_000,
onProgress: ({ downloadedBytes, totalBytes, speedBytesPerSecond }) => {
const now = Date.now()
const isComplete = totalBytes !== null && downloadedBytes >= totalBytes
if (!isComplete && now - lastProgressUpdateAt < 250) {
return
}
lastProgressUpdateAt = now
setStartupDownloadState({
downloadedBytes,
totalBytes,
speedBytesPerSecond,
})
},
})
console.log('[provisionRclone] download complete, size:', downloadedFile.byteLength) console.log('[provisionRclone] download complete, size:', downloadedFile.byteLength)
setStartupMessage('Download complete')
console.log('[provisionRclone] checking if temp rclone directory exists') console.log('[provisionRclone] checking if temp rclone directory exists')
resetStartupDownloadState()
setStartupMessage('Preparing temporary files')
let tempDirExists = false let tempDirExists = false
try { try {
tempDirExists = await exists('rclone', { tempDirExists = await exists('rclone', {
@@ -738,7 +480,6 @@ export async function provisionRclone() {
console.log('[provisionRclone] zipPath', zipPath) console.log('[provisionRclone] zipPath', zipPath)
console.log('[provisionRclone] writing zip file to disk') console.log('[provisionRclone] writing zip file to disk')
setStartupMessage('Saving rclone archive')
try { try {
await writeFile(zipPath, new Uint8Array(downloadedFile)) await writeFile(zipPath, new Uint8Array(downloadedFile))
console.log('[provisionRclone] wrote zip file successfully') console.log('[provisionRclone] wrote zip file successfully')
@@ -751,7 +492,6 @@ export async function provisionRclone() {
const extractPath = `${tempDirPath}${sep()}rclone${sep()}extracted` const extractPath = `${tempDirPath}${sep()}rclone${sep()}extracted`
console.log('[provisionRclone] extracting zip file to:', extractPath) console.log('[provisionRclone] extracting zip file to:', extractPath)
setStartupMessage('Extracting rclone')
try { try {
await invoke('unzip_file', { await invoke('unzip_file', {
zipPath, zipPath,
@@ -780,7 +520,6 @@ export async function provisionRclone() {
console.log('[provisionRclone] rcloneBinaryPath', rcloneBinaryPath) console.log('[provisionRclone] rcloneBinaryPath', rcloneBinaryPath)
console.log('[provisionRclone] verifying extracted binary exists') console.log('[provisionRclone] verifying extracted binary exists')
setStartupMessage('Verifying extracted rclone')
try { try {
const binaryExists = await exists(rcloneBinaryPath) const binaryExists = await exists(rcloneBinaryPath)
console.log('[provisionRclone] rcloneBinaryPathExists', binaryExists) console.log('[provisionRclone] rcloneBinaryPathExists', binaryExists)
@@ -796,7 +535,6 @@ export async function provisionRclone() {
} }
console.log('[provisionRclone] getting app local data directory') console.log('[provisionRclone] getting app local data directory')
setStartupMessage('Preparing application data folder')
const appLocalDataDirPath = await appLocalDataDir() const appLocalDataDirPath = await appLocalDataDir()
console.log('[provisionRclone] appLocalDataDirPath', appLocalDataDirPath) console.log('[provisionRclone] appLocalDataDirPath', appLocalDataDirPath)
@@ -816,7 +554,6 @@ export async function provisionRclone() {
console.log('[provisionRclone] targetBinaryPath', targetBinaryPath) console.log('[provisionRclone] targetBinaryPath', targetBinaryPath)
console.log('[provisionRclone] copying binary to final location') console.log('[provisionRclone] copying binary to final location')
setStartupMessage('Installing rclone')
const maxCopyRetries = 3 const maxCopyRetries = 3
for (let attempt = 1; attempt <= maxCopyRetries; attempt++) { for (let attempt = 1; attempt <= maxCopyRetries; attempt++) {
console.log(`[provisionRclone] copy attempt ${attempt}/${maxCopyRetries}`) console.log(`[provisionRclone] copy attempt ${attempt}/${maxCopyRetries}`)
@@ -851,7 +588,6 @@ export async function provisionRclone() {
} }
console.log('[provisionRclone] verifying installation') console.log('[provisionRclone] verifying installation')
setStartupMessage('Verifying rclone installation')
const hasInstalled = await isInternalRcloneInstalled() const hasInstalled = await isInternalRcloneInstalled()
console.log('[provisionRclone] installation verified:', hasInstalled) console.log('[provisionRclone] installation verified:', hasInstalled)
@@ -861,7 +597,6 @@ export async function provisionRclone() {
} }
console.log('[provisionRclone] rclone has been installed successfully') console.log('[provisionRclone] rclone has been installed successfully')
setStartupMessage('Rclone is ready')
return true return true
} }
+1 -3
View File
@@ -35,14 +35,12 @@ export async function openWindow({
export async function openSmallWindow({ export async function openSmallWindow({
name, name,
url, url,
transparent = true,
}: { }: {
name: string name: string
url: string url: string
transparent?: boolean
}) { }) {
console.log('[openSmallWindow] ', name, url) console.log('[openSmallWindow] ', name, url)
await invoke('open_small_window', { name, url, transparent }) await invoke('open_small_window', { name, url })
return WebviewWindow.getByLabel(name) return WebviewWindow.getByLabel(name)
} }
+9 -17
View File
@@ -395,7 +395,7 @@ async function registerRcloneWindowListeners() {
rcloneListenersRegistered = true rcloneListenersRegistered = true
} }
async function startRclone(): Promise<boolean> { async function startRclone() {
console.log('[startRclone]') console.log('[startRclone]')
await registerRcloneWindowListeners() await registerRcloneWindowListeners()
@@ -430,8 +430,7 @@ async function startRclone(): Promise<boolean> {
okLabel: 'Exit', okLabel: 'Exit',
} }
) )
await exit(0) return await exit(0)
return false
} }
const command = rclone?.system || rclone?.internal const command = rclone?.system || rclone?.internal
@@ -439,7 +438,7 @@ async function startRclone(): Promise<boolean> {
if (!command) { if (!command) {
console.error('[startRclone] initRclone returned without a runnable command') console.error('[startRclone] initRclone returned without a runnable command')
Sentry.captureException(new Error('initRclone returned without a runnable command.')) Sentry.captureException(new Error('initRclone returned without a runnable command.'))
return false return
} }
command.addListener('close', async (event) => { command.addListener('close', async (event) => {
@@ -477,7 +476,6 @@ async function startRclone(): Promise<boolean> {
console.log('[startRclone] running rclone') console.log('[startRclone] running rclone')
await new Promise((resolve) => setTimeout(resolve, 500)) await new Promise((resolve) => setTimeout(resolve, 500))
return true
} }
async function startupMounts() { async function startupMounts() {
@@ -1120,16 +1118,10 @@ waitForHydration()
.then(() => validateInstance()) .then(() => validateInstance())
.then(() => checkAlreadyRunning()) .then(() => checkAlreadyRunning())
.then(() => startRclone()) .then(() => startRclone())
.then(async (started) => { .then(() => checkRclone())
if (!started) { .then(() => handleDeepLink())
console.log('[main] rclone did not start, leaving startup window visible') .then(() => showStartup())
return .then(() => startupMounts())
} .then(() => resumeTasks())
await checkRclone() .then(() => initTray())
await handleDeepLink()
await showStartup()
await startupMounts()
await resumeTasks()
await initTray()
})
.catch(console.error) .catch(console.error)
+3 -10
View File
@@ -213,7 +213,6 @@ pub async fn open_small_window(
app_handle: AppHandle, app_handle: AppHandle,
name: String, name: String,
url: String, url: String,
transparent: Option<bool>,
) -> Result<(), String> { ) -> Result<(), String> {
if let Some(existing) = app_handle.get_webview_window(&name) { if let Some(existing) = app_handle.get_webview_window(&name) {
existing.set_focus().map_err(|e| e.to_string())?; existing.set_focus().map_err(|e| e.to_string())?;
@@ -235,16 +234,12 @@ pub async fn open_small_window(
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
if transparent.unwrap_or(true) { builder = builder.transparent(true);
builder = builder.transparent(true);
}
} }
let window = builder.build().map_err(|e| e.to_string())?; let window = builder.build().map_err(|e| e.to_string())?;
if let Err(err) = window.center() { window.center().map_err(|e| e.to_string())?;
log::warn!("failed to center small window before show: {}", err);
}
window.set_zoom(1.0).map_err(|e| e.to_string())?; window.set_zoom(1.0).map_err(|e| e.to_string())?;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -260,9 +255,7 @@ pub async fn open_small_window(
window.set_always_on_top(true).map_err(|e| e.to_string())?; window.set_always_on_top(true).map_err(|e| e.to_string())?;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if let Err(err) = window.center() { window.center().map_err(|e| e.to_string())?;
log::warn!("failed to center small window after show: {}", err);
}
if os != "windows" && os != "macos" { if os != "windows" && os != "macos" {
window.set_resizable(true).map_err(|e| e.to_string())?; window.set_resizable(true).map_err(|e| e.to_string())?;
+3 -3
View File
@@ -14,15 +14,15 @@
"windows": [ "windows": [
{ {
"title": "Rclone UI", "title": "Rclone UI",
"width": 1, "width": 0,
"height": 1, "height": 0,
"center": true, "center": true,
"hiddenTitle": false, "hiddenTitle": false,
"resizable": false, "resizable": false,
"fullscreen": false, "fullscreen": false,
"visible": false, "visible": false,
"decorations": false, "decorations": false,
"focus": false, "focus": true,
"url": "tray.html", "url": "tray.html",
"alwaysOnTop": true, "alwaysOnTop": true,
"backgroundThrottling": "disabled", "backgroundThrottling": "disabled",
+58 -96
View File
@@ -1,11 +1,10 @@
import { Button, Divider, Progress } from '@heroui/react' import { Button, Divider } from '@heroui/react'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window' import { getCurrentWindow } from '@tauri-apps/api/window'
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 { AnimatePresence, motion } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { formatBytes } from '../../lib/format'
import { openSmallWindow } from '../../lib/window' import { openSmallWindow } from '../../lib/window'
import { useStore } from '../../store/memory' import { useStore } from '../../store/memory'
import { usePersistedStore } from '../../store/persisted' import { usePersistedStore } from '../../store/persisted'
@@ -44,11 +43,6 @@ export default function Startup() {
const [titleIndex, setTitleIndex] = useState(0) const [titleIndex, setTitleIndex] = useState(0)
const startupStatus = useStore((state) => state.startupStatus) const startupStatus = useStore((state) => state.startupStatus)
const startupMessage = useStore((state) => state.startupMessage)
const startupIsDownloading = useStore((state) => state.startupIsDownloading)
const startupDownloadedBytes = useStore((state) => state.startupDownloadedBytes)
const startupTotalBytes = useStore((state) => state.startupTotalBytes)
const startupDownloadSpeed = useStore((state) => state.startupDownloadSpeed)
const toolbarShortcut = usePersistedStore((state) => state.toolbarShortcut) const toolbarShortcut = usePersistedStore((state) => state.toolbarShortcut)
const shortcutDisplay = useMemo(() => { const shortcutDisplay = useMemo(() => {
@@ -71,37 +65,6 @@ export default function Startup() {
() => startupStatus === 'error' || startupStatus === 'fatal', () => startupStatus === 'error' || startupStatus === 'fatal',
[startupStatus] [startupStatus]
) )
const isBusy = useMemo(
() => startupStatus === 'initializing' || startupStatus === 'updating',
[startupStatus]
)
const downloadProgress = useMemo(() => {
if (!startupTotalBytes || startupTotalBytes <= 0) {
return null
}
return Math.min((startupDownloadedBytes / startupTotalBytes) * 100, 100)
}, [startupDownloadedBytes, startupTotalBytes])
const downloadProgressLabel = useMemo(() => {
if (!startupIsDownloading) {
return null
}
const downloaded = formatBytes(startupDownloadedBytes)
const total = startupTotalBytes ? formatBytes(startupTotalBytes) : 'Unknown size'
const speed = startupDownloadSpeed ? `${formatBytes(startupDownloadSpeed)}/s` : null
if (speed) {
return `${downloaded} / ${total}${speed}`
}
return `${downloaded} / ${total}`
}, [
startupDownloadSpeed,
startupDownloadedBytes,
startupIsDownloading,
startupTotalBytes,
])
useEffect(() => { useEffect(() => {
let intervalId: NodeJS.Timeout | null = null let intervalId: NodeJS.Timeout | null = null
@@ -121,16 +84,11 @@ export default function Startup() {
} }
}, [startupStatus]) }, [startupStatus])
// Linux can emit focus changes while the startup window is still provisioning rclone. // Close window when it loses focus
useEffect(() => { useEffect(() => {
const currentWindow = getCurrentWindow() const currentWindow = getCurrentWindow()
const unlisten = currentWindow.onFocusChanged(async ({ payload: focused }) => { const unlisten = currentWindow.onFocusChanged(async ({ payload: focused }) => {
if (!focused) { if (!focused) {
const status = useStore.getState().startupStatus
const busy = status === 'initializing' || status === 'updating'
if (platform() === 'linux' && busy) {
return
}
await currentWindow.hide() await currentWindow.hide()
await currentWindow.destroy() await currentWindow.destroy()
} }
@@ -158,8 +116,7 @@ export default function Startup() {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="ml-2 text-2xl" className="ml-2 text-2xl"
> >
{startupMessage || Could not complete the operation, please try again later.
'Could not complete the operation, please try again later.'}
</motion.p> </motion.p>
)} )}
{startupStatus === 'initialized' && ( {startupStatus === 'initialized' && (
@@ -184,40 +141,39 @@ export default function Startup() {
Rclone has just been updated, thanks for waiting! Rclone has just been updated, thanks for waiting!
</motion.p> </motion.p>
)} )}
{isBusy && ( {startupStatus === 'initializing' && (
<motion.div <motion.p
key="busy" key="initializing"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="flex flex-col items-center gap-4 px-8" className="ml-2 text-3xl"
> >
<p className="ml-2 text-3xl"> <span
<span key={titleIndex}
key={titleIndex} className="inline-block align-middle animate-fade-in-up"
className="inline-block align-middle animate-fade-in-up" >
> {GREET[titleIndex]}
{startupStatus === 'updating' </span>{' '}
? WAIT[titleIndex % WAIT.length] <span className="inline-block align-middle">👋</span>
: GREET[titleIndex % GREET.length]} </motion.p>
</span>{' '} )}
<span className="inline-block align-middle">👋</span> {startupStatus === 'updating' && (
</p> <motion.p
key="updating"
{startupIsDownloading && ( initial={{ opacity: 0 }}
<div className="w-full max-w-md space-y-2"> animate={{ opacity: 1 }}
<Progress exit={{ opacity: 0 }}
value={downloadProgress ?? undefined} className="ml-2 text-3xl"
isIndeterminate={downloadProgress === null} >
aria-label="Rclone download progress" <span
className="w-full" key={titleIndex}
/> className="inline-block align-middle animate-fade-in-up"
<p className="text-sm text-center text-default-500"> >
{downloadProgressLabel} {WAIT[titleIndex]}
</p> </span>{' '}
</div> <span className="inline-block align-middle">👋</span>
)} </motion.p>
</motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
@@ -295,13 +251,13 @@ export default function Startup() {
</Button> </Button>
</motion.div> </motion.div>
)} )}
{isBusy && ( {startupStatus === 'initializing' && (
<motion.div <motion.p
key="busy" key="initializing"
initial={{ opacity: 0, scale: 0.95 }} initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }} exit={{ opacity: 0, scale: 0.95 }}
className="flex flex-col items-center w-full gap-4 px-8 text-center uppercase text-small" className="uppercase text-small"
> >
<motion.span <motion.span
animate={{ opacity: [1, 0.5, 1] }} animate={{ opacity: [1, 0.5, 1] }}
@@ -311,23 +267,29 @@ export default function Startup() {
ease: 'easeInOut', ease: 'easeInOut',
}} }}
> >
{startupMessage || Rclone is initializing
(startupStatus === 'updating'
? 'Rclone is updating'
: 'Rclone is initializing')}
</motion.span> </motion.span>
{startupIsDownloading && ( </motion.p>
<Button )}
variant="flat" {startupStatus === 'updating' && (
color="default" <motion.p
onPress={async () => { key="updating"
await exit(0) initial={{ opacity: 0, scale: 0.95 }}
}} animate={{ opacity: 1, scale: 1 }}
> exit={{ opacity: 0, scale: 0.95 }}
Quit for now className="uppercase text-small"
</Button> >
)} <motion.span
</motion.div> animate={{ opacity: [1, 0.5, 1] }}
transition={{
repeat: Number.POSITIVE_INFINITY,
duration: 4,
ease: 'easeInOut',
}}
>
Rclone is updating
</motion.span>
</motion.p>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
-10
View File
@@ -14,11 +14,6 @@ interface State {
| 'fatal' | 'fatal'
startupDisplayed: boolean startupDisplayed: boolean
startupMessage: string | null
startupIsDownloading: boolean
startupDownloadedBytes: number
startupTotalBytes: number | null
startupDownloadSpeed: number | null
isRestartingRclone: boolean isRestartingRclone: boolean
@@ -42,11 +37,6 @@ export const useStore = create<State>()(
startupStatus: null, startupStatus: null,
startupDisplayed: false, startupDisplayed: false,
startupMessage: null,
startupIsDownloading: false,
startupDownloadedBytes: 0,
startupTotalBytes: null,
startupDownloadSpeed: null,
isRestartingRclone: false, isRestartingRclone: false,