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,
|
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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())?;
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user