mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-19 12:47:25 +08:00
fix(desktop): make update badge/panel re-openable
This commit is contained in:
@@ -131,6 +131,15 @@ export function initUpdater(windowGetter: () => BrowserWindow | null): void {
|
|||||||
// reply "not-available" so the menu can show "up to date".
|
// reply "not-available" so the menu can show "up to date".
|
||||||
export function checkForUpdates(): void {
|
export function checkForUpdates(): void {
|
||||||
if (!app.isPackaged) {
|
if (!app.isPackaged) {
|
||||||
|
// Dev-only UI harness: set COW_MOCK_UPDATE=1 to simulate an available
|
||||||
|
// update so the update panel/menu interactions can be exercised in
|
||||||
|
// `npm run dev` (where there's no real feed). Never runs in a packaged app.
|
||||||
|
if (process.env.COW_MOCK_UPDATE) {
|
||||||
|
const version = process.env.COW_MOCK_UPDATE_VERSION || '9.9.9'
|
||||||
|
log(`checkForUpdates: not packaged, MOCK available version=${version}`)
|
||||||
|
send({ state: 'available', version })
|
||||||
|
return
|
||||||
|
}
|
||||||
log('checkForUpdates: not packaged, replying not-available')
|
log('checkForUpdates: not packaged, replying not-available')
|
||||||
send({ state: 'not-available' })
|
send({ state: 'not-available' })
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,44 +1,27 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React from 'react'
|
||||||
import { Download, RefreshCw, X, Loader2 } from 'lucide-react'
|
import { Download, RefreshCw, X, Loader2 } from 'lucide-react'
|
||||||
import { t } from '../i18n'
|
import { t } from '../i18n'
|
||||||
import { useUpdateStore, hasPendingUpdate } from '../store/updateStore'
|
import { useUpdateStore, hasAvailableUpdate } from '../store/updateStore'
|
||||||
|
|
||||||
// Compact update panel anchored to the NavRail footer. Only mounts content
|
// Compact update panel anchored to the NavRail footer. Shown whenever an update
|
||||||
// when there's a pending update; otherwise renders nothing so it stays out of
|
// is available AND the panel is open (auto-opened on detection, re-openable via
|
||||||
// the way until electron-updater reports a new version.
|
// "check for update"). Dismissing (×) just closes it; the menu can re-open it.
|
||||||
const UpdateBanner: React.FC = () => {
|
const UpdateBanner: React.FC = () => {
|
||||||
const state = useUpdateStore()
|
const state = useUpdateStore()
|
||||||
const [open, setOpen] = useState(false)
|
const open = state.panelOpen
|
||||||
|
|
||||||
const pending = hasPendingUpdate(state)
|
const available = hasAvailableUpdate(state)
|
||||||
const status = state.status
|
const status = state.status
|
||||||
|
|
||||||
// Auto-open the panel the moment a new version is first detected.
|
// Render nothing when there's no update, or the panel is closed (dismissed).
|
||||||
useEffect(() => {
|
if (!available || !open) return null
|
||||||
if (status?.state === 'available') setOpen(true)
|
|
||||||
}, [status?.state])
|
|
||||||
|
|
||||||
if (!pending) return null
|
|
||||||
|
|
||||||
const version = state.version
|
const version = state.version
|
||||||
const downloading = status?.state === 'downloading'
|
const downloading = status?.state === 'downloading'
|
||||||
const downloaded = status?.state === 'downloaded'
|
const downloaded = status?.state === 'downloaded'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-14 left-2 right-2 z-40">
|
<div className="absolute bottom-2 left-2 right-2 z-40">
|
||||||
{/* Collapsed pill: a red-dotted button that re-opens the panel. */}
|
|
||||||
{!open && (
|
|
||||||
<button
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
className="relative w-full flex items-center gap-2 rounded-btn bg-accent-soft text-accent px-3 py-2 text-[13px] font-medium cursor-pointer hover:bg-accent-soft/80 transition-colors"
|
|
||||||
>
|
|
||||||
<span className="absolute -top-1 -left-1 h-2 w-2 rounded-full bg-danger" />
|
|
||||||
<Download size={15} />
|
|
||||||
<span className="truncate">{t('update_available')}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{open && (
|
|
||||||
<div className="rounded-lg border border-default bg-elevated shadow-lg p-3 space-y-2.5">
|
<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="flex items-start justify-between gap-2">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
@@ -46,10 +29,7 @@ const UpdateBanner: React.FC = () => {
|
|||||||
{version && <p className="text-xs text-content-tertiary mt-0.5">v{version}</p>}
|
{version && <p className="text-xs text-content-tertiary mt-0.5">v{version}</p>}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => state.dismiss()}
|
||||||
setOpen(false)
|
|
||||||
state.dismiss()
|
|
||||||
}}
|
|
||||||
className="text-content-tertiary hover:text-content cursor-pointer flex-shrink-0"
|
className="text-content-tertiary hover:text-content cursor-pointer flex-shrink-0"
|
||||||
title={t('update_later')}
|
title={t('update_later')}
|
||||||
>
|
>
|
||||||
@@ -89,7 +69,6 @@ const UpdateBanner: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { t, getLang, setLang, Lang } from '../i18n'
|
|||||||
import { useUIStore } from '../store/uiStore'
|
import { useUIStore } from '../store/uiStore'
|
||||||
import { useTheme } from '../hooks/useTheme'
|
import { useTheme } from '../hooks/useTheme'
|
||||||
import { usePlatform } from '../hooks/usePlatform'
|
import { usePlatform } from '../hooks/usePlatform'
|
||||||
import { useUpdateStore, hasPendingUpdate } from '../store/updateStore'
|
import { useUpdateStore, hasPendingUpdate, hasAvailableUpdate } from '../store/updateStore'
|
||||||
import UpdateBanner from '../components/UpdateBanner'
|
import UpdateBanner from '../components/UpdateBanner'
|
||||||
|
|
||||||
// Fallback shown when app.getVersion() is unavailable (dev/web preview). Keep
|
// Fallback shown when app.getVersion() is unavailable (dev/web preview). Keep
|
||||||
@@ -83,7 +83,11 @@ const NavRail: React.FC<NavRailProps> = ({ onLangChange }) => {
|
|||||||
const width = collapsed ? 'w-[56px]' : 'w-[208px]'
|
const width = collapsed ? 'w-[56px]' : 'w-[208px]'
|
||||||
|
|
||||||
const updateState = useUpdateStore()
|
const updateState = useUpdateStore()
|
||||||
|
// Footer dot: hidden once dismissed for this version (user asked for this).
|
||||||
const pendingUpdate = hasPendingUpdate(updateState)
|
const pendingUpdate = hasPendingUpdate(updateState)
|
||||||
|
// Menu "check for update" dot: stays as long as an update actually exists,
|
||||||
|
// even after dismissing the footer badge.
|
||||||
|
const availableUpdate = hasAvailableUpdate(updateState)
|
||||||
const checking = updateState.status?.state === 'checking'
|
const checking = updateState.status?.state === 'checking'
|
||||||
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
@@ -148,7 +152,10 @@ const NavRail: React.FC<NavRailProps> = ({ onLangChange }) => {
|
|||||||
|
|
||||||
const checkUpdate = () => {
|
const checkUpdate = () => {
|
||||||
setCheckedManually(true)
|
setCheckedManually(true)
|
||||||
window.electronAPI?.checkForUpdate?.()
|
// 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)
|
||||||
|
updateState.recheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -211,8 +218,8 @@ const NavRail: React.FC<NavRailProps> = ({ onLangChange }) => {
|
|||||||
<FooterMenu
|
<FooterMenu
|
||||||
theme={theme}
|
theme={theme}
|
||||||
checking={checking}
|
checking={checking}
|
||||||
pendingUpdate={pendingUpdate}
|
pendingUpdate={availableUpdate}
|
||||||
upToDate={checkedManually && updateStatusState === 'not-available'}
|
upToDate={checkedManually && updateStatusState === 'not-available' && !availableUpdate}
|
||||||
onLogs={() => {
|
onLogs={() => {
|
||||||
setMenuOpen(false)
|
setMenuOpen(false)
|
||||||
navigate('/logs')
|
navigate('/logs')
|
||||||
|
|||||||
@@ -9,9 +9,19 @@ interface UpdateState {
|
|||||||
percent: number
|
percent: number
|
||||||
/** 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
|
||||||
|
* for update" menu item can re-open it on demand. */
|
||||||
|
panelOpen: boolean
|
||||||
|
|
||||||
setStatus: (s: UpdateStatus) => void
|
setStatus: (s: UpdateStatus) => void
|
||||||
|
/** Dismiss the floating badge/panel for the current version (footer dot goes
|
||||||
|
* away), but keep the update itself known so the menu can still surface it. */
|
||||||
dismiss: () => void
|
dismiss: () => void
|
||||||
|
openPanel: () => void
|
||||||
|
closePanel: () => void
|
||||||
|
/** User explicitly clicked "check for update": ask main to re-check, and if
|
||||||
|
* an update is already known, re-open the panel immediately (undismiss). */
|
||||||
|
recheck: () => void
|
||||||
|
|
||||||
// Actions proxied to the main process via the preload bridge.
|
// Actions proxied to the main process via the preload bridge.
|
||||||
download: () => void
|
download: () => void
|
||||||
@@ -23,16 +33,32 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
version: null,
|
version: null,
|
||||||
percent: 0,
|
percent: 0,
|
||||||
dismissedVersion: null,
|
dismissedVersion: null,
|
||||||
|
panelOpen: false,
|
||||||
|
|
||||||
setStatus: (s) =>
|
setStatus: (s) =>
|
||||||
set(() => {
|
set(() => {
|
||||||
if (s.state === 'available') return { status: s, version: s.version, percent: 0 }
|
// 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 === 'downloading') return { status: s, percent: s.percent }
|
||||||
if (s.state === 'downloaded') return { status: s, version: s.version, percent: 100 }
|
if (s.state === 'downloaded') return { status: s, version: s.version, percent: 100 }
|
||||||
return { status: s }
|
return { status: s }
|
||||||
}),
|
}),
|
||||||
|
|
||||||
dismiss: () => set((st) => ({ dismissedVersion: st.version })),
|
dismiss: () => set((st) => ({ dismissedVersion: st.version, panelOpen: false })),
|
||||||
|
openPanel: () => set({ panelOpen: true }),
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
// Always kick a fresh check too (picks up newer versions / recovers errors).
|
||||||
|
window.electronAPI?.checkForUpdate?.()
|
||||||
|
},
|
||||||
|
|
||||||
download: () => window.electronAPI?.downloadUpdate?.(),
|
download: () => window.electronAPI?.downloadUpdate?.(),
|
||||||
install: () => window.electronAPI?.installUpdate?.(),
|
install: () => window.electronAPI?.installUpdate?.(),
|
||||||
@@ -45,11 +71,18 @@ export function initUpdateListener(): (() => void) | undefined {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether a new version should be surfaced (available/downloading/downloaded
|
// Whether an update exists at all (available/downloading/downloaded),
|
||||||
// and not dismissed for that version).
|
// regardless of dismiss. Drives the "check for update" menu item's dot, which
|
||||||
export function hasPendingUpdate(state: UpdateState): boolean {
|
// should persist as long as an update is actually available.
|
||||||
|
export function hasAvailableUpdate(state: UpdateState): boolean {
|
||||||
const s = state.status
|
const s = state.status
|
||||||
if (!s) return false
|
if (!s) return false
|
||||||
const active = s.state === 'available' || s.state === 'downloading' || s.state === 'downloaded'
|
return s.state === 'available' || s.state === 'downloading' || s.state === 'downloaded'
|
||||||
return active && state.dismissedVersion !== state.version
|
}
|
||||||
|
|
||||||
|
// Whether a new version should be surfaced in the floating footer badge:
|
||||||
|
// available and not dismissed for that version. Dismissing hides only this,
|
||||||
|
// not the menu dot (hasAvailableUpdate).
|
||||||
|
export function hasPendingUpdate(state: UpdateState): boolean {
|
||||||
|
return hasAvailableUpdate(state) && state.dismissedVersion !== state.version
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user