mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +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 status = state.status
|
||||||
const errored = status?.state === 'error'
|
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
|
// 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
|
// error. Keeping it up on error is important: a failed download must surface
|
||||||
// a visible message instead of silently doing nothing.
|
// a visible message instead of silently doing nothing.
|
||||||
if (!open || (!available && !errored)) return null
|
if (!open || (!available && !errored)) return null
|
||||||
|
|
||||||
const version = state.version
|
const version = state.version
|
||||||
|
const preparing = state.preparing
|
||||||
const downloading = status?.state === 'downloading'
|
const downloading = status?.state === 'downloading'
|
||||||
const downloaded = status?.state === 'downloaded'
|
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 (
|
return (
|
||||||
<div className="absolute bottom-2 left-2 right-2 z-40">
|
<div className="absolute bottom-2 left-2 right-2 z-40">
|
||||||
@@ -62,7 +80,21 @@ const UpdateBanner: React.FC = () => {
|
|||||||
</div>
|
</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="space-y-1">
|
||||||
<div className="flex items-center gap-2 text-xs text-content-secondary">
|
<div className="flex items-center gap-2 text-xs text-content-secondary">
|
||||||
<Loader2 size={13} className="animate-spin" />
|
<Loader2 size={13} className="animate-spin" />
|
||||||
@@ -74,7 +106,7 @@ const UpdateBanner: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!errored && !downloading && !downloaded && (
|
{!errored && !busy && !downloaded && (
|
||||||
<button
|
<button
|
||||||
onClick={() => state.download()}
|
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"
|
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_available: '发现新版本',
|
||||||
update_download: '下载更新',
|
update_download: '下载更新',
|
||||||
update_downloading: '正在下载',
|
update_downloading: '正在下载',
|
||||||
|
update_preparing: '正在准备…',
|
||||||
|
update_verifying: '正在校验,即将完成…',
|
||||||
|
update_installing: '正在安装并重启,请稍候…',
|
||||||
update_restart: '重启以更新',
|
update_restart: '重启以更新',
|
||||||
update_later: '稍后',
|
update_later: '稍后',
|
||||||
update_latest: '已是最新版本',
|
update_latest: '已是最新版本',
|
||||||
@@ -444,6 +447,9 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
update_available: 'New version available',
|
update_available: 'New version available',
|
||||||
update_download: 'Download update',
|
update_download: 'Download update',
|
||||||
update_downloading: 'Downloading',
|
update_downloading: 'Downloading',
|
||||||
|
update_preparing: 'Preparing…',
|
||||||
|
update_verifying: 'Verifying, almost done…',
|
||||||
|
update_installing: 'Installing and restarting, please wait…',
|
||||||
update_restart: 'Restart to update',
|
update_restart: 'Restart to update',
|
||||||
update_later: 'Later',
|
update_later: 'Later',
|
||||||
update_latest: 'You are up to date',
|
update_latest: 'You are up to date',
|
||||||
|
|||||||
@@ -155,9 +155,11 @@ const NavRail: React.FC<NavRailProps> = ({ onLangChange }) => {
|
|||||||
|
|
||||||
const checkUpdate = () => {
|
const checkUpdate = () => {
|
||||||
setCheckedManually(true)
|
setCheckedManually(true)
|
||||||
// Re-open the update panel if an update is already known; also kicks a
|
// If an update is already known, recheck() re-opens its panel, so close the
|
||||||
// fresh check. Closing the menu so the re-opened panel is visible.
|
// menu to reveal it. Otherwise keep the menu OPEN: the "up to date" result
|
||||||
setMenuOpen(false)
|
// 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()
|
updateState.recheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ interface UpdateState {
|
|||||||
version: string | null
|
version: string | null
|
||||||
/** Download progress 0-100 while state === 'downloading'. */
|
/** Download progress 0-100 while state === 'downloading'. */
|
||||||
percent: number
|
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). */
|
/** User dismissed the badge for this version (don't nag again until next). */
|
||||||
dismissedVersion: string | null
|
dismissedVersion: string | null
|
||||||
/** Whether the update panel is currently shown. Lifted here so the "check
|
/** 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,
|
status: null,
|
||||||
version: null,
|
version: null,
|
||||||
percent: 0,
|
percent: 0,
|
||||||
|
preparing: false,
|
||||||
|
progressPeaked: false,
|
||||||
|
installing: false,
|
||||||
dismissedVersion: null,
|
dismissedVersion: null,
|
||||||
panelOpen: false,
|
panelOpen: false,
|
||||||
|
|
||||||
setStatus: (s) =>
|
setStatus: (s) =>
|
||||||
set(() => {
|
set((st) => {
|
||||||
// A newly detected version auto-opens the panel.
|
// 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 === 'available')
|
||||||
if (s.state === 'downloading') return { status: s, percent: s.percent }
|
return { status: s, version: s.version, percent: 0, preparing: false, progressPeaked: false, panelOpen: true }
|
||||||
if (s.state === 'downloaded') return { status: s, version: s.version, percent: 100 }
|
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 }
|
return { status: s }
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -50,18 +74,29 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
closePanel: () => set({ panelOpen: false }),
|
closePanel: () => set({ panelOpen: false }),
|
||||||
|
|
||||||
recheck: () => {
|
recheck: () => {
|
||||||
// Open the panel and clear any dismiss so the result of this explicit check
|
// Clear any dismiss so a known update surfaces again. Only re-open the panel
|
||||||
// is always visible — available, error, or (via the menu label) up-to-date.
|
// when an update actually exists — if we're already up to date, opening the
|
||||||
// Previously the panel only opened when an update was already known, so a
|
// panel would just flash it (the banner renders nothing for not-available)
|
||||||
// download/check error rendered nothing and looked like a dead click.
|
// and the "up to date" feedback belongs in the menu, not a panel. The menu
|
||||||
set({ dismissedVersion: null, panelOpen: true })
|
// (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).
|
// 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.
|
// Pass the UI language so downloads route to the China CDN / R2 accordingly.
|
||||||
window.electronAPI?.checkForUpdate?.(getLang())
|
window.electronAPI?.checkForUpdate?.(getLang())
|
||||||
},
|
},
|
||||||
|
|
||||||
download: () => window.electronAPI?.downloadUpdate?.(getLang()),
|
download: () => {
|
||||||
install: () => window.electronAPI?.installUpdate?.(),
|
// 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.
|
// Subscribe to main-process update events. Returns an unsubscribe fn.
|
||||||
|
|||||||
Reference in New Issue
Block a user