mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 23:43:35 +00:00
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)
This commit is contained in:
1 parent
001f86a550
commit
5a5023d1ea
5 files changed
+216
-3
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [appVersion, setAppVersion] = useState<string | null>(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<HTMLDivElement>(null);
|
||||
const hudContentRef = useRef<HTMLDivElement>(null);
|
||||
const hudBarRef = useRef<HTMLDivElement>(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 <CheckCircle2 size={14} />;
|
||||
case "checking":
|
||||
case "downloading":
|
||||
return <RefreshCw size={14} className={styles.updateBadgeSpin} />;
|
||||
default:
|
||||
return <ArrowUpCircle size={14} />;
|
||||
}
|
||||
})();
|
||||
|
||||
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 = (
|
||||
<>
|
||||
<div className="flex items-center gap-[5px]">
|
||||
@@ -1054,6 +1161,19 @@ export function LaunchWindow() {
|
||||
<RxDragHandleDots2 size={14} className="text-[#6b6b78]" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void handleUpdateButtonClick();
|
||||
}}
|
||||
className={updateButtonClassName}
|
||||
title={updateButtonTitle}
|
||||
disabled={updateActionPending}
|
||||
>
|
||||
{updateButtonIcon}
|
||||
<span>{updateButtonLabel}</span>
|
||||
</button>
|
||||
|
||||
<div className={styles.barStateViewport}>
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
<motion.div
|
||||
|
||||
@@ -37,7 +37,18 @@
|
||||
"pause": "Pause",
|
||||
"stop": "Stop",
|
||||
"cancel": "Cancel",
|
||||
"more": "More"
|
||||
"more": "More",
|
||||
"update": {
|
||||
"update": "Update",
|
||||
"updated": "Updated",
|
||||
"idleTitle": "Check for updates.",
|
||||
"checkingTitle": "Checking for updates...",
|
||||
"downloadingTitle": "Downloading update...",
|
||||
"errorTitle": "Update check failed. Click to retry.",
|
||||
"upToDateTitle": "Recordly {{version}} is up to date.",
|
||||
"availableTitle": "Recordly {{version}} is available.",
|
||||
"availableGenericTitle": "An update is available."
|
||||
}
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loadingSources": "Loading sources...",
|
||||
|
||||
@@ -37,7 +37,18 @@
|
||||
"pause": "Pausa",
|
||||
"stop": "Detener",
|
||||
"cancel": "Cancelar",
|
||||
"more": "Más"
|
||||
"more": "Más",
|
||||
"update": {
|
||||
"update": "Actualizar",
|
||||
"updated": "Actualizado",
|
||||
"idleTitle": "Buscar actualizaciones.",
|
||||
"checkingTitle": "Buscando actualizaciones...",
|
||||
"downloadingTitle": "Descargando actualización...",
|
||||
"errorTitle": "La comprobación de actualizaciones falló. Haz clic para reintentar.",
|
||||
"upToDateTitle": "Recordly {{version}} está actualizado.",
|
||||
"availableTitle": "Recordly {{version}} está disponible.",
|
||||
"availableGenericTitle": "Hay una actualización disponible."
|
||||
}
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loadingSources": "Cargando fuentes...",
|
||||
|
||||
@@ -37,7 +37,18 @@
|
||||
"pause": "暂停",
|
||||
"stop": "停止",
|
||||
"cancel": "取消",
|
||||
"more": "更多"
|
||||
"more": "更多",
|
||||
"update": {
|
||||
"update": "更新",
|
||||
"updated": "已是最新",
|
||||
"idleTitle": "检查更新。",
|
||||
"checkingTitle": "正在检查更新...",
|
||||
"downloadingTitle": "正在下载更新...",
|
||||
"errorTitle": "检查更新失败。点击重试。",
|
||||
"upToDateTitle": "Recordly {{version}} 已是最新版本。",
|
||||
"availableTitle": "Recordly {{version}} 可更新。",
|
||||
"availableGenericTitle": "有可用更新。"
|
||||
}
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loadingSources": "正在加载源...",
|
||||
|
||||
Reference in new issue
Block a user