Skip to content

Commit a4b8f90

Browse files
feat(update): implement update check functionality and UI integration
1 parent 79c3390 commit a4b8f90

File tree

10 files changed

+188
-14
lines changed

10 files changed

+188
-14
lines changed

electron/ipc/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ import './path'
66
import './project'
77
import './settings'
88
import './system'
9+
import './update'

electron/ipc/system.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import process from 'node:process'
55
import { ipcMain, shell } from 'electron'
66

77
// 在系统默认浏览器中打开链接
8-
ipcMain.handle('external:open', (_, url: string): void => {
8+
ipcMain.handle('external:open', async (_, url: string): Promise<void> => {
99
shell.openExternal(url)
1010
.then(() => {})
1111
.catch(err => console.error('Failed to open link:', err))
1212
})
1313

1414
// 使用资源管理器打开路径
15-
ipcMain.handle('explorer:open', (_, folderPath: string): void => {
15+
ipcMain.handle('explorer:open', async (_, folderPath: string): Promise<void> => {
1616
const resolvedPath = path.resolve(folderPath)
1717
// 使用系统默认的资源管理器打开文件夹
1818
shell.openPath(resolvedPath).catch((err) => {
@@ -21,7 +21,7 @@ ipcMain.handle('explorer:open', (_, folderPath: string): void => {
2121
})
2222

2323
// 使用终端打开路径
24-
ipcMain.handle('terminal:open', (_, folderPath: string): void => {
24+
ipcMain.handle('terminal:open', async (_, folderPath: string): Promise<void> => {
2525
const resolvedPath = path.resolve(folderPath)
2626
// 根据操作系统选择合适的终端命令
2727
const isWindows = process.platform === 'win32'

electron/ipc/theme.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ipcMain } from 'electron'
33
import { getMainWindow } from '../main'
44

55
// 设置主题
6-
ipcMain.handle('theme:set', (event, theme: string): void => {
6+
ipcMain.handle('theme:set', async (_, theme: string): Promise<void> => {
77
const colorSettings = theme === 'dark'
88
? { color: '#2B2D30', symbolColor: '#DFE1E5' }
99
: { color: '#F2F2F2', symbolColor: '#222323' }

electron/ipc/update.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { app, ipcMain } from 'electron'
2+
3+
interface CheckUpdateResult {
4+
hasUpdate: boolean
5+
currentVersion: string
6+
latestVersion?: string
7+
url?: string
8+
name?: string
9+
notes?: string
10+
publishedAt?: string
11+
error?: string
12+
}
13+
14+
function normalizeVersion(v: string | undefined): string {
15+
if (!v)
16+
return '0.0.0'
17+
// remove leading v/V and trim whitespace
18+
const cleaned = v.trim().replace(/^v/i, '')
19+
// drop pre-release/build metadata for comparison
20+
return cleaned.split(/[+-]/)[0]
21+
}
22+
23+
function compareSemver(a: string, b: string): number {
24+
const pa = normalizeVersion(a).split('.').map(n => Number.parseInt(n, 10) || 0)
25+
const pb = normalizeVersion(b).split('.').map(n => Number.parseInt(n, 10) || 0)
26+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
27+
const na = pa[i] ?? 0
28+
const nb = pb[i] ?? 0
29+
if (na > nb)
30+
return 1
31+
if (na < nb)
32+
return -1
33+
}
34+
return 0
35+
}
36+
37+
ipcMain.handle('update:check', async (): Promise<CheckUpdateResult> => {
38+
try {
39+
// robust version source for dev and packaged
40+
const currentVersion = app.getVersion() || '0.0.0'
41+
42+
// GitHub Releases API: latest (excludes drafts and prereleases)
43+
const res = await fetch('https://api.github.com/repos/MidnightCrowing/CodeNest/releases/latest', {
44+
headers: {
45+
'Accept': 'application/vnd.github+json',
46+
'User-Agent': 'CodeNest (Electron)',
47+
},
48+
})
49+
50+
if (!res.ok) {
51+
// e.g. rate limited or not found
52+
return { hasUpdate: false, currentVersion, error: `Request failed: ${res.status}` }
53+
}
54+
55+
const json: any = await res.json()
56+
const latestTag = json?.tag_name as string | undefined
57+
const latestVersion = normalizeVersion(latestTag || json?.name)
58+
const url: string | undefined = json?.html_url || 'https://github.com/MidnightCrowing/CodeNest/releases/latest'
59+
const name: string | undefined = json?.name
60+
const notes: string | undefined = json?.body
61+
const publishedAt: string | undefined = json?.published_at
62+
63+
const cmp = compareSemver(currentVersion, latestVersion)
64+
const hasUpdate = cmp < 0
65+
66+
return {
67+
hasUpdate,
68+
currentVersion,
69+
latestVersion,
70+
url,
71+
name,
72+
notes,
73+
publishedAt,
74+
}
75+
}
76+
catch (e: any) {
77+
const currentVersion = app.getVersion() || '0.0.0'
78+
return { hasUpdate: false, currentVersion, error: e?.message || String(e) }
79+
}
80+
})

electron/preload.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ contextBridge.exposeInMainWorld('api', {
3232
openInExplorer: (path: string) => ipcRenderer.invoke('explorer:open', path),
3333
openInTerminal: (path: string) => ipcRenderer.invoke('terminal:open', path),
3434

35+
// update
36+
checkUpdate: () => ipcRenderer.invoke('update:check'),
37+
3538
// theme
3639
setWindowTheme: (currentTheme: 'light' | 'dark') => ipcRenderer.invoke('theme:set', currentTheme),
3740
})

src/global.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ declare global {
3535
openInExplorer: (path: string) => void
3636
openInTerminal: (path: string) => void
3737

38+
// update
39+
checkUpdate: () => Promise<{
40+
hasUpdate: boolean
41+
currentVersion: string
42+
latestVersion?: string
43+
url?: string
44+
name?: string
45+
notes?: string
46+
publishedAt?: string
47+
error?: string
48+
}>
49+
3850
// theme
3951
setWindowTheme: (currentTheme: ThemeEnum) => void
4052
}

src/locales/en.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,12 @@ settings:
162162
cancel: No
163163
about:
164164
title: About
165-
version: Version
165+
version:
166+
title: Version
167+
check_update: Check for updates
168+
checking_update: Checking for updates...
169+
is_latest: You are on the latest version
170+
found_new: New version available
166171
link: Link
167172
import_export:
168173
title: Import/Export

src/locales/zh-CN.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,12 @@ settings:
154154
cancel:
155155
about:
156156
title: 关于
157-
version: 版本
157+
version:
158+
title: 版本
159+
check_update: 检查更新
160+
checking_update: 正在检查更新...
161+
is_latest: 已是最新版本
162+
found_new: 发现新版本
158163
link: 链接
159164
import_export:
160165
title: 导入/导出

src/views/ProjectConfig/ProjectConfigProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ProjectKind } from '~/constants/localProject'
55
type Nullable<T> = {
66
[K in keyof T]: T[K] | null;
77
}
8-
export type NullableLocalProject = Omit<Nullable<LocalProject>, 'kind' | 'license' | 'isTemporary'> & {
8+
export type NullableLocalProject = Nullable<LocalProject> & {
99
kind: ProjectKind
1010
license: LicenseEnum | string
1111
isTemporary: boolean

src/views/Settings/pages/About.vue

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts" setup>
2-
import { JeTransparentButton } from 'jetv-ui'
2+
import { JeLink, JeLoader, JeTransparentButton } from 'jetv-ui'
33
import { useI18n } from 'vue-i18n'
44
55
import { useProjectsStore } from '~/stores/projectsStore'
@@ -9,10 +9,43 @@ import { version } from '../../../../package.json'
99
const { t } = useI18n()
1010
const projects = useProjectsStore()
1111
12+
const checking = ref(false)
13+
const updateInfo = ref<{
14+
hasUpdate: boolean
15+
currentVersion: string
16+
latestVersion?: string
17+
url?: string
18+
name?: string
19+
notes?: string
20+
publishedAt?: string
21+
error?: string
22+
} | null>(null)
23+
1224
function openGithub() {
1325
window.api.openExternal('https://github.com/MidnightCrowing/CodeNest')
1426
}
1527
28+
function openRelease(url?: string) {
29+
const target = url || 'https://github.com/MidnightCrowing/CodeNest/releases/latest'
30+
window.api.openExternal(target)
31+
}
32+
33+
async function onCheckUpdate() {
34+
if (checking.value)
35+
return
36+
checking.value = true
37+
try {
38+
const res = await window.api.checkUpdate()
39+
updateInfo.value = res
40+
}
41+
catch (e) {
42+
updateInfo.value = { hasUpdate: false, currentVersion: version, error: String(e) }
43+
}
44+
finally {
45+
checking.value = false
46+
}
47+
}
48+
1649
async function importProjects() {
1750
await window.api.importProject()
1851
await projects.loadProjects()
@@ -32,9 +65,44 @@ function exportProjects() {
3265
{{ t('settings.about.title') }}
3366
</h3>
3467

35-
<div p="y-5px" flex="~ items-center" gap="5px">
36-
<strong>{{ t('settings.about.version') }}: </strong>
37-
<span> v{{ version }}</span>
68+
<div p="y-5px" flex="~ row" gap="5px">
69+
<strong>{{ t('settings.about.version.title') }}: </strong>
70+
71+
<div flex="~ col" gap="5px">
72+
<span> v{{ version }}</span>
73+
74+
<div flex="~ row items-center" gap="8px">
75+
<JeLink
76+
type="internal"
77+
:disabled="checking"
78+
@click="onCheckUpdate"
79+
>
80+
{{ t('settings.about.version.check_update') }}
81+
</JeLink>
82+
83+
<template v-if="checking">
84+
<div flex="~ row items-center" gap="2px">
85+
<JeLoader />
86+
<span text="secondary">{{ t('settings.about.version.checking_update') }}</span>
87+
</div>
88+
</template>
89+
90+
<template v-else-if="updateInfo && !updateInfo.hasUpdate && !updateInfo.error">
91+
<span text="secondary">{{ t('settings.about.version.is_latest') }}</span>
92+
</template>
93+
94+
<template v-else-if="updateInfo && updateInfo.hasUpdate">
95+
<JeLink type="internal" @click="openRelease(updateInfo.url)">
96+
{{ t('settings.about.version.found_new') }}
97+
<span v-if="updateInfo.latestVersion">: v{{ updateInfo.latestVersion }}</span>
98+
</JeLink>
99+
</template>
100+
101+
<template v-else-if="updateInfo && updateInfo.error">
102+
<span text="light:$red-5 dark:$red-5">{{ updateInfo.error }}</span>
103+
</template>
104+
</div>
105+
</div>
38106
</div>
39107

40108
<div p="y-5px" flex="~ items-center" gap="5px">
@@ -44,7 +112,7 @@ function exportProjects() {
44112
flex="~ items-center" gap="5px"
45113
@click="openGithub"
46114
>
47-
<span i-custom:github text="17px" />
115+
<span class="i-custom:github" text="17px" />
48116
GitHub
49117
</JeTransparentButton>
50118
</div>
@@ -57,15 +125,15 @@ function exportProjects() {
57125
flex="~ items-center" gap="5px"
58126
@click="importProjects"
59127
>
60-
<span i-custom:import text="17px" />
128+
<span class="i-custom:import" text="17px" />
61129
{{ t('settings.about.import_export.import_desc') }}
62130
</JeTransparentButton>
63131
<JeTransparentButton
64132
type="subtle"
65133
flex="~ items-center" gap="5px"
66134
@click="exportProjects"
67135
>
68-
<span i-custom:export text="17px" />
136+
<span class="i-custom:export" text="17px" />
69137
{{ t('settings.about.import_export.export_desc') }}
70138
</JeTransparentButton>
71139
</div>

0 commit comments

Comments
 (0)