|
1 | 1 | import { createFileRoute } from "@tanstack/react-router"; |
2 | 2 | import { useQuery } from "@tanstack/react-query"; |
3 | | -import { useCallback, useState } from "react"; |
4 | | -import { type ProviderKind } from "@t3tools/contracts"; |
| 3 | +import { useCallback, useEffect, useState } from "react"; |
| 4 | +import { type DesktopUpdateState, type ProviderKind } from "@t3tools/contracts"; |
5 | 5 | import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; |
6 | 6 |
|
7 | 7 | import { |
@@ -211,6 +211,82 @@ function SettingsRouteView() { |
211 | 211 | Partial<Record<ProviderKind, string | null>> |
212 | 212 | >({}); |
213 | 213 |
|
| 214 | + const [updateState, setUpdateState] = useState<DesktopUpdateState | null>(null); |
| 215 | + const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); |
| 216 | + |
| 217 | + const hasDesktopBridge = isElectron && !!window.desktopBridge; |
| 218 | + |
| 219 | + useEffect(() => { |
| 220 | + if (!hasDesktopBridge) return; |
| 221 | + const bridge = window.desktopBridge!; |
| 222 | + void bridge |
| 223 | + .getUpdateState() |
| 224 | + .then(setUpdateState) |
| 225 | + .catch(() => {}); |
| 226 | + const unsubscribe = bridge.onUpdateState(setUpdateState); |
| 227 | + return unsubscribe; |
| 228 | + }, [hasDesktopBridge]); |
| 229 | + |
| 230 | + const handleCheckForUpdate = useCallback(async () => { |
| 231 | + if (!hasDesktopBridge) return; |
| 232 | + setIsCheckingUpdate(true); |
| 233 | + try { |
| 234 | + const state = await window.desktopBridge!.checkForUpdate(); |
| 235 | + setUpdateState(state); |
| 236 | + } catch { |
| 237 | + setUpdateState((prev) => |
| 238 | + prev |
| 239 | + ? { |
| 240 | + ...prev, |
| 241 | + status: "error", |
| 242 | + message: "Failed to check for updates.", |
| 243 | + errorContext: "check", |
| 244 | + } |
| 245 | + : prev, |
| 246 | + ); |
| 247 | + } finally { |
| 248 | + setIsCheckingUpdate(false); |
| 249 | + } |
| 250 | + }, [hasDesktopBridge]); |
| 251 | + |
| 252 | + const handleDownloadUpdate = useCallback(async () => { |
| 253 | + if (!hasDesktopBridge) return; |
| 254 | + try { |
| 255 | + const result = await window.desktopBridge!.downloadUpdate(); |
| 256 | + setUpdateState(result.state); |
| 257 | + } catch (error) { |
| 258 | + setUpdateState((prev) => |
| 259 | + prev |
| 260 | + ? { |
| 261 | + ...prev, |
| 262 | + status: "error", |
| 263 | + message: error instanceof Error ? error.message : "Failed to download update.", |
| 264 | + errorContext: "download", |
| 265 | + } |
| 266 | + : prev, |
| 267 | + ); |
| 268 | + } |
| 269 | + }, [hasDesktopBridge]); |
| 270 | + |
| 271 | + const handleInstallUpdate = useCallback(async () => { |
| 272 | + if (!hasDesktopBridge) return; |
| 273 | + try { |
| 274 | + const result = await window.desktopBridge!.installUpdate(); |
| 275 | + setUpdateState(result.state); |
| 276 | + } catch (error) { |
| 277 | + setUpdateState((prev) => |
| 278 | + prev |
| 279 | + ? { |
| 280 | + ...prev, |
| 281 | + status: "error", |
| 282 | + message: error instanceof Error ? error.message : "Failed to install update.", |
| 283 | + errorContext: "install", |
| 284 | + } |
| 285 | + : prev, |
| 286 | + ); |
| 287 | + } |
| 288 | + }, [hasDesktopBridge]); |
| 289 | + |
214 | 290 | const codexBinaryPath = settings.codexBinaryPath; |
215 | 291 | const codexHomePath = settings.codexHomePath; |
216 | 292 | const accentColor = settings.accentColor; |
@@ -995,14 +1071,88 @@ function SettingsRouteView() { |
995 | 1071 | </p> |
996 | 1072 | </div> |
997 | 1073 |
|
998 | | - <div className="flex items-center justify-between rounded-lg border border-border bg-background px-3 py-2"> |
999 | | - <div> |
1000 | | - <p className="text-sm font-medium text-foreground">Version</p> |
1001 | | - <p className="text-xs text-muted-foreground"> |
1002 | | - Current version of the application. |
1003 | | - </p> |
| 1074 | + <div className="space-y-3"> |
| 1075 | + <div className="flex items-center justify-between rounded-lg border border-border bg-background px-3 py-2"> |
| 1076 | + <div> |
| 1077 | + <p className="text-sm font-medium text-foreground">Version</p> |
| 1078 | + <p className="text-xs text-muted-foreground"> |
| 1079 | + {updateState?.status === "up-to-date" |
| 1080 | + ? "You're on the latest version." |
| 1081 | + : updateState?.status === "checking" |
| 1082 | + ? "Checking for updates..." |
| 1083 | + : updateState?.status === "available" |
| 1084 | + ? `Version ${updateState.availableVersion ?? "unknown"} is available.` |
| 1085 | + : updateState?.status === "downloading" |
| 1086 | + ? `Downloading update${typeof updateState.downloadPercent === "number" ? ` (${Math.floor(updateState.downloadPercent)}%)` : ""}...` |
| 1087 | + : updateState?.status === "downloaded" |
| 1088 | + ? `Version ${updateState.downloadedVersion ?? updateState.availableVersion ?? "unknown"} is ready to install.` |
| 1089 | + : updateState?.status === "error" |
| 1090 | + ? (updateState.message ?? "Update check failed.") |
| 1091 | + : "Current version of the application."} |
| 1092 | + </p> |
| 1093 | + {updateState?.checkedAt ? ( |
| 1094 | + <p className="mt-0.5 text-[11px] text-muted-foreground/70"> |
| 1095 | + Last checked: {new Date(updateState.checkedAt).toLocaleString()} |
| 1096 | + </p> |
| 1097 | + ) : null} |
| 1098 | + </div> |
| 1099 | + <div className="ml-3 flex shrink-0 items-center gap-2"> |
| 1100 | + <code className="text-xs font-medium text-muted-foreground">{APP_VERSION}</code> |
| 1101 | + {hasDesktopBridge ? ( |
| 1102 | + <> |
| 1103 | + {updateState?.status === "available" ? ( |
| 1104 | + <Button size="xs" onClick={handleDownloadUpdate}> |
| 1105 | + Download |
| 1106 | + </Button> |
| 1107 | + ) : null} |
| 1108 | + {updateState?.status === "downloaded" ? ( |
| 1109 | + <Button size="xs" onClick={handleInstallUpdate}> |
| 1110 | + Restart & Install |
| 1111 | + </Button> |
| 1112 | + ) : null} |
| 1113 | + {updateState?.status === "error" && |
| 1114 | + updateState.errorContext === "download" && |
| 1115 | + updateState.availableVersion ? ( |
| 1116 | + <Button size="xs" variant="outline" onClick={handleDownloadUpdate}> |
| 1117 | + Retry Download |
| 1118 | + </Button> |
| 1119 | + ) : null} |
| 1120 | + {updateState?.status === "error" && |
| 1121 | + updateState.errorContext === "install" && |
| 1122 | + updateState.downloadedVersion ? ( |
| 1123 | + <Button size="xs" variant="outline" onClick={handleInstallUpdate}> |
| 1124 | + Retry Install |
| 1125 | + </Button> |
| 1126 | + ) : null} |
| 1127 | + <Button |
| 1128 | + size="xs" |
| 1129 | + variant="outline" |
| 1130 | + disabled={ |
| 1131 | + isCheckingUpdate || |
| 1132 | + updateState?.status === "checking" || |
| 1133 | + updateState?.status === "downloading" |
| 1134 | + } |
| 1135 | + onClick={handleCheckForUpdate} |
| 1136 | + > |
| 1137 | + {isCheckingUpdate || updateState?.status === "checking" |
| 1138 | + ? "Checking..." |
| 1139 | + : "Check for Updates"} |
| 1140 | + </Button> |
| 1141 | + </> |
| 1142 | + ) : null} |
| 1143 | + </div> |
1004 | 1144 | </div> |
1005 | | - <code className="text-xs font-medium text-muted-foreground">{APP_VERSION}</code> |
| 1145 | + {updateState?.status === "downloading" && |
| 1146 | + typeof updateState.downloadPercent === "number" ? ( |
| 1147 | + <div className="px-1"> |
| 1148 | + <div className="h-1.5 w-full overflow-hidden rounded-full bg-muted"> |
| 1149 | + <div |
| 1150 | + className="h-full rounded-full bg-primary transition-all duration-300" |
| 1151 | + style={{ width: `${updateState.downloadPercent}%` }} |
| 1152 | + /> |
| 1153 | + </div> |
| 1154 | + </div> |
| 1155 | + ) : null} |
1006 | 1156 | </div> |
1007 | 1157 | </section> |
1008 | 1158 | </div> |
|
0 commit comments