Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ jobs:
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
projectPath: desktop
tauriScript: bunx tauri
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEY1RTlBNEYxNDc4NDdEMEUKUldRT2ZZUkg4YVRwOVZHUjNXSzh2R1ZsREJwL1UzU1JKOEl2WHhMMkR3RVpJbzhjS2h0Y0J1NGcK",
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlCOUIwRDExQTc5RTFGMzYKUldRMkg1Nm5FUTJibTJ2cGlHY0pkL0dGemxXMUlzc01pVTVMM1U3WGpmWUtrUC8wK2ErSXhLKzEK",
"endpoints": [
"https://github.com/NanmiCoder/cc-haha/releases/latest/download/latest.json"
],
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/tauri.release-ci.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"bundle": {
"createUpdaterArtifacts": false
"createUpdaterArtifacts": true
}
}
133 changes: 53 additions & 80 deletions desktop/src/components/shared/UpdateChecker.tsx
Original file line number Diff line number Diff line change
@@ -1,110 +1,83 @@
import { useEffect, useState } from 'react'
import { useUIStore } from '../../stores/uiStore'
import { useEffect } from 'react'
import { useTranslation } from '../../i18n'

type UpdateInfo = {
version: string
downloading: boolean
progress: number
}

let isTauri = false
try {
isTauri = '__TAURI_INTERNALS__' in window
} catch {
// not in Tauri
}
import { isTauriRuntime } from '../../lib/desktopRuntime'
import { useUpdateStore } from '../../stores/updateStore'

export function UpdateChecker() {
const [update, setUpdate] = useState<UpdateInfo | null>(null)
const addToast = useUIStore((s) => s.addToast)
const t = useTranslation()
const status = useUpdateStore((s) => s.status)
const availableVersion = useUpdateStore((s) => s.availableVersion)
const releaseNotes = useUpdateStore((s) => s.releaseNotes)
const progressPercent = useUpdateStore((s) => s.progressPercent)
const error = useUpdateStore((s) => s.error)
const shouldPrompt = useUpdateStore((s) => s.shouldPrompt)
const initialize = useUpdateStore((s) => s.initialize)
const installUpdate = useUpdateStore((s) => s.installUpdate)
const dismissPrompt = useUpdateStore((s) => s.dismissPrompt)

useEffect(() => {
if (!isTauri) return

const checkForUpdate = async () => {
try {
const { check } = await import('@tauri-apps/plugin-updater')
const available = await check()
if (available) {
setUpdate({ version: available.version, downloading: false, progress: 0 })
addToast({
type: 'info',
message: t('update.newVersion', { version: available.version }),
duration: 0, // persist until dismissed
})
}
} catch {
// Updater not configured or no network — silently ignore
}
}

// Check after a short delay so UI loads first
const timer = setTimeout(checkForUpdate, 5000)
return () => clearTimeout(timer)
}, [addToast])

if (!update || !isTauri) return null
void initialize()
}, [initialize])

const handleUpdate = async () => {
try {
const { check } = await import('@tauri-apps/plugin-updater')
const { relaunch } = await import('@tauri-apps/plugin-process')
const available = await check()
if (!available) return
if (!isTauriRuntime()) return null

setUpdate((u) => u && { ...u, downloading: true })
const showPopup =
shouldPrompt && !!availableVersion && ['available', 'downloading', 'restarting'].includes(status)

await available.downloadAndInstall((event) => {
if (event.event === 'Started' && event.data.contentLength) {
setUpdate((u) => u && { ...u, progress: 0 })
} else if (event.event === 'Progress') {
setUpdate((u) => {
if (!u) return u
return { ...u, progress: Math.min(u.progress + (event.data.chunkLength ?? 0), 100) }
})
} else if (event.event === 'Finished') {
setUpdate((u) => u && { ...u, progress: 100 })
}
})
if (!showPopup) return null

await relaunch()
} catch (err) {
addToast({
type: 'error',
message: t('update.failed', { error: err instanceof Error ? err.message : String(err) }),
})
setUpdate((u) => u && { ...u, downloading: false })
}
}
const statusText =
status === 'restarting'
? t('update.restarting')
: status === 'downloading'
? t('update.downloading')
: null

