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:
1 parent
a9acdbe500
commit
f25a1627f3
11 files changed
+416
-45
No files matched your search
+1
-1
@@ -5,7 +5,7 @@ import { registerIpc } from './ipc'
|
|||||||
import { killAllPtys } from './pty'
|
import { killAllPtys } from './pty'
|
||||||
import { applyStartupSystemSettings, loadSettings } from './settingsStore'
|
import { applyStartupSystemSettings, loadSettings } from './settingsStore'
|
||||||
import { initTray, markQuitting, onMainWindowClose } from './tray'
|
import { initTray, markQuitting, onMainWindowClose } from './tray'
|
||||||
import { configureAutoUpdater } from './updater'
|
import { configureAutoUpdater, registerUpdateIpc } from './updater'
|
||||||
import { applyWindowChrome } from './windowChrome'
|
import { applyWindowChrome } from './windowChrome'
|
||||||
import { getThemeById } from '@shared/theme'
|
import { getThemeById } from '@shared/theme'
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ const DEFAULT_SYSTEM: SystemSettings = {
|
|||||||
launchAtLogin: false,
|
launchAtLogin: false,
|
||||||
preventSleep: false,
|
preventSleep: false,
|
||||||
globalShowHide: '',
|
globalShowHide: '',
|
||||||
closeAction: 'ask'
|
closeAction: 'ask',
|
||||||
|
autoCheckUpdate: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const TERMINAL_KEYS = new Set(Object.keys(DEFAULT_SETTINGS.terminal) as (keyof TerminalSettings)[])
|
const TERMINAL_KEYS = new Set(Object.keys(DEFAULT_SETTINGS.terminal) as (keyof TerminalSettings)[])
|
||||||
@@ -91,7 +92,8 @@ function deepMerge(raw: unknown): { settings: AppSettings; errors: string[] } {
|
|||||||
launchAtLogin: candidate.launchAtLogin === true,
|
launchAtLogin: candidate.launchAtLogin === true,
|
||||||
preventSleep: candidate.preventSleep === true,
|
preventSleep: candidate.preventSleep === true,
|
||||||
globalShowHide,
|
globalShowHide,
|
||||||
closeAction
|
closeAction,
|
||||||
|
autoCheckUpdate: candidate.autoCheckUpdate !== false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-36
@@ -1,54 +1,153 @@
|
|||||||
import { app } from 'electron'
|
import { app, ipcMain, net } from 'electron'
|
||||||
import { autoUpdater } from 'electron-updater'
|
import { autoUpdater } from 'electron-updater'
|
||||||
|
import { Ipc, type ReleaseNote, type UpdateState } from '../shared/ipc'
|
||||||
|
import { broadcast } from './broadcast'
|
||||||
|
import { loadSettings } from './settingsStore'
|
||||||
|
import { markQuitting } from './tray'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default update feed (generic provider). The main agent uploads latest.yml and
|
* Update feeds: domestic Gitea generic package is tried first (fast in
|
||||||
* the NSIS installer to this stable path.
|
* China); GitHub releases is the fallback. Both feeds carry the NSIS .exe —
|
||||||
*
|
* electron-updater does NOT support MSI auto-updates, MSI is manual only.
|
||||||
* KNOWN LIMITATION: electron-updater does NOT support MSI auto-updates. The
|
|
||||||
* update channel always runs through the NSIS .exe (latest.yml + .exe are
|
|
||||||
* uploaded together). MSI is manual/enterprise distribution only.
|
|
||||||
*/
|
*/
|
||||||
const DEFAULT_FEED_URL =
|
const GITEA_FEED =
|
||||||
'https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/'
|
'https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/'
|
||||||
|
const GITEA_RELEASES_API =
|
||||||
|
'https://git.codingplan.site/api/v1/repos/admin/OpenTerminal/releases?limit=10'
|
||||||
|
const GITHUB_REPO = { owner: 'billowliu2', repo: 'OpenTerminal' }
|
||||||
|
const GITHUB_RELEASES_API =
|
||||||
|
'https://api.github.com/repos/billowliu2/OpenTerminal/releases?per_page=10'
|
||||||
|
|
||||||
|
let state: UpdateState = { status: 'idle', currentVersion: app.getVersion() }
|
||||||
|
let activeFeed: 'gitea' | 'github' = 'gitea'
|
||||||
|
let eventsWired = false
|
||||||
|
|
||||||
|
function setState(patch: Partial<UpdateState>): void {
|
||||||
|
state = { ...state, ...patch, currentVersion: app.getVersion() }
|
||||||
|
broadcast(Ipc.UPDATE_STATE, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useFeed(feed: 'gitea' | 'github'): void {
|
||||||
|
activeFeed = feed
|
||||||
|
if (feed === 'gitea') {
|
||||||
|
const token = process.env.OT_UPDATE_TOKEN
|
||||||
|
autoUpdater.setFeedURL({
|
||||||
|
provider: 'generic',
|
||||||
|
url: process.env.OT_UPDATE_URL || GITEA_FEED,
|
||||||
|
...(token ? { requestHeaders: { Authorization: `token ${token}` } } : {})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
autoUpdater.setFeedURL({ provider: 'github', ...GITHUB_REPO })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireEvents(): void {
|
||||||
|
if (eventsWired) return
|
||||||
|
eventsWired = true
|
||||||
|
autoUpdater.on('checking-for-update', () => setState({ status: 'checking', error: undefined }))
|
||||||
|
autoUpdater.on('update-available', (info) =>
|
||||||
|
setState({ status: 'available', version: info.version, feed: activeFeed, percent: undefined })
|
||||||
|
)
|
||||||
|
autoUpdater.on('update-not-available', () => setState({ status: 'latest', version: undefined }))
|
||||||
|
autoUpdater.on('download-progress', (p) =>
|
||||||
|
setState({ status: 'downloading', percent: Math.round(p.percent) })
|
||||||
|
)
|
||||||
|
autoUpdater.on('update-downloaded', (info) =>
|
||||||
|
setState({ status: 'downloaded', version: info.version, percent: undefined })
|
||||||
|
)
|
||||||
|
autoUpdater.on('error', (err) =>
|
||||||
|
setState({ status: 'error', error: err instanceof Error ? err.message : String(err) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check the active feed; on failure switch Gitea → GitHub and retry once. */
|
||||||
|
async function checkWithFallback(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await autoUpdater.checkForUpdates()
|
||||||
|
} catch (err) {
|
||||||
|
if (activeFeed !== 'gitea') throw err
|
||||||
|
console.warn('[updater] gitea feed failed, falling back to github:', err)
|
||||||
|
useFeed('github')
|
||||||
|
await autoUpdater.checkForUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCheck(): Promise<UpdateState> {
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
setState({ status: 'dev' })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
setState({ status: 'checking', error: undefined, percent: undefined })
|
||||||
|
try {
|
||||||
|
await checkWithFallback()
|
||||||
|
} catch (err) {
|
||||||
|
setState({ status: 'error', error: err instanceof Error ? err.message : String(err) })
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDownload(): Promise<void> {
|
||||||
|
if (!app.isPackaged) return
|
||||||
|
setState({ status: 'downloading', percent: 0 })
|
||||||
|
try {
|
||||||
|
await autoUpdater.downloadUpdate()
|
||||||
|
} catch (err) {
|
||||||
|
setState({ status: 'error', error: err instanceof Error ? err.message : String(err) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gitea releases first, GitHub releases as fallback; both are public reads. */
|
||||||
|
async function fetchChangelog(): Promise<ReleaseNote[]> {
|
||||||
|
for (const url of [GITEA_RELEASES_API, GITHUB_RELEASES_API]) {
|
||||||
|
try {
|
||||||
|
const resp = await net.fetch(url, { headers: { 'User-Agent': 'OpenTerminal' } })
|
||||||
|
if (!resp.ok) continue
|
||||||
|
const data = (await resp.json()) as Array<{
|
||||||
|
tag_name?: string
|
||||||
|
name?: string
|
||||||
|
published_at?: string
|
||||||
|
body?: string
|
||||||
|
}>
|
||||||
|
return data
|
||||||
|
.map((r) => ({
|
||||||
|
version: r.tag_name ?? r.name ?? '',
|
||||||
|
date: (r.published_at ?? '').slice(0, 10),
|
||||||
|
body: r.body ?? ''
|
||||||
|
}))
|
||||||
|
.filter((r) => r.version)
|
||||||
|
} catch {
|
||||||
|
// try next source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerUpdateIpc(): void {
|
||||||
|
ipcMain.handle(Ipc.UPDATE_CHECK, handleCheck)
|
||||||
|
ipcMain.handle(Ipc.UPDATE_DOWNLOAD, handleDownload)
|
||||||
|
ipcMain.handle(Ipc.UPDATE_INSTALL, () => {
|
||||||
|
// The close interceptor (tray flow) would swallow this quit — mark first.
|
||||||
|
markQuitting()
|
||||||
|
autoUpdater.quitAndInstall()
|
||||||
|
})
|
||||||
|
ipcMain.handle(Ipc.UPDATE_CHANGELOG, fetchChangelog)
|
||||||
|
}
|
||||||
|
|
||||||
export function configureAutoUpdater(): void {
|
export function configureAutoUpdater(): void {
|
||||||
// Update checks only run in packaged builds; no-op during development.
|
// Update checks only run in packaged builds; no-op during development.
|
||||||
if (!app.isPackaged) {
|
if (!app.isPackaged) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
useFeed('gitea')
|
||||||
const url = process.env.OT_UPDATE_URL || DEFAULT_FEED_URL
|
wireEvents()
|
||||||
const token = process.env.OT_UPDATE_TOKEN
|
|
||||||
|
|
||||||
autoUpdater.setFeedURL({
|
|
||||||
provider: 'generic',
|
|
||||||
url,
|
|
||||||
...(token ? { requestHeaders: { Authorization: `token ${token}` } } : {})
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.logger = console
|
autoUpdater.logger = console
|
||||||
autoUpdater.autoDownload = true
|
autoUpdater.autoDownload = false
|
||||||
autoUpdater.autoInstallOnAppQuit = true
|
autoUpdater.autoInstallOnAppQuit = true
|
||||||
|
|
||||||
// Check shortly after startup; log errors without crashing the app.
|
// Startup check only when the user left it enabled (设置 → 关于).
|
||||||
|
if (loadSettings().system.autoCheckUpdate === false) return
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
autoUpdater
|
void handleCheck()
|
||||||
.checkForUpdatesAndNotify()
|
|
||||||
.catch((err) => console.warn('[updater] checkForUpdates failed:', err))
|
|
||||||
}, 5000)
|
}, 5000)
|
||||||
timer.unref?.()
|
timer.unref?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Public entry point for later UI wiring (not connected this milestone). */
|
|
||||||
export function checkForUpdates(): Promise<void> {
|
|
||||||
if (!app.isPackaged) {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
return autoUpdater
|
|
||||||
.checkForUpdates()
|
|
||||||
.then(() => {})
|
|
||||||
.catch((err) => {
|
|
||||||
console.warn('[updater] checkForUpdates failed:', err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+12
-2
@@ -2,7 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron'
|
|||||||
import { Ipc } from '../shared/ipc'
|
import { Ipc } from '../shared/ipc'
|
||||||
import type { AppSettings } from '../shared/settings'
|
import type { AppSettings } from '../shared/settings'
|
||||||
import type { AppApi } from '../shared/api'
|
import type { AppApi } from '../shared/api'
|
||||||
import type { LayoutMeta, PtyCreateOptions, PtyDataEvent, PtyExitEvent, ZmodemOfferEvent, ZmodemResponse, ZmodemDoneEvent } from '../shared/ipc'
|
import type { LayoutMeta, PtyCreateOptions, PtyDataEvent, PtyExitEvent, ReleaseNote, UpdateState, ZmodemOfferEvent, ZmodemResponse, ZmodemDoneEvent } from '../shared/ipc'
|
||||||
import type {
|
import type {
|
||||||
HostKeyAction,
|
HostKeyAction,
|
||||||
HostKeyPromptEvent,
|
HostKeyPromptEvent,
|
||||||
@@ -117,7 +117,17 @@ const api: AppApi = {
|
|||||||
listLayouts: () => ipcRenderer.invoke(Ipc.LAYOUTS_LIST),
|
listLayouts: () => ipcRenderer.invoke(Ipc.LAYOUTS_LIST),
|
||||||
getLayout: (id: string) => ipcRenderer.invoke(Ipc.LAYOUTS_GET, id),
|
getLayout: (id: string) => ipcRenderer.invoke(Ipc.LAYOUTS_GET, id),
|
||||||
saveLayout: (meta: LayoutMeta, json: string) => ipcRenderer.invoke(Ipc.LAYOUTS_SAVE, meta, json),
|
saveLayout: (meta: LayoutMeta, json: string) => ipcRenderer.invoke(Ipc.LAYOUTS_SAVE, meta, json),
|
||||||
deleteLayout: (id: string) => ipcRenderer.invoke(Ipc.LAYOUTS_DELETE, id)
|
deleteLayout: (id: string) => ipcRenderer.invoke(Ipc.LAYOUTS_DELETE, id),
|
||||||
|
|
||||||
|
updateCheck: () => ipcRenderer.invoke(Ipc.UPDATE_CHECK),
|
||||||
|
updateDownload: () => ipcRenderer.invoke(Ipc.UPDATE_DOWNLOAD),
|
||||||
|
updateInstall: () => ipcRenderer.send(Ipc.UPDATE_INSTALL),
|
||||||
|
updateChangelog: () => ipcRenderer.invoke(Ipc.UPDATE_CHANGELOG),
|
||||||
|
onUpdateState: (cb: (s: UpdateState) => void) => {
|
||||||
|
const listener = (_: unknown, s: UpdateState): void => cb(s)
|
||||||
|
ipcRenderer.on(Ipc.UPDATE_STATE, listener)
|
||||||
|
return () => ipcRenderer.removeListener(Ipc.UPDATE_STATE, listener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('api', api)
|
contextBridge.exposeInMainWorld('api', api)
|
||||||
@@ -84,7 +84,12 @@ if (typeof window !== 'undefined' && !window.api) {
|
|||||||
listLayouts: async () => [],
|
listLayouts: async () => [],
|
||||||
getLayout: async () => null,
|
getLayout: async () => null,
|
||||||
saveLayout: async () => undefined,
|
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
|
;(window as unknown as { api: AppApi }).api = api
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { useSettingsStore } from './store'
|
|||||||
import { CursorSettingsTab, FontSettingsTab, RenderSettingsTab, SystemSettingsTab } from './SettingsTabs'
|
import { CursorSettingsTab, FontSettingsTab, RenderSettingsTab, SystemSettingsTab } from './SettingsTabs'
|
||||||
import { ThemeSettingsTab } from './ThemeSettingsTab'
|
import { ThemeSettingsTab } from './ThemeSettingsTab'
|
||||||
import { HighlightTab } from './HighlightTab'
|
import { HighlightTab } from './HighlightTab'
|
||||||
|
import { AboutTab } from './AboutTab'
|
||||||
import { ThemeEditor } from '../theme/editor/ThemeEditor'
|
import { ThemeEditor } from '../theme/editor/ThemeEditor'
|
||||||
import './settings.css'
|
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))
|
const closeEditor = (): void => setEditorState((prev) => (prev.open ? { ...prev, open: false } : prev))
|
||||||
|
|||||||
@@ -344,3 +344,102 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
SessionOpenResult
|
SessionOpenResult
|
||||||
} from './ipc'
|
} from './ipc'
|
||||||
import type { AppSettings } from './settings'
|
import type { AppSettings } from './settings'
|
||||||
|
import type { ReleaseNote, UpdateState } from './ipc'
|
||||||
import type {
|
import type {
|
||||||
HostKeyAction,
|
HostKeyAction,
|
||||||
HostKeyPromptEvent,
|
HostKeyPromptEvent,
|
||||||
@@ -105,4 +106,11 @@ export interface AppApi {
|
|||||||
/** json = serialized dockview layout (DockviewApi.toJSON()) */
|
/** json = serialized dockview layout (DockviewApi.toJSON()) */
|
||||||
saveLayout(meta: LayoutMeta, json: string): Promise<void>
|
saveLayout(meta: LayoutMeta, json: string): Promise<void>
|
||||||
deleteLayout(id: string): Promise<void>
|
deleteLayout(id: string): Promise<void>
|
||||||
|
|
||||||
|
// ---- updater (domestic feed first, GitHub fallback) ----
|
||||||
|
updateCheck(): Promise<UpdateState>
|
||||||
|
updateDownload(): Promise<void>
|
||||||
|
updateInstall(): void
|
||||||
|
updateChangelog(): Promise<ReleaseNote[]>
|
||||||
|
onUpdateState(cb: (s: UpdateState) => void): () => void
|
||||||
}
|
}
|
||||||
+40
-1
@@ -90,7 +90,15 @@ export const Ipc = {
|
|||||||
ZMODEM_DONE: 'zmodem:done',
|
ZMODEM_DONE: 'zmodem:done',
|
||||||
|
|
||||||
// ---- misc ----
|
// ---- misc ----
|
||||||
APP_INFO: 'app:info'
|
APP_INFO: 'app:info',
|
||||||
|
|
||||||
|
// ---- updater (check/download/install + state broadcast) ----
|
||||||
|
UPDATE_CHECK: 'update:check',
|
||||||
|
UPDATE_DOWNLOAD: 'update:download',
|
||||||
|
UPDATE_INSTALL: 'update:install',
|
||||||
|
UPDATE_CHANGELOG: 'update:changelog',
|
||||||
|
/** main -> renderer broadcast: UpdateState */
|
||||||
|
UPDATE_STATE: 'update:state'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export interface PtyCreateOptions {
|
export interface PtyCreateOptions {
|
||||||
@@ -122,6 +130,37 @@ export interface AppInfo {
|
|||||||
homeDir: string
|
homeDir: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- updater ----
|
||||||
|
export type UpdateStatus =
|
||||||
|
| 'idle'
|
||||||
|
| 'checking'
|
||||||
|
| 'available'
|
||||||
|
| 'latest'
|
||||||
|
| 'downloading'
|
||||||
|
| 'downloaded'
|
||||||
|
| 'error'
|
||||||
|
/** dev builds have no update channel */
|
||||||
|
| 'dev'
|
||||||
|
|
||||||
|
export interface UpdateState {
|
||||||
|
status: UpdateStatus
|
||||||
|
/** running app version */
|
||||||
|
currentVersion: string
|
||||||
|
/** newer version when available/downloading/downloaded */
|
||||||
|
version?: string
|
||||||
|
/** 0-100 while downloading */
|
||||||
|
percent?: number
|
||||||
|
error?: string
|
||||||
|
/** which feed served the last check: domestic Gitea first, GitHub fallback */
|
||||||
|
feed?: 'gitea' | 'github'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReleaseNote {
|
||||||
|
version: string
|
||||||
|
date: string
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface LayoutMeta {
|
export interface LayoutMeta {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export interface SystemSettings {
|
|||||||
globalShowHide?: string
|
globalShowHide?: string
|
||||||
/** close-button behavior: prompt every time / hide to tray / quit directly */
|
/** close-button behavior: prompt every time / hide to tray / quit directly */
|
||||||
closeAction?: 'ask' | 'tray' | 'exit'
|
closeAction?: 'ask' | 'tray' | 'exit'
|
||||||
|
/** check for updates shortly after startup (manual check always available) */
|
||||||
|
autoCheckUpdate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
@@ -102,5 +104,5 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
},
|
},
|
||||||
customThemes: [],
|
customThemes: [],
|
||||||
highlightRules: DEFAULT_HIGHLIGHT_RULES,
|
highlightRules: DEFAULT_HIGHLIGHT_RULES,
|
||||||
system: { launchAtLogin: false, preventSleep: false, globalShowHide: '', closeAction: 'ask' }
|
system: { launchAtLogin: false, preventSleep: false, globalShowHide: '', closeAction: 'ask', autoCheckUpdate: true }
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user