From b8e1e925b3ed04afa4b70a180d20439300a35691 Mon Sep 17 00:00:00 2001 From: nineap Date: Thu, 9 Jul 2026 16:36:39 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D1=82=20UpdateChecker=20(=D0=B4=D0=B5=D1=81=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D0=BF)=20=D0=B8=20React=20ErrorBoundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UpdateChecker: динамический импорт @tauri-apps/plugin-updater (no-op вне Tauri/на мобилке), диалог с прогрессом, downloadAndInstall + relaunch. ErrorBoundary оборачивает всё приложение — раньше необработанное исключение в рендере роняло весь SPA в белый экран без возможности восстановиться без ручной перезагрузки страницы. Co-Authored-By: Claude Sonnet 5 --- src/App.tsx | 12 +++-- src/features/ErrorBoundary.tsx | 53 +++++++++++++++++++ src/features/UpdateChecker.tsx | 94 ++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/features/ErrorBoundary.tsx create mode 100644 src/features/UpdateChecker.tsx diff --git a/src/App.tsx b/src/App.tsx index 6e7af1d..b50299d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,8 @@ import { NetrunnerMatrix } from "matrix-engine/NetrunnerMatrix"; import { useSettingsStore } from "./store/useSettingsStore"; import { cn } from "@/lib/utils"; import { TelegramWeb } from "./pages/TelegramWeb"; +import { UpdateChecker } from "./features/UpdateChecker"; +import { ErrorBoundary } from "./features/ErrorBoundary"; function AppContent() { const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -102,6 +104,8 @@ function AppContent() { onClose={() => setIsMenuOpen(false)} /> + +
: } /> @@ -118,8 +122,10 @@ function AppContent() { export default function App() { return ( - - - + + + + + ); } diff --git a/src/features/ErrorBoundary.tsx b/src/features/ErrorBoundary.tsx new file mode 100644 index 0000000..bb6921c --- /dev/null +++ b/src/features/ErrorBoundary.tsx @@ -0,0 +1,53 @@ +import { Component, type ErrorInfo, type ReactNode } from "react"; +import { withTranslation, type WithTranslation } from "react-i18next"; +import { Button } from "@/components/ui/button"; + +interface Props extends WithTranslation { + children: ReactNode; +} + +interface State { + error: Error | null; +} + +/** + * Ловит непойманные ошибки рендера где угодно в дереве ниже — без неё любой + * баг в компоненте (например, неожиданный ответ бэкенда, на который никто не + * рассчитывал) валит всё приложение в белый экран без единого сообщения. + * React error boundary обязан быть классовым компонентом — хуки этого не умеют. + */ +class ErrorBoundaryImpl extends Component { + state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error("[ErrorBoundary] Непойманная ошибка рендера:", error, info.componentStack); + } + + handleReload = () => { + this.setState({ error: null }); + window.location.reload(); + }; + + render() { + const { error } = this.state; + const { t } = this.props; + + if (!error) return this.props.children; + + return ( +
+

{t("error_boundary_title")}

+

+ {t("error_boundary_desc")} +

+ +
+ ); + } +} + +export const ErrorBoundary = withTranslation()(ErrorBoundaryImpl); diff --git a/src/features/UpdateChecker.tsx b/src/features/UpdateChecker.tsx new file mode 100644 index 0000000..42ad464 --- /dev/null +++ b/src/features/UpdateChecker.tsx @@ -0,0 +1,94 @@ +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(null); + const [status, setStatus] = useState("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 ( + {}}> + + + {t("update_available_title")} + + {t("update_available_desc", { version: update.version })} + + + + + + + + ); +}