b8e1e925b3
UpdateChecker: динамический импорт @tauri-apps/plugin-updater (no-op вне Tauri/на мобилке), диалог с прогрессом, downloadAndInstall + relaunch. ErrorBoundary оборачивает всё приложение — раньше необработанное исключение в рендере роняло весь SPA в белый экран без возможности восстановиться без ручной перезагрузки страницы. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { Update } from "@tauri-apps/plugin-updater";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
type Status = "idle" | "downloading" | "error";
|
|
|
|
/**
|
|
* Проверка обновлений — только десктоп (Windows/macOS/Linux). На мобильных
|
|
* сборках плагин не зарегистрирован (см. src-tauri/src/lib.rs, #[cfg(desktop)]),
|
|
* поэтому check() там просто недоступен — ошибка тихо проглатывается ниже,
|
|
* без падения остального приложения.
|
|
*/
|
|
export function UpdateChecker() {
|
|
const { t } = useTranslation();
|
|
const [update, setUpdate] = useState<Update | null>(null);
|
|
const [status, setStatus] = useState<Status>("idle");
|
|
const [progress, setProgress] = useState(0);
|
|
const installing = useRef(false);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const { check } = await import("@tauri-apps/plugin-updater");
|
|
const result = await check();
|
|
if (!cancelled && result?.available) {
|
|
setUpdate(result);
|
|
}
|
|
} catch {
|
|
// Мобильная сборка или плагин недоступен в этом окружении — не критично.
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
if (!update) return null;
|
|
|
|
const installUpdate = async () => {
|
|
if (installing.current) return;
|
|
installing.current = true;
|
|
setStatus("downloading");
|
|
try {
|
|
let downloaded = 0;
|
|
let total = 0;
|
|
await update.downloadAndInstall((event) => {
|
|
if (event.event === "Started") {
|
|
total = event.data.contentLength ?? 0;
|
|
} else if (event.event === "Progress") {
|
|
downloaded += event.data.chunkLength;
|
|
setProgress(total > 0 ? Math.round((downloaded / total) * 100) : 0);
|
|
}
|
|
});
|
|
const { relaunch } = await import("@tauri-apps/plugin-process");
|
|
await relaunch();
|
|
} catch (e) {
|
|
console.error("[UpdateChecker] Установка обновления не удалась:", e);
|
|
setStatus("error");
|
|
installing.current = false;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open onOpenChange={() => {}}>
|
|
<DialogContent showCloseButton={status !== "downloading"}>
|
|
<DialogHeader>
|
|
<DialogTitle>{t("update_available_title")}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("update_available_desc", { version: update.version })}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button onClick={installUpdate} disabled={status === "downloading"}>
|
|
{status === "downloading"
|
|
? t("update_downloading", { progress })
|
|
: status === "error"
|
|
? t("update_retry")
|
|
: t("update_install_now")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|