openpencil/src/app/shell/updater.ts
Danila Poyarkov c3f5189f8f style: tighten import grouping
- Configure oxfmt custom import groups for workspace, app, package, and test aliases
- Keep type imports grouped with their matching source category instead of one global tail group
- Expand the format script to cover formatter config, Vite files, and scripts
2026-05-06 02:22:08 +03:00

121 lines
3.4 KiB
TypeScript

import { promiseTimeout } from '@vueuse/core'
import type { Ref } from 'vue'
import { toast } from '@/app/shell/ui'
import { isTauri } from '@/app/tauri/env'
const STARTUP_UPDATE_CHECK_DELAY_MS = 2500
interface UpdaterMessages {
appUpToDate: string
updateAvailableTitle: string
updateAvailable: (params: { version: string }) => string
updateInstallPrompt: string
downloadingUpdate: (params: { version: string }) => string
updateInstalledTitle: string
updateInstalled: (params: { version: string; size: string }) => string
updateUnavailable: string
updateCheckFailed: (params: { error: string }) => string
}
interface UpdateCheckOptions {
silent?: boolean
messages: Ref<UpdaterMessages>
}
let startupCheckStarted = false
let updateCheckInFlight: Promise<void> | null = null
export async function checkForAppUpdate(options: UpdateCheckOptions) {
if (!isTauri()) return
if (updateCheckInFlight) return updateCheckInFlight
const { silent = false, messages } = options
updateCheckInFlight = runUpdateCheck(silent, messages).finally(() => {
updateCheckInFlight = null
})
return updateCheckInFlight
}
export function scheduleStartupUpdateCheck(messages: Ref<UpdaterMessages>) {
if (startupCheckStarted || !isTauri()) return
startupCheckStarted = true
void promiseTimeout(STARTUP_UPDATE_CHECK_DELAY_MS).then(() =>
checkForAppUpdate({ silent: true, messages })
)
}
async function runUpdateCheck(silent: boolean, messages: Ref<UpdaterMessages>) {
try {
const [{ check }, { confirm, message }, { relaunch }] = await Promise.all([
import('@tauri-apps/plugin-updater'),
import('@tauri-apps/plugin-dialog'),
import('@tauri-apps/plugin-process')
])
const update = await check()
const t = messages.value
if (!update) {
if (!silent) toast.info(t.appUpToDate)
return
}
const details = [
t.updateAvailable({ version: update.version }),
update.body ? `\n${update.body}` : '',
`\n${t.updateInstallPrompt}`
].join('')
const shouldInstall = await confirm(details, {
title: t.updateAvailableTitle,
kind: 'info'
})
if (!shouldInstall) return
let downloaded = 0
let contentLength: number | undefined
toast.info(t.downloadingUpdate({ version: update.version }))
await update.downloadAndInstall((event) => {
if (event.event === 'Started') {
contentLength = event.data.contentLength
return
}
if (event.event === 'Progress') {
downloaded += event.data.chunkLength
}
})
const sizeLabel = contentLength
? ` (${formatBytes(downloaded)} of ${formatBytes(contentLength)})`
: ''
await message(t.updateInstalled({ version: update.version, size: sizeLabel }), {
title: t.updateInstalledTitle,
kind: 'info'
})
await relaunch()
} catch (error) {
if (!silent) {
const message = error instanceof Error ? error.message : String(error)
toast.warning(
isMissingUpdateManifestError(message)
? messages.value.updateUnavailable
: messages.value.updateCheckFailed({ error: message })
)
}
}
}
function isMissingUpdateManifestError(message: string) {
return message.toLowerCase().includes('valid release json')
}
function formatBytes(bytes: number) {
if (bytes < 1024) return `${bytes} B`
const kib = bytes / 1024
if (kib < 1024) return `${kib.toFixed(1)} KiB`
return `${(kib / 1024).toFixed(1)} MiB`
}