From 5a5023d1eacac1e1bcbbd467212d879fac2cbeb1 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:21:32 +1100 Subject: [PATCH] feat(launch): add update status badge to launch window toolbar - Add update badge button that shows current updater state (idle, checking, up-to-date, available, downloading, ready, error) - Badge triggers check/download/install based on current state - Poll update status summary every 2.5s for live state reflection - Add CSS module styles for badge variants (quiet, hot, spin) - Add i18n strings for badge labels and tooltips (en, es, zh-CN) --- src/components/launch/LaunchWindow.module.css | 60 +++++++++ src/components/launch/LaunchWindow.tsx | 120 ++++++++++++++++++ src/i18n/locales/en/launch.json | 13 +- src/i18n/locales/es/launch.json | 13 +- src/i18n/locales/zh-CN/launch.json | 13 +- 5 files changed, 216 insertions(+), 3 deletions(-) diff --git a/src/components/launch/LaunchWindow.module.css b/src/components/launch/LaunchWindow.module.css index bfa9de88..9bd61c6f 100644 --- a/src/components/launch/LaunchWindow.module.css +++ b/src/components/launch/LaunchWindow.module.css @@ -35,6 +35,66 @@ padding-right: 2px; } +.updateBadge { + display: inline-flex; + align-items: center; + gap: 7px; + height: 34px; + padding: 0 12px; + border-radius: 11px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.03); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.01em; + transition: all 0.15s ease; + cursor: pointer; + flex-shrink: 0; +} + +.updateBadge:disabled { + opacity: 0.72; + cursor: default; +} + +.updateBadgeQuiet { + color: #a5b4c7; + border-color: rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.035); +} + +.updateBadgeQuiet:hover:not(:disabled) { + color: #d7dee8; + background: rgba(255, 255, 255, 0.06); +} + +.updateBadgeHot { + color: #f8fbff; + border-color: rgba(125, 211, 252, 0.24); + background: linear-gradient(180deg, rgba(125, 211, 252, 0.12), rgba(125, 211, 252, 0.04)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.updateBadgeHot:hover:not(:disabled) { + color: #ffffff; + border-color: rgba(125, 211, 252, 0.36); + background: linear-gradient(180deg, rgba(125, 211, 252, 0.17), rgba(125, 211, 252, 0.07)); + transform: translateY(-1px); +} + +.updateBadgeSpin { + animation: updateBadgeSpin 0.9s linear infinite; +} + +@keyframes updateBadgeSpin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + .barState { display: flex; align-items: center; diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index f9ba3fe5..8f227105 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,6 +1,8 @@ import { AppWindow, + ArrowUpCircle, ChevronUp, + CheckCircle2, Eye, EyeOff, FolderOpen, @@ -12,6 +14,7 @@ import { MoreVertical, Pause, Play, + RefreshCw, Square, Timer, Video, @@ -185,6 +188,17 @@ export function LaunchWindow() { const [hideHudFromCapture, setHideHudFromCapture] = useState(true); const [platform, setPlatform] = useState(null); const [appVersion, setAppVersion] = useState(null); + const [updateStatus, setUpdateStatus] = useState<{ + status: "idle" | "checking" | "up-to-date" | "available" | "downloading" | "ready" | "error"; + currentVersion: string; + availableVersion: string | null; + detail?: string; + }>({ + status: "idle", + currentVersion: "", + availableVersion: null, + }); + const [updateActionPending, setUpdateActionPending] = useState(false); const dropdownRef = useRef(null); const hudContentRef = useRef(null); const hudBarRef = useRef(null); @@ -372,6 +386,31 @@ export function LaunchWindow() { }; }, []); + useEffect(() => { + let mounted = true; + + const refreshUpdateStatus = async () => { + try { + const summary = await window.electronAPI.getUpdateStatusSummary(); + if (mounted) { + setUpdateStatus(summary); + } + } catch (error) { + console.error("Failed to load update status summary:", error); + } + }; + + void refreshUpdateStatus(); + const pollTimer = window.setInterval(() => { + void refreshUpdateStatus(); + }, 2500); + + return () => { + mounted = false; + window.clearInterval(pollTimer); + }; + }, []); + useEffect(() => { let cancelled = false; const loadVersion = async () => { @@ -619,6 +658,74 @@ export function LaunchWindow() { toggleDropdown("webcam"); }; + const updateButtonLabel = + updateStatus.status === "up-to-date" + ? t("recording.update.updated") + : t("recording.update.update"); + const updateButtonTitle = (() => { + switch (updateStatus.status) { + case "up-to-date": + return t("recording.update.upToDateTitle", "Recordly {{version}} is up to date.", { + version: updateStatus.currentVersion, + }); + case "available": + case "ready": + return updateStatus.availableVersion + ? t("recording.update.availableTitle", "Recordly {{version}} is available.", { + version: updateStatus.availableVersion, + }) + : t("recording.update.availableGenericTitle"); + case "downloading": + return updateStatus.detail ?? t("recording.update.downloadingTitle"); + case "checking": + return t("recording.update.checkingTitle"); + case "error": + return updateStatus.detail ?? t("recording.update.errorTitle"); + default: + return t("recording.update.idleTitle"); + } + })(); + const updateButtonClassName = `${styles.updateBadge} ${updateStatus.status === "up-to-date" ? styles.updateBadgeQuiet : styles.updateBadgeHot} ${styles.electronNoDrag}`; + const updateButtonIcon = (() => { + switch (updateStatus.status) { + case "up-to-date": + return ; + case "checking": + case "downloading": + return ; + default: + return ; + } + })(); + + const handleUpdateButtonClick = async () => { + if (updateActionPending || updateStatus.status === "downloading") { + return; + } + + setUpdateActionPending(true); + try { + switch (updateStatus.status) { + case "available": + await window.electronAPI.downloadAvailableUpdate(); + break; + case "ready": + await window.electronAPI.installDownloadedUpdate(); + break; + default: + await window.electronAPI.checkForAppUpdates(); + break; + } + + const summary = await window.electronAPI.getUpdateStatusSummary(); + setUpdateStatus(summary); + } catch (error) { + console.error("Failed to handle update button action:", error); + } finally { + setUpdateActionPending(false); + } + }; + const recordingControls = ( <>
@@ -1054,6 +1161,19 @@ export function LaunchWindow() {
+ +