From d7498440739acf3a363f55d230a5793dc0f2d2fe Mon Sep 17 00:00:00 2001 From: FTCHD <144691102+FTCHD@users.noreply.github.com> Date: Sun, 10 May 2026 21:51:38 +0300 Subject: [PATCH] Revert "Fix Linux first-launch startup feedback when provisioning bundled rclone" This reverts commit 3201b0306c1b27434557a6f823d6d3a3654be695. --- lib/rclone/init.ts | 289 ++----------------------------------- lib/window.ts | 4 +- main.ts | 26 ++-- src-tauri/common/window.rs | 13 +- src-tauri/tauri.conf.json | 6 +- src/pages/Startup.tsx | 154 ++++++++------------ store/memory.ts | 10 -- 7 files changed, 86 insertions(+), 416 deletions(-) diff --git a/lib/rclone/init.ts b/lib/rclone/init.ts index 9fd8d24..d4b70bd 100644 --- a/lib/rclone/init.ts +++ b/lib/rclone/init.ts @@ -25,198 +25,6 @@ import { shouldUpdateRclone, } 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(operation: Promise, timeoutMs: number, errorMessage: string) { - let timeoutId: ReturnType | undefined - const timeout = new Promise((_, 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 | 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[]) { console.log('[initRclone] starting with args:', args) @@ -228,30 +36,12 @@ export async function initRclone(args: string[]) { // rclone not available, let's download it if (!system && !internal) { console.log('[initRclone] no rclone installation found, provisioning...') - resetStartupDownloadState() - useStore.setState({ - startupDisplayed: true, - startupStatus: 'initializing', - startupMessage: 'Preparing rclone for first launch', - }) + useStore.setState({ startupDisplayed: true, startupStatus: 'initializing' }) await openSmallWindow({ name: '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) if (!success) { console.error('[initRclone] provision failed, setting fatal status') @@ -260,8 +50,7 @@ export async function initRclone(args: string[]) { } console.log('[initRclone] provision succeeded') - resetStartupDownloadState() - useStore.setState({ startupStatus: 'initialized', startupMessage: null }) + useStore.setState({ startupStatus: 'initialized' }) if (!['windows', 'macos'].includes(platform())) { usePersistedStore.setState({ hideStartup: true }) @@ -276,16 +65,11 @@ export async function initRclone(args: string[]) { if (shouldUpdateRclone(rcloneVersion)) { console.log('[initRclone] needs update') - resetStartupDownloadState() - useStore.setState({ - startupStatus: 'updating', - startupMessage: 'Updating rclone', - }) + useStore.setState({ startupStatus: 'updating' }) await openSmallWindow({ name: 'Startup', url: '/startup', - transparent: platform() !== 'linux', }) try { @@ -298,10 +82,7 @@ export async function initRclone(args: string[]) { '[initRclone] system rclone update failed or was cancelled by user, code:', code ) - useStore.setState({ - startupStatus: 'error', - startupMessage: 'Could not update the system rclone installation.', - }) + useStore.setState({ startupStatus: 'error' }) 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.', { @@ -318,13 +99,11 @@ export async function initRclone(args: string[]) { } } else { console.log('[initRclone] system rclone updated successfully') - resetStartupDownloadState() - useStore.setState({ startupStatus: 'updated', startupMessage: null }) + useStore.setState({ startupStatus: 'updated' }) } } if (internal) { console.log('[initRclone] updating internal rclone') - setStartupMessage('Updating the bundled rclone binary') const instance = Command.create('rclone-internal', ['selfupdate']) const updateResult = await instance.execute() 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:', updateResult.code ) - useStore.setState({ - startupStatus: 'error', - startupMessage: 'Could not update the bundled rclone binary.', - }) + useStore.setState({ startupStatus: 'error' }) } else { console.log('[initRclone] internal rclone updated successfully') - resetStartupDownloadState() - useStore.setState({ startupStatus: 'updated', startupMessage: null }) + useStore.setState({ startupStatus: 'updated' }) } } } catch (error) { console.error('[initRclone] failed to update rclone', error) - useStore.setState({ - startupStatus: 'error', - startupMessage: - error instanceof Error ? error.message : 'Failed to update rclone.', - }) + useStore.setState({ startupStatus: 'error' }) } await new Promise((resolve) => setTimeout(resolve, 1000)) @@ -614,13 +385,10 @@ export async function initRclone(args: string[]) { */ export async function provisionRclone() { console.log('[provisionRclone] starting provisioning process') - resetStartupDownloadState() - setStartupMessage('Checking the latest rclone version') console.log('[provisionRclone] fetching latest version info') - const currentVersionString = await fetchTextWithTimeout( - 'https://downloads.rclone.org/version.txt', - 30_000 + const currentVersionString = await fetch('https://downloads.rclone.org/version.txt').then( + (res) => res.text() ) console.log('[provisionRclone] currentVersionString', currentVersionString) @@ -632,7 +400,6 @@ export async function provisionRclone() { return false } console.log('[provisionRclone] currentVersion', currentVersion) - setStartupMessage(`Preparing rclone ${currentVersion}`) const currentPlatform = platform() console.log('[provisionRclone] currentPlatform', currentPlatform) @@ -648,7 +415,6 @@ export async function provisionRclone() { console.log('[provisionRclone] tempDirPath', tempDirPath) console.log('[provisionRclone] detecting system architecture') - setStartupMessage('Detecting system architecture') const arch = (await invoke('get_arch')) as 'arm64' | 'amd64' | '386' | 'unknown' console.log('[provisionRclone] arch', arch) @@ -662,34 +428,10 @@ export async function provisionRclone() { console.log('[provisionRclone] downloadUrl', downloadUrl) console.log('[provisionRclone] downloading rclone binary') - setStartupMessage(`Downloading rclone ${currentVersion}`) - 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, - }) - }, - }) + const downloadedFile = await fetch(downloadUrl).then((res) => res.arrayBuffer()) console.log('[provisionRclone] download complete, size:', downloadedFile.byteLength) - setStartupMessage('Download complete') console.log('[provisionRclone] checking if temp rclone directory exists') - resetStartupDownloadState() - setStartupMessage('Preparing temporary files') let tempDirExists = false try { tempDirExists = await exists('rclone', { @@ -738,7 +480,6 @@ export async function provisionRclone() { console.log('[provisionRclone] zipPath', zipPath) console.log('[provisionRclone] writing zip file to disk') - setStartupMessage('Saving rclone archive') try { await writeFile(zipPath, new Uint8Array(downloadedFile)) console.log('[provisionRclone] wrote zip file successfully') @@ -751,7 +492,6 @@ export async function provisionRclone() { const extractPath = `${tempDirPath}${sep()}rclone${sep()}extracted` console.log('[provisionRclone] extracting zip file to:', extractPath) - setStartupMessage('Extracting rclone') try { await invoke('unzip_file', { zipPath, @@ -780,7 +520,6 @@ export async function provisionRclone() { console.log('[provisionRclone] rcloneBinaryPath', rcloneBinaryPath) console.log('[provisionRclone] verifying extracted binary exists') - setStartupMessage('Verifying extracted rclone') try { const binaryExists = await exists(rcloneBinaryPath) console.log('[provisionRclone] rcloneBinaryPathExists', binaryExists) @@ -796,7 +535,6 @@ export async function provisionRclone() { } console.log('[provisionRclone] getting app local data directory') - setStartupMessage('Preparing application data folder') const appLocalDataDirPath = await appLocalDataDir() console.log('[provisionRclone] appLocalDataDirPath', appLocalDataDirPath) @@ -816,7 +554,6 @@ export async function provisionRclone() { console.log('[provisionRclone] targetBinaryPath', targetBinaryPath) console.log('[provisionRclone] copying binary to final location') - setStartupMessage('Installing rclone') const maxCopyRetries = 3 for (let attempt = 1; attempt <= maxCopyRetries; attempt++) { console.log(`[provisionRclone] copy attempt ${attempt}/${maxCopyRetries}`) @@ -851,7 +588,6 @@ export async function provisionRclone() { } console.log('[provisionRclone] verifying installation') - setStartupMessage('Verifying rclone installation') const hasInstalled = await isInternalRcloneInstalled() console.log('[provisionRclone] installation verified:', hasInstalled) @@ -861,7 +597,6 @@ export async function provisionRclone() { } console.log('[provisionRclone] rclone has been installed successfully') - setStartupMessage('Rclone is ready') return true } diff --git a/lib/window.ts b/lib/window.ts index e9ea1a7..c645545 100644 --- a/lib/window.ts +++ b/lib/window.ts @@ -35,14 +35,12 @@ export async function openWindow({ export async function openSmallWindow({ name, url, - transparent = true, }: { name: string url: string - transparent?: boolean }) { console.log('[openSmallWindow] ', name, url) - await invoke('open_small_window', { name, url, transparent }) + await invoke('open_small_window', { name, url }) return WebviewWindow.getByLabel(name) } diff --git a/main.ts b/main.ts index 9b53cb8..3870b4d 100644 --- a/main.ts +++ b/main.ts @@ -395,7 +395,7 @@ async function registerRcloneWindowListeners() { rcloneListenersRegistered = true } -async function startRclone(): Promise { +async function startRclone() { console.log('[startRclone]') await registerRcloneWindowListeners() @@ -430,8 +430,7 @@ async function startRclone(): Promise { okLabel: 'Exit', } ) - await exit(0) - return false + return await exit(0) } const command = rclone?.system || rclone?.internal @@ -439,7 +438,7 @@ async function startRclone(): Promise { if (!command) { console.error('[startRclone] initRclone returned without a runnable command') Sentry.captureException(new Error('initRclone returned without a runnable command.')) - return false + return } command.addListener('close', async (event) => { @@ -477,7 +476,6 @@ async function startRclone(): Promise { console.log('[startRclone] running rclone') await new Promise((resolve) => setTimeout(resolve, 500)) - return true } async function startupMounts() { @@ -1120,16 +1118,10 @@ waitForHydration() .then(() => validateInstance()) .then(() => checkAlreadyRunning()) .then(() => startRclone()) - .then(async (started) => { - if (!started) { - console.log('[main] rclone did not start, leaving startup window visible') - return - } - await checkRclone() - await handleDeepLink() - await showStartup() - await startupMounts() - await resumeTasks() - await initTray() - }) + .then(() => checkRclone()) + .then(() => handleDeepLink()) + .then(() => showStartup()) + .then(() => startupMounts()) + .then(() => resumeTasks()) + .then(() => initTray()) .catch(console.error) diff --git a/src-tauri/common/window.rs b/src-tauri/common/window.rs index 3356099..23eb43c 100644 --- a/src-tauri/common/window.rs +++ b/src-tauri/common/window.rs @@ -213,7 +213,6 @@ pub async fn open_small_window( app_handle: AppHandle, name: String, url: String, - transparent: Option, ) -> Result<(), String> { if let Some(existing) = app_handle.get_webview_window(&name) { existing.set_focus().map_err(|e| e.to_string())?; @@ -235,16 +234,12 @@ pub async fn open_small_window( #[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())?; - if let Err(err) = window.center() { - log::warn!("failed to center small window before show: {}", err); - } + window.center().map_err(|e| e.to_string())?; window.set_zoom(1.0).map_err(|e| e.to_string())?; #[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())?; #[cfg(target_os = "linux")] - if let Err(err) = window.center() { - log::warn!("failed to center small window after show: {}", err); - } + window.center().map_err(|e| e.to_string())?; if os != "windows" && os != "macos" { window.set_resizable(true).map_err(|e| e.to_string())?; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 623dd26..0546260 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -14,15 +14,15 @@ "windows": [ { "title": "Rclone UI", - "width": 1, - "height": 1, + "width": 0, + "height": 0, "center": true, "hiddenTitle": false, "resizable": false, "fullscreen": false, "visible": false, "decorations": false, - "focus": false, + "focus": true, "url": "tray.html", "alwaysOnTop": true, "backgroundThrottling": "disabled", diff --git a/src/pages/Startup.tsx b/src/pages/Startup.tsx index 91e9023..907f86d 100644 --- a/src/pages/Startup.tsx +++ b/src/pages/Startup.tsx @@ -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 { getCurrentWindow } from '@tauri-apps/api/window' import { platform } from '@tauri-apps/plugin-os' import { exit } from '@tauri-apps/plugin-process' import { AnimatePresence, motion } from 'framer-motion' import { useEffect, useMemo, useState } from 'react' -import { formatBytes } from '../../lib/format' import { openSmallWindow } from '../../lib/window' import { useStore } from '../../store/memory' import { usePersistedStore } from '../../store/persisted' @@ -44,11 +43,6 @@ export default function Startup() { const [titleIndex, setTitleIndex] = useState(0) 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 shortcutDisplay = useMemo(() => { @@ -71,37 +65,6 @@ export default function Startup() { () => startupStatus === 'error' || startupStatus === 'fatal', [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(() => { let intervalId: NodeJS.Timeout | null = null @@ -121,16 +84,11 @@ export default function Startup() { } }, [startupStatus]) - // Linux can emit focus changes while the startup window is still provisioning rclone. + // Close window when it loses focus useEffect(() => { const currentWindow = getCurrentWindow() const unlisten = currentWindow.onFocusChanged(async ({ payload: focused }) => { if (!focused) { - const status = useStore.getState().startupStatus - const busy = status === 'initializing' || status === 'updating' - if (platform() === 'linux' && busy) { - return - } await currentWindow.hide() await currentWindow.destroy() } @@ -158,8 +116,7 @@ export default function Startup() { exit={{ opacity: 0 }} className="ml-2 text-2xl" > - {startupMessage || - 'Could not complete the operation, please try again later.'} + Could not complete the operation, please try again later. )} {startupStatus === 'initialized' && ( @@ -184,40 +141,39 @@ export default function Startup() { Rclone has just been updated, thanks for waiting! )} - {isBusy && ( - -

- - {startupStatus === 'updating' - ? WAIT[titleIndex % WAIT.length] - : GREET[titleIndex % GREET.length]} - {' '} - 👋 -

- - {startupIsDownloading && ( -
- -

- {downloadProgressLabel} -

-
- )} -
+ + {GREET[titleIndex]} + {' '} + 👋 + + )} + {startupStatus === 'updating' && ( + + + {WAIT[titleIndex]} + {' '} + 👋 + )} @@ -295,13 +251,13 @@ export default function Startup() { )} - {isBusy && ( - - {startupMessage || - (startupStatus === 'updating' - ? 'Rclone is updating' - : 'Rclone is initializing')} + Rclone is initializing - {startupIsDownloading && ( - - )} - + + )} + {startupStatus === 'updating' && ( + + + Rclone is updating + + )} diff --git a/store/memory.ts b/store/memory.ts index ba42b06..8c8c9bb 100644 --- a/store/memory.ts +++ b/store/memory.ts @@ -14,11 +14,6 @@ interface State { | 'fatal' startupDisplayed: boolean - startupMessage: string | null - startupIsDownloading: boolean - startupDownloadedBytes: number - startupTotalBytes: number | null - startupDownloadSpeed: number | null isRestartingRclone: boolean @@ -42,11 +37,6 @@ export const useStore = create()( startupStatus: null, startupDisplayed: false, - startupMessage: null, - startupIsDownloading: false, - startupDownloadedBytes: 0, - startupTotalBytes: null, - startupDownloadSpeed: null, isRestartingRclone: false,