mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 03:03:19 +08:00
fix(desktop): smoother update UX
This commit is contained in:
@@ -14,14 +14,32 @@ const UpdateBanner: React.FC = () => {
|
||||
const status = state.status
|
||||
const errored = status?.state === 'error'
|
||||
|
||||
// Full-screen "installing…" overlay: bridges the otherwise blank window
|
||||
// between clicking "restart to install" and the app actually quitting to
|
||||
// swap the bundle. (The gap AFTER quit, before relaunch, is OS-level and
|
||||
// can't be covered.)
|
||||
if (state.installing) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex flex-col items-center justify-center gap-3 bg-base/90 backdrop-blur-sm">
|
||||
<Loader2 size={28} className="animate-spin text-accent" />
|
||||
<p className="text-sm text-content-secondary">{t('update_installing')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Show the panel when it's open AND we either know of an update or hit an
|
||||
// error. Keeping it up on error is important: a failed download must surface
|
||||
// a visible message instead of silently doing nothing.
|
||||
if (!open || (!available && !errored)) return null
|
||||
|
||||
const version = state.version
|
||||
const preparing = state.preparing
|
||||
const downloading = status?.state === 'downloading'
|
||||
const downloaded = status?.state === 'downloaded'
|
||||
// macOS emits a second progress pass (verify) after hitting 100%; show it as
|
||||
// an indeterminate "verifying" state rather than a bar restarting from 0.
|
||||
const verifying = downloading && state.progressPeaked
|
||||
const busy = preparing || downloading
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-2 left-2 right-2 z-40">
|
||||
@@ -62,7 +80,21 @@ const UpdateBanner: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!errored && downloading && (
|
||||
{!errored && preparing && (
|
||||
<div className="flex items-center gap-2 text-xs text-content-secondary py-1">
|
||||
<Loader2 size={13} className="animate-spin" />
|
||||
<span>{t('update_preparing')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!errored && downloading && verifying && (
|
||||
<div className="flex items-center gap-2 text-xs text-content-secondary py-1">
|
||||
<Loader2 size={13} className="animate-spin" />
|
||||
<span>{t('update_verifying')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!errored && downloading && !verifying && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-xs text-content-secondary">
|
||||
<Loader2 size={13} className="animate-spin" />
|
||||
@@ -74,7 +106,7 @@ const UpdateBanner: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!errored && !downloading && !downloaded && (
|
||||
{!errored && !busy && !downloaded && (
|
||||
<button
|
||||
onClick={() => state.download()}
|
||||
className="w-full inline-flex items-center justify-center gap-2 rounded-btn bg-accent text-accent-contrast hover:bg-accent-hover px-3 py-2 text-[13px] font-medium cursor-pointer transition-colors"
|
||||
|
||||
@@ -72,6 +72,9 @@ const translations: Record<string, Record<string, string>> = {
|
||||
update_available: '发现新版本',
|
||||
update_download: '下载更新',
|
||||
update_downloading: '正在下载',
|
||||
update_preparing: '正在准备…',
|
||||
update_verifying: '正在校验,即将完成…',
|
||||
update_installing: '正在安装并重启,请稍候…',
|
||||
update_restart: '重启以更新',
|
||||
update_later: '稍后',
|
||||
update_latest: '已是最新版本',
|
||||
@@ -444,6 +447,9 @@ const translations: Record<string, Record<string, string>> = {
|
||||
update_available: 'New version available',
|
||||
update_download: 'Download update',
|
||||
update_downloading: 'Downloading',
|
||||
update_preparing: 'Preparing…',
|
||||
update_verifying: 'Verifying, almost done…',
|
||||
update_installing: 'Installing and restarting, please wait…',
|
||||
update_restart: 'Restart to update',
|
||||
update_later: 'Later',
|
||||
update_latest: 'You are up to date',
|
||||
|
||||
@@ -155,9 +155,11 @@ const NavRail: React.FC<NavRailProps> = ({ onLangChange }) => {
|
||||
|
||||
const checkUpdate = () => {
|
||||
setCheckedManually(true)
|
||||
// Re-open the update panel if an update is already known; also kicks a
|
||||
// fresh check. Closing the menu so the re-opened panel is visible.
|
||||
setMenuOpen(false)
|
||||
// If an update is already known, recheck() re-opens its panel, so close the
|
||||
// menu to reveal it. Otherwise keep the menu OPEN: the "up to date" result
|
||||
// shows inline as the menu label — closing it (which resets checkedManually)
|
||||
// is exactly what made the box flash and never show "up to date".
|
||||
if (availableUpdate) setMenuOpen(false)
|
||||
updateState.recheck()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,18 @@ interface UpdateState {
|
||||
version: string | null
|
||||
/** Download progress 0-100 while state === 'downloading'. */
|
||||
percent: number
|
||||
/** User clicked "download" but no progress event has arrived yet. Gives the
|
||||
* button an instant busy state so it can't be clicked again during the 1-2s
|
||||
* lead-up to the first progress event. Cleared on the first 'downloading'. */
|
||||
preparing: boolean
|
||||
/** The download progress already reached ~100% once. macOS (Squirrel.Mac)
|
||||
* emits a SECOND progress pass (verify / block-map) after the first, which
|
||||
* used to make the bar visibly restart from 0. Once peaked we render an
|
||||
* indeterminate "verifying" state instead of a confusing second bar. */
|
||||
progressPeaked: boolean
|
||||
/** User clicked "restart to install"; show a full-screen "installing…"
|
||||
* overlay for the brief window before the app quits to swap the bundle. */
|
||||
installing: boolean
|
||||
/** User dismissed the badge for this version (don't nag again until next). */
|
||||
dismissedVersion: string | null
|
||||
/** Whether the update panel is currently shown. Lifted here so the "check
|
||||
@@ -33,15 +45,27 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
||||
status: null,
|
||||
version: null,
|
||||
percent: 0,
|
||||
preparing: false,
|
||||
progressPeaked: false,
|
||||
installing: false,
|
||||
dismissedVersion: null,
|
||||
panelOpen: false,
|
||||
|
||||
setStatus: (s) =>
|
||||
set(() => {
|
||||
set((st) => {
|
||||
// A newly detected version auto-opens the panel.
|
||||
if (s.state === 'available') return { status: s, version: s.version, percent: 0, panelOpen: true }
|
||||
if (s.state === 'downloading') return { status: s, percent: s.percent }
|
||||
if (s.state === 'downloaded') return { status: s, version: s.version, percent: 100 }
|
||||
if (s.state === 'available')
|
||||
return { status: s, version: s.version, percent: 0, preparing: false, progressPeaked: false, panelOpen: true }
|
||||
if (s.state === 'downloading') {
|
||||
// First real progress event clears the "preparing" busy state. Track
|
||||
// when we've hit ~100% so the Squirrel.Mac second pass renders as an
|
||||
// indeterminate "verifying" state instead of a bar restarting from 0.
|
||||
const peaked = st.progressPeaked || s.percent >= 99
|
||||
return { status: s, percent: s.percent, preparing: false, progressPeaked: peaked }
|
||||
}
|
||||
if (s.state === 'downloaded')
|
||||
return { status: s, version: s.version, percent: 100, preparing: false }
|
||||
if (s.state === 'error') return { status: s, preparing: false, installing: false }
|
||||
return { status: s }
|
||||
}),
|
||||
|
||||
@@ -50,18 +74,29 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
||||
closePanel: () => set({ panelOpen: false }),
|
||||
|
||||
recheck: () => {
|
||||
// Open the panel and clear any dismiss so the result of this explicit check
|
||||
// is always visible — available, error, or (via the menu label) up-to-date.
|
||||
// Previously the panel only opened when an update was already known, so a
|
||||
// download/check error rendered nothing and looked like a dead click.
|
||||
set({ dismissedVersion: null, panelOpen: true })
|
||||
// Clear any dismiss so a known update surfaces again. Only re-open the panel
|
||||
// when an update actually exists — if we're already up to date, opening the
|
||||
// panel would just flash it (the banner renders nothing for not-available)
|
||||
// and the "up to date" feedback belongs in the menu, not a panel. The menu
|
||||
// (NavRail) decides whether to close itself + show the panel based on this.
|
||||
const known = hasAvailableUpdate(get())
|
||||
set({ dismissedVersion: null, panelOpen: known })
|
||||
// Always kick a fresh check too (picks up newer versions / recovers errors).
|
||||
// Pass the UI language so downloads route to the China CDN / R2 accordingly.
|
||||
window.electronAPI?.checkForUpdate?.(getLang())
|
||||
},
|
||||
|
||||
download: () => window.electronAPI?.downloadUpdate?.(getLang()),
|
||||
install: () => window.electronAPI?.installUpdate?.(),
|
||||
download: () => {
|
||||
// Enter a busy state immediately so the button can't be clicked twice while
|
||||
// we wait (1-2s) for the first download-progress event to arrive.
|
||||
set({ preparing: true, progressPeaked: false })
|
||||
window.electronAPI?.downloadUpdate?.(getLang())
|
||||
},
|
||||
install: () => {
|
||||
// Show the "installing…" overlay before the app quits to swap the bundle.
|
||||
set({ installing: true })
|
||||
window.electronAPI?.installUpdate?.()
|
||||
},
|
||||
}))
|
||||
|
||||
// Subscribe to main-process update events. Returns an unsubscribe fn.
|
||||
|
||||
Reference in New Issue
Block a user