Simpl-Resultat/src/hooks/useUpdater.ts
Le-King-Fu 0adfa5fe5e feat: add Settings page with in-app updater support
Add a Settings page with about card (app name + version) and an update
section that uses the Tauri v2 updater plugin to check GitHub Releases,
download signed installers, and relaunch. Includes full state machine
(idle/checking/available/downloading/readyToInstall/installing/error)
with progress bar and retry. Database in %APPDATA% is never touched.

- Add tauri-plugin-updater and tauri-plugin-process (Rust + npm)
- Configure updater endpoint, pubkey placeholder, and passive install mode
- Add signing env vars and updaterJsonPreferNsis to release workflow
- Add Settings nav item, route, and fr/en translations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 11:47:25 +00:00

122 lines
3.6 KiB
TypeScript

import { useReducer, useCallback, useRef } from "react";
import { check, type Update } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
type UpdateStatus =
| "idle"
| "checking"
| "upToDate"
| "available"
| "downloading"
| "readyToInstall"
| "installing"
| "error";
interface UpdaterState {
status: UpdateStatus;
version: string | null;
progress: number;
contentLength: number | null;
error: string | null;
}
type UpdaterAction =
| { type: "CHECK_START" }
| { type: "UP_TO_DATE" }
| { type: "AVAILABLE"; version: string }
| { type: "DOWNLOAD_START" }
| { type: "DOWNLOAD_PROGRESS"; downloaded: number; contentLength: number | null }
| { type: "READY_TO_INSTALL" }
| { type: "INSTALLING" }
| { type: "ERROR"; error: string };
const initialState: UpdaterState = {
status: "idle",
version: null,
progress: 0,
contentLength: null,
error: null,
};
function reducer(state: UpdaterState, action: UpdaterAction): UpdaterState {
switch (action.type) {
case "CHECK_START":
return { ...initialState, status: "checking" };
case "UP_TO_DATE":
return { ...state, status: "upToDate", error: null };
case "AVAILABLE":
return { ...state, status: "available", version: action.version, error: null };
case "DOWNLOAD_START":
return { ...state, status: "downloading", progress: 0, contentLength: null, error: null };
case "DOWNLOAD_PROGRESS":
return { ...state, progress: action.downloaded, contentLength: action.contentLength ?? state.contentLength };
case "READY_TO_INSTALL":
return { ...state, status: "readyToInstall", error: null };
case "INSTALLING":
return { ...state, status: "installing", error: null };
case "ERROR":
return { ...state, status: "error", error: action.error };
}
}
export function useUpdater() {
const [state, dispatch] = useReducer(reducer, initialState);
const updateRef = useRef<Update | null>(null);
const checkForUpdate = useCallback(async () => {
dispatch({ type: "CHECK_START" });
try {
const update = await check();
if (update) {
updateRef.current = update;
dispatch({ type: "AVAILABLE", version: update.version });
} else {
dispatch({ type: "UP_TO_DATE" });
}
} catch (e) {
dispatch({ type: "ERROR", error: e instanceof Error ? e.message : String(e) });
}
}, []);
const downloadAndInstall = useCallback(async () => {
const update = updateRef.current;
if (!update) return;
dispatch({ type: "DOWNLOAD_START" });
try {
let downloaded = 0;
await update.downloadAndInstall((event) => {
if (event.event === "Started") {
dispatch({
type: "DOWNLOAD_PROGRESS",
downloaded: 0,
contentLength: event.data.contentLength ?? null,
});
} else if (event.event === "Progress") {
downloaded += event.data.chunkLength;
dispatch({
type: "DOWNLOAD_PROGRESS",
downloaded,
contentLength: null,
});
} else if (event.event === "Finished") {
// handled below
}
});
dispatch({ type: "READY_TO_INSTALL" });
} catch (e) {
dispatch({ type: "ERROR", error: e instanceof Error ? e.message : String(e) });
}
}, []);
const installAndRestart = useCallback(async () => {
dispatch({ type: "INSTALLING" });
try {
await relaunch();
} catch (e) {
dispatch({ type: "ERROR", error: e instanceof Error ? e.message : String(e) });
}
}, []);
return { state, checkForUpdate, downloadAndInstall, installAndRestart };
}