Add in-app updater functionality

This commit is contained in:
Christoph Brandau
2026-06-30 16:56:17 +02:00
parent 9ebddf3abe
commit c5853a6e9b
3 changed files with 425 additions and 0 deletions
+100
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
import { AlertCircle, Check, FolderOpen, GitBranch, GitMerge, LoaderCircle } from "@lucide/svelte";
import TitleBar from "./lib/TitleBar.svelte";
@@ -15,6 +16,7 @@
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
import ResolveDialog from "./lib/components/ResolveDialog.svelte";
import StatusPanel from "./lib/components/StatusPanel.svelte";
import UpdateToast from "./lib/components/UpdateToast.svelte";
import {
checkoutBranch,
@@ -71,6 +73,8 @@
stripAuthPrefix,
} from "./lib/credentials";
type UpdateToastState = "available" | "downloading" | "installed" | "error";
// ── State ──────────────────────────────────────────────────────────────────
let repoPath = "";
@@ -111,6 +115,16 @@
let lastStatusFingerprint = "";
const AUTO_REFRESH_INTERVAL = 4000;
let autoRefreshTimer: ReturnType<typeof setInterval> | undefined;
let updateToastOpen = false;
let updateToastState: UpdateToastState = "available";
let pendingUpdate: Update | null = null;
let updateVersion = "";
let updateCurrentVersion = "";
let updateProgress = 0;
let updateError = "";
let updateCheckInFlight = false;
let updateDownloadTotal = 0;
let updateDownloadedBytes = 0;
// ── Derived ────────────────────────────────────────────────────────────────
@@ -133,6 +147,7 @@
onMount(() => {
autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
void checkForUpdates();
});
onDestroy(() => {
@@ -166,6 +181,78 @@
if (autoRefreshEnabled) void autoRefreshTick();
}
// ── Updates ────────────────────────────────────────────────────────────────
async function checkForUpdates() {
if (updateCheckInFlight || updateToastState === "downloading") return;
updateCheckInFlight = true;
try {
const update = await check();
if (!update) return;
if (pendingUpdate && pendingUpdate !== update) {
void pendingUpdate.close().catch(() => {});
}
pendingUpdate = update;
updateVersion = update.version;
updateCurrentVersion = update.currentVersion;
updateProgress = 0;
updateError = "";
updateToastState = "available";
updateToastOpen = true;
} catch {
// Update checks should be quiet when offline or when the endpoint is unavailable.
} finally {
updateCheckInFlight = false;
}
}
function updateDownloadProgress(event: DownloadEvent) {
if (event.event === "Started") {
updateProgress = 0;
updateDownloadTotal = event.data.contentLength ?? 0;
updateDownloadedBytes = 0;
return;
}
if (event.event === "Progress") {
updateDownloadedBytes += event.data.chunkLength;
updateProgress = updateDownloadTotal > 0
? Math.min(99, Math.round((updateDownloadedBytes / updateDownloadTotal) * 100))
: 0;
return;
}
updateProgress = 100;
}
async function installPendingUpdate() {
if (!pendingUpdate || updateToastState === "downloading") return;
updateToastState = "downloading";
updateProgress = 0;
updateError = "";
updateToastOpen = true;
updateDownloadTotal = 0;
updateDownloadedBytes = 0;
try {
await pendingUpdate.downloadAndInstall(updateDownloadProgress);
updateProgress = 100;
updateToastState = "installed";
} catch (error) {
updateToastState = "error";
updateError = errorToMessage(error);
}
}
function dismissUpdateToast() {
if (updateToastState === "downloading") return;
updateToastOpen = false;
}
// ── Utilities ──────────────────────────────────────────────────────────────
function applyStatus(nextStatus: GitStatus) {
@@ -1009,6 +1096,19 @@
</div>
</main>
{#if updateToastOpen}
<UpdateToast
state={updateToastState}
version={updateVersion}
currentVersion={updateCurrentVersion}
progress={updateProgress}
error={updateError}
onInstall={installPendingUpdate}
onLater={dismissUpdateToast}
onDismiss={dismissUpdateToast}
/>
{/if}
<!-- Compare diff dialog -->
{#if compareDialogOpen && comparison}
<CompareDialog