return (
<div className="fixed top-4 right-4 z-[200] max-w-xs">
<div className="fixed top-4 right-4 z-[200] max-w-sm">
<div className="bg-[var(--color-surface-container-low)] border border-[var(--color-border)] rounded-[var(--radius-lg)] shadow-[var(--shadow-dropdown)] p-4">
<p className="text-sm font-medium text-[var(--color-text-primary)]">
{t('update.available', { version: update.version })}
{t('update.available', { version: availableVersion })}
</p>
{update.downloading ? (
<div className="mt-2">

{releaseNotes && (
<p className="mt-2 text-xs leading-5 text-[var(--color-text-secondary)] whitespace-pre-wrap line-clamp-5">
{releaseNotes}
</p>
)}

{(status === 'downloading' || status === 'restarting') && (
<div className="mt-3">
<div className="h-1.5 bg-[var(--color-surface)] rounded-full overflow-hidden">
<div
className="h-full bg-[var(--color-text-accent)] transition-all duration-300"
style={{ width: `${Math.min(update.progress, 100)}%` }}
style={{ width: `${Math.min(progressPercent, 100)}%` }}
/>
</div>
<p className="text-xs text-[var(--color-text-tertiary)] mt-1">{t('update.downloading')}</p>
{statusText && (
<p className="text-xs text-[var(--color-text-tertiary)] mt-1">
{statusText} {status === 'downloading' ? `${progressPercent}%` : ''}
</p>
)}
</div>
) : (
<div className="mt-2 flex gap-2">
)}

{error && (
<p className="mt-2 text-xs text-[var(--color-error)]">
{t('update.failed', { error })}
</p>
)}

{status === 'available' && (
<div className="mt-3 flex gap-2">
<button
onClick={handleUpdate}
onClick={() => void installUpdate()}
className="px-3 py-1 text-xs font-medium rounded-[var(--radius-md)] bg-[var(--color-text-accent)] text-white hover:opacity-90 transition-opacity"
>
{t('update.now')}
</button>
<button
onClick={() => setUpdate(null)}
onClick={dismissPrompt}
className="px-3 py-1 text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] transition-colors"
>
{t('update.later')}
Expand Down
12 changes: 12 additions & 0 deletions desktop/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ export const en = {
'settings.about.starHint': 'If this project helps you, consider giving it a Star',
'settings.about.author': 'Author',
'settings.about.socialMedia': 'Social Media',
'settings.about.updates': 'App Updates',
'settings.about.updatesDesc': 'Check GitHub Releases, download the installer, and relaunch after install.',

// Settings > Computer Use
'settings.tab.computerUse': 'Computer Use',
Expand Down Expand Up @@ -576,10 +578,20 @@ export const en = {

// ─── Update Checker ──────────────────────────────────────
'update.available': 'v{version} available',
'update.availableLabel': 'Available',
'update.checking': 'Checking for updates...',
'update.checkNow': 'Check now',
'update.checkedAt': 'Last checked {time}',
'update.currentVersionUnknown': 'Unknown',
'update.newVersion': 'New version v{version} available',
'update.downloading': 'Downloading...',
'update.idle': 'Check for updates to compare your installed version with the latest GitHub Release.',
'update.now': 'Update now',
'update.later': 'Later',
'update.progress': 'Downloading update... {progress}%',
'update.releaseNotes': 'Release Notes',
'update.restarting': 'Restarting to finish update...',
'update.upToDate': 'You are up to date on v{version}.',
'update.failed': 'Update failed: {error}',

// ─── Active Session ──────────────────────────────────────
Expand Down
12 changes: 12 additions & 0 deletions desktop/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export const zh: Record<TranslationKey, string> = {
'settings.about.starHint': '如果这个项目对你有帮助,欢迎给个 Star',
'settings.about.author': '作者',
'settings.about.socialMedia': '社交媒体',
'settings.about.updates': '应用更新',
'settings.about.updatesDesc': '检查 GitHub Releases,下载安装包,并在安装后自动重启。',

// Settings > Computer Use
'settings.tab.computerUse': 'Computer Use',
Expand Down Expand Up @@ -578,10 +580,20 @@ export const zh: Record<TranslationKey, string> = {

// ─── 更新检查 ──────────────────────────────────────
'update.available': 'v{version} 可用',
'update.availableLabel': '可更新版本',
'update.checking': '正在检查更新...',
'update.checkNow': '检查更新',
'update.checkedAt': '上次检查时间 {time}',
'update.currentVersionUnknown': '未知版本',
'update.newVersion': '新版本 v{version} 可用',
'update.downloading': '下载中...',
'update.idle': '点击检查更新,对比当前安装版本和 GitHub Releases 的最新版本。',
'update.now': '立即更新',
'update.later': '稍后',
'update.progress': '正在下载更新... {progress}%',
'update.releaseNotes': '更新说明',
'update.restarting': '正在重启以完成更新...',
'update.upToDate': '当前已是最新版本 v{version}。',
'update.failed': '更新失败: {error}',

// ─── 活跃会话 ──────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/lib/desktopRuntime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getDefaultBaseUrl, setBaseUrl } from '../api/client'

function isTauriRuntime() {
export function isTauriRuntime() {
if (typeof window === 'undefined') return false
return '__TAURI_INTERNALS__' in window || '__TAURI__' in window
}
Expand Down
Loading
Loading