Revert "Fix Linux first-launch startup feedback when provisioning bundled rclone"
This reverts commit 3201b0306c.
This commit is contained in:
+12
-277
@@ -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<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[]) {
|
||||
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
|
||||
}
|
||||
|
||||
+1
-3
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ async function registerRcloneWindowListeners() {
|
||||
rcloneListenersRegistered = true
|
||||
}
|
||||
|
||||
async function startRclone(): Promise<boolean> {
|
||||
async function startRclone() {
|
||||
console.log('[startRclone]')
|
||||
|
||||
await registerRcloneWindowListeners()
|
||||
@@ -430,8 +430,7 @@ async function startRclone(): Promise<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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)
|
||||
|
||||
@@ -213,7 +213,6 @@ pub async fn open_small_window(
|
||||
app_handle: AppHandle,
|
||||
name: String,
|
||||
url: String,
|
||||
transparent: Option<bool>,
|
||||
) -> 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())?;
|
||||
|
||||
@@ -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",
|
||||
|
||||
+58
-96
@@ -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.
|
||||
</motion.p>
|
||||
)}
|
||||
{startupStatus === 'initialized' && (
|
||||
@@ -184,40 +141,39 @@ export default function Startup() {
|
||||
Rclone has just been updated, thanks for waiting!
|
||||
</motion.p>
|
||||
)}
|
||||
{isBusy && (
|
||||
<motion.div
|
||||
key="busy"
|
||||
{startupStatus === 'initializing' && (
|
||||
<motion.p
|
||||
key="initializing"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
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
|
||||
key={titleIndex}
|
||||
className="inline-block align-middle animate-fade-in-up"
|
||||
>
|
||||
{startupStatus === 'updating'
|
||||
? WAIT[titleIndex % WAIT.length]
|
||||
: GREET[titleIndex % GREET.length]}
|
||||
</span>{' '}
|
||||
<span className="inline-block align-middle">👋</span>
|
||||
</p>
|
||||
|
||||
{startupIsDownloading && (
|
||||
<div className="w-full max-w-md space-y-2">
|
||||
<Progress
|
||||
value={downloadProgress ?? undefined}
|
||||
isIndeterminate={downloadProgress === null}
|
||||
aria-label="Rclone download progress"
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-sm text-center text-default-500">
|
||||
{downloadProgressLabel}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
<span
|
||||
key={titleIndex}
|
||||
className="inline-block align-middle animate-fade-in-up"
|
||||
>
|
||||
{GREET[titleIndex]}
|
||||
</span>{' '}
|
||||
<span className="inline-block align-middle">👋</span>
|
||||
</motion.p>
|
||||
)}
|
||||
{startupStatus === 'updating' && (
|
||||
<motion.p
|
||||
key="updating"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="ml-2 text-3xl"
|
||||
>
|
||||
<span
|
||||
key={titleIndex}
|
||||
className="inline-block align-middle animate-fade-in-up"
|
||||
>
|
||||
{WAIT[titleIndex]}
|
||||
</span>{' '}
|
||||
<span className="inline-block align-middle">👋</span>
|
||||
</motion.p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
@@ -295,13 +251,13 @@ export default function Startup() {
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
{isBusy && (
|
||||
<motion.div
|
||||
key="busy"
|
||||
{startupStatus === 'initializing' && (
|
||||
<motion.p
|
||||
key="initializing"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
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
|
||||
animate={{ opacity: [1, 0.5, 1] }}
|
||||
@@ -311,23 +267,29 @@ export default function Startup() {
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
{startupMessage ||
|
||||
(startupStatus === 'updating'
|
||||
? 'Rclone is updating'
|
||||
: 'Rclone is initializing')}
|
||||
Rclone is initializing
|
||||
</motion.span>
|
||||
{startupIsDownloading && (
|
||||
<Button
|
||||
variant="flat"
|
||||
color="default"
|
||||
onPress={async () => {
|
||||
await exit(0)
|
||||
}}
|
||||
>
|
||||
Quit for now
|
||||
</Button>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.p>
|
||||
)}
|
||||
{startupStatus === 'updating' && (
|
||||
<motion.p
|
||||
key="updating"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="uppercase text-small"
|
||||
>
|
||||
<motion.span
|
||||
animate={{ opacity: [1, 0.5, 1] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 4,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
Rclone is updating
|
||||
</motion.span>
|
||||
</motion.p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
@@ -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<State>()(
|
||||
|
||||
startupStatus: null,
|
||||
startupDisplayed: false,
|
||||
startupMessage: null,
|
||||
startupIsDownloading: false,
|
||||
startupDownloadedBytes: 0,
|
||||
startupTotalBytes: null,
|
||||
startupDownloadSpeed: null,
|
||||
|
||||
isRestartingRclone: false,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user