feat: about tab with update check/download (Gitea feed first, GitHub fallback)

- updater: dual-feed state machine, manual check/download/install IPC,
  changelog from Gitea releases API with GitHub fallback
- settings: new About tab (version, channel, auto-check toggle, progress)
- system.autoCheckUpdate setting gates the startup check
This commit is contained in:
Bill committed 2026-09-07 17:18:15 +08:00
1 parent a9acdbe500
commit f25a1627f3
11 files changed
+417 -46

No files matched your search

+6 -1
View File
@@ -84,7 +84,12 @@ if (typeof window !== 'undefined' && !window.api) {
listLayouts: async () => [],
getLayout: async () => null,
saveLayout: async () => undefined,
deleteLayout: async () => undefined
deleteLayout: async () => undefined,
updateCheck: async () => ({ status: 'dev' as const, currentVersion: 'dev' }),
updateDownload: async () => undefined,
updateInstall: noop,
updateChangelog: async () => [],
onUpdateState: () => noop
}
;(window as unknown as { api: AppApi }).api = api
}
+105
View File
@@ -0,0 +1,105 @@
import { useEffect, useState } from 'react'
import { Button, Progress, Select, Switch, Tag } from 'antd'
import type { AppInfo, ReleaseNote, UpdateState, UpdateStatus } from '@shared/ipc'
import { useSettingsStore } from './store'
const STATUS_TEXT: Record<UpdateStatus, string> = {
idle: '点击检查更新',
checking: '正在检查更新…',
available: '发现新版本',
latest: '已经是最新版本',
downloading: '正在下载…',
downloaded: '下载完成,可重启安装',
error: '检查失败',
dev: '开发模式不支持更新检查'
}
/** 关于页:版本信息 + 更新检查/下载/安装 + 更新日志(国内源优先,GitHub 兜底)。 */
export function AboutTab(): React.JSX.Element {
const [version, setVersion] = useState('')
const [state, setState] = useState<UpdateState | null>(null)
const [notes, setNotes] = useState<ReleaseNote[]>([])
const updateSystem = useSettingsStore((s) => s.updateSystem)
const autoCheck = useSettingsStore((s) => s.settings.system.autoCheckUpdate !== false)
useEffect(() => {
void window.api.appInfo().then((i: AppInfo) => setVersion(i.appVersion))
void window.api.updateChangelog().then(setNotes).catch(() => undefined)
return window.api.onUpdateState(setState)
}, [])
const status = state?.status ?? 'idle'
const statusText = STATUS_TEXT[status] + (state?.version ? ` v${state.version}` : '')
return (
<div className="about-tab">
<div className="about-header">
<div className="about-logo">OT</div>
<div>
<div className="about-name">OpenTerminal</div>
<div className="about-version">
当前版本: {version || '…'}{' '}
<span className={'about-status' + (status === 'available' ? ' is-new' : '')}>
{statusText}
</span>
{state?.error && <div className="about-error">{state.error}</div>}
</div>
</div>
</div>
<div className="about-controls">
<Select
value="stable"
options={[{ value: 'stable', label: '稳定更新通道' }]}
style={{ width: 150 }}
/>
<label className="about-autocheck">
<Switch
size="small"
checked={autoCheck}
onChange={(checked) => void updateSystem({ autoCheckUpdate: checked })}
/>
启动时自动检查
</label>
{status === 'available' && (
<Button type="primary" onClick={() => void window.api.updateDownload()}>
下载更新{state?.feed === 'github' ? '(GitHub 源)' : ''}
</Button>
)}
{status === 'downloaded' && (
<Button type="primary" onClick={() => window.api.updateInstall()}>
重启安装
</Button>
)}
{(status === 'idle' || status === 'latest' || status === 'error') && (
<Button type="primary" onClick={() => void window.api.updateCheck()}>
检查更新
</Button>
)}
{status === 'checking' && (
<Button type="primary" loading disabled>
检查中
</Button>
)}
</div>
{status === 'downloading' && (
<Progress percent={state?.percent ?? 0} size="small" status="active" />
)}
<div className="about-changelog">
<div className="about-changelog-title">更新日志</div>
{notes.length === 0 && <div className="about-changelog-empty">暂无更新日志</div>}
{notes.map((n) => (
<div key={n.version} className="about-release">
<div className="about-release-head">
<Tag color="green">{n.version}</Tag>
<span className="about-release-date">{n.date}</span>
</div>
<div className="about-release-body">{n.body}</div>
</div>
))}
</div>
</div>
)
}
+3 -1
View File
@@ -6,6 +6,7 @@ import { useSettingsStore } from './store'
import { CursorSettingsTab, FontSettingsTab, RenderSettingsTab, SystemSettingsTab } from './SettingsTabs'
import { ThemeSettingsTab } from './ThemeSettingsTab'
import { HighlightTab } from './HighlightTab'
import { AboutTab } from './AboutTab'
import { ThemeEditor } from '../theme/editor/ThemeEditor'
import './settings.css'
@@ -35,7 +36,8 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps): React.JS
/>
)
},
{ key: 'system', label: '系统', children: <SystemSettingsTab /> }
{ key: 'system', label: '系统', children: <SystemSettingsTab /> },
{ key: 'about', label: '关于', children: <AboutTab /> }
]
const closeEditor = (): void => setEditorState((prev) => (prev.open ? { ...prev, open: false } : prev))
+100 -1
View File
@@ -343,4 +343,103 @@
line-height: 1.6;
overflow: hidden;
text-overflow: ellipsis;
}
}
/* ---- about / updater tab ---- */
.about-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 18px;
}
.about-logo {
width: 48px;
height: 48px;
border-radius: 12px;
background: #1e2531;
color: #3fd06a;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 18px;
}
.about-name {
font-size: 16px;
font-weight: 600;
}
.about-version {
font-size: 12px;
opacity: 0.7;
margin-top: 3px;
}
.about-status {
margin-left: 10px;
}
.about-status.is-new {
color: #3fd06a;
}
.about-error {
color: #f85149;
margin-top: 4px;
font-size: 12px;
word-break: break-all;
}
.about-controls {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 18px;
}
.about-autocheck {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
opacity: 0.85;
}
.about-changelog {
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding-top: 12px;
}
.about-changelog-title {
font-weight: 600;
margin-bottom: 10px;
}
.about-changelog-empty {
opacity: 0.5;
font-size: 12px;
}
.about-release {
margin-bottom: 14px;
}
.about-release-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.about-release-date {
font-size: 12px;
opacity: 0.6;
}
.about-release-body {
font-size: 12px;
opacity: 0.85;
white-space: pre-wrap;
line-height: 1.6;
}