mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 03:03:19 +08:00
fix(desktop): auto-update install now actually replaces the app
This commit is contained in:
@@ -253,7 +253,14 @@ function setupIPC() {
|
||||
setUpdateLanguage(lang)
|
||||
startDownload()
|
||||
})
|
||||
ipcMain.handle('update-install', () => quitAndInstall())
|
||||
ipcMain.handle('update-install', () => {
|
||||
// Let the window actually close so the app can fully quit — otherwise the
|
||||
// close-to-tray handler preventDefault()s it, the process stays alive, and
|
||||
// Squirrel.Mac can't swap the app bundle (the update silently no-ops and
|
||||
// relaunching still shows the old version).
|
||||
isQuitting = true
|
||||
quitAndInstall()
|
||||
})
|
||||
|
||||
// Synchronous OS locale lookup (e.g. "zh-CN", "en-US"). Used by the renderer
|
||||
// to pick a sensible default UI language on first run before any paint.
|
||||
|
||||
@@ -229,6 +229,16 @@ function attemptDownload(): void {
|
||||
export function quitAndInstall(): void {
|
||||
if (!app.isPackaged) return
|
||||
log('quitAndInstall: relaunching to install update')
|
||||
// isSilent=false (show installer), isForceRunAfter=true (relaunch after).
|
||||
autoUpdater.quitAndInstall(false, true)
|
||||
// isSilent MUST be true on Windows: our NSIS installer is an "assisted"
|
||||
// installer (oneClick:false + allowToChangeInstallationDirectory), so a
|
||||
// non-silent update re-shows the install-dir / install-mode wizard instead of
|
||||
// updating in place. Silent skips the wizard and updates directly. macOS
|
||||
// (Squirrel.Mac) ignores isSilent, so false there is fine.
|
||||
// isForceRunAfter=true relaunches the app once the install finishes.
|
||||
// Drop window-all-closed handlers first: with an assisted NSIS installer a
|
||||
// lingering handler can keep the process alive and stop the installer from
|
||||
// replacing files / relaunching (a documented electron-updater gotcha).
|
||||
app.removeAllListeners('window-all-closed')
|
||||
const isSilent = process.platform === 'win32'
|
||||
autoUpdater.quitAndInstall(isSilent, true)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Download, RefreshCw, X, Loader2 } from 'lucide-react'
|
||||
import { Download, RefreshCw, X, Loader2, AlertTriangle } from 'lucide-react'
|
||||
import { t } from '../i18n'
|
||||
import { useUpdateStore, hasAvailableUpdate } from '../store/updateStore'
|
||||
|
||||
@@ -12,9 +12,12 @@ const UpdateBanner: React.FC = () => {
|
||||
|
||||
const available = hasAvailableUpdate(state)
|
||||
const status = state.status
|
||||
const errored = status?.state === 'error'
|
||||
|
||||
// Render nothing when there's no update, or the panel is closed (dismissed).
|
||||
if (!available || !open) return null
|
||||
// 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 downloading = status?.state === 'downloading'
|
||||
@@ -25,8 +28,12 @@ const UpdateBanner: React.FC = () => {
|
||||
<div className="rounded-lg border border-default bg-elevated shadow-lg p-3 space-y-2.5">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[13px] font-semibold text-content">{t('update_available')}</p>
|
||||
{version && <p className="text-xs text-content-tertiary mt-0.5">v{version}</p>}
|
||||
<p className="text-[13px] font-semibold text-content">
|
||||
{errored ? t('update_failed') : t('update_available')}
|
||||
</p>
|
||||
{!errored && version && (
|
||||
<p className="text-xs text-content-tertiary mt-0.5">v{version}</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => state.dismiss()}
|
||||
@@ -37,7 +44,25 @@ const UpdateBanner: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{downloading && (
|
||||
{errored && (
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-start gap-2 text-xs text-content-secondary">
|
||||
<AlertTriangle size={13} className="text-amber-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="break-words">
|
||||
{status?.state === 'error' ? status.message : ''}
|
||||
</span>
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
<RefreshCw size={15} />
|
||||
{t('update_retry')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!errored && downloading && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-xs text-content-secondary">
|
||||
<Loader2 size={13} className="animate-spin" />
|
||||
@@ -49,7 +74,7 @@ const UpdateBanner: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!downloading && !downloaded && (
|
||||
{!errored && !downloading && !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"
|
||||
@@ -59,7 +84,7 @@ const UpdateBanner: React.FC = () => {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{downloaded && (
|
||||
{!errored && downloaded && (
|
||||
<button
|
||||
onClick={() => state.install()}
|
||||
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"
|
||||
|
||||
@@ -77,6 +77,8 @@ const translations: Record<string, Record<string, string>> = {
|
||||
update_latest: '已是最新版本',
|
||||
update_check: '检查更新',
|
||||
update_checking: '正在检查…',
|
||||
update_failed: '更新失败',
|
||||
update_retry: '重试',
|
||||
menu_more: '更多',
|
||||
menu_theme_light: '浅色模式',
|
||||
menu_theme_dark: '深色模式',
|
||||
@@ -447,6 +449,8 @@ const translations: Record<string, Record<string, string>> = {
|
||||
update_latest: 'You are up to date',
|
||||
update_check: 'Check for updates',
|
||||
update_checking: 'Checking…',
|
||||
update_failed: 'Update failed',
|
||||
update_retry: 'Retry',
|
||||
menu_more: 'More',
|
||||
menu_theme_light: 'Light mode',
|
||||
menu_theme_dark: 'Dark mode',
|
||||
|
||||
@@ -50,13 +50,11 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
||||
closePanel: () => set({ panelOpen: false }),
|
||||
|
||||
recheck: () => {
|
||||
const st = get()
|
||||
// If we already know about an available/downloaded update, just re-open the
|
||||
// panel (clearing the dismiss for this version) instead of waiting on a
|
||||
// network round-trip — the user asked to see it.
|
||||
if (hasAvailableUpdate(st)) {
|
||||
set({ dismissedVersion: null, panelOpen: true })
|
||||
}
|
||||
// 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 })
|
||||
// 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())
|
||||
|
||||
Reference in New Issue
Block a user