"use client"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { BrailleSpinner } from "@/components/braille-spinner"; import { SettingsRow, Toggle } from "./controls.tsx"; /** Observational snapshot from electron/main.cjs; never updater permission. */ export type UpdateState = { status: "idle" | "checking" | "downloading" | "up-to-date" | "downloaded" | "error" | "disabled"; version: string | null; revision: number; }; export const APP_VERSION = process.env.LH_VERSION ?? ""; const HOMEPAGE = process.env.LH_HOMEPAGE ?? ""; export const MANUAL_UPDATE_HINT = "Checking downloads available updates. Downloaded updates install when you quit the app."; export const AUTO_UPDATE_HINT = "Check at startup and every 24 hours. Turning this off does not stop updates already in progress or ready to install."; const UPDATE_ERROR = "Couldn’t check for updates. Try again later."; /** Subscribe before requesting the snapshot. IPC replies and pushes can * cross; a revision prevents an older reply from hiding a newer event. * The banner shares this read-only observer so late loads recover too. */ export function useUpdateState() { const [state, setState] = useState(null); const [failed, setFailed] = useState(false); useEffect(() => { const lh = window.lh; if (!lh) return; let live = true; let revision = -1; const accept = (next: UpdateState) => { if (!live || next.revision < revision) return; revision = next.revision; setState(next); setFailed(false); }; const off = lh.onUpdateStatus?.(accept); void lh.getUpdateState?.().then(accept).catch(() => { if (live && revision === -1) setFailed(true); }); return () => { live = false; off?.(); }; }, []); return { state, failed }; } export function updateStatusText(state: UpdateState | null): string | null { switch (state?.status) { case "checking": return "Checking for updates…"; case "downloading": return state.version ? `Downloading version ${state.version}…` : "Downloading an update…"; case "up-to-date": return "No newer version was found at the last check."; case "downloaded": return `${state.version ? `Version ${state.version}` : "An update"} is ready. It will install when you quit the app.`; case "error": return UPDATE_ERROR; default: return null; } } /** Version information is useful everywhere; update controls only belong * to a shell with an available updater. Opening this row never checks. */ export function UpdateSettings({ autoUpdate, onAutoUpdateChange, version = APP_VERSION, homepage = HOMEPAGE, }: { autoUpdate: boolean; onAutoUpdateChange: (on: boolean) => void; version?: string; homepage?: string; }) { const { state, failed } = useUpdateState(); const [checkFailed, setCheckFailed] = useState(false); useEffect(() => setCheckFailed(false), [state]); const active = state !== null && state.status !== "disabled"; const busy = state?.status === "checking" || state?.status === "downloading"; const status = state?.status === "downloaded" ? updateStatusText(state) : failed || checkFailed ? UPDATE_ERROR : updateStatusText(state); const releaseNotes = version && homepage ? `${homepage}/releases/tag/v${version}` : null; const check = () => { setCheckFailed(false); void window.lh?.checkForUpdates?.().catch(() => setCheckFailed(true)); }; return ( <> {status && {status}} {active && {MANUAL_UPDATE_HINT}} {releaseNotes && Release notes} } > {active && (state.status === "downloaded" ? ( ) : ( ))} {active && ( )} ); }