fix(stage-tamagotchi): autoUpdater fix with channels
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { object, optional, string } from 'valibot'
|
||||
import { object, optional, picklist, string } from 'valibot'
|
||||
|
||||
import { createConfig } from '../libs/electron/persistence'
|
||||
|
||||
export const globalAppConfigSchema = object({
|
||||
language: optional(string(), 'en'),
|
||||
updateChannel: optional(picklist(['stable', 'alpha', 'beta', 'nightly', 'canary'])),
|
||||
})
|
||||
|
||||
export function createGlobalAppConfig() {
|
||||
|
||||
@@ -101,7 +101,19 @@ app.whenReady().then(async () => {
|
||||
|
||||
const appConfig = injeca.provide('configs:app', () => createGlobalAppConfig())
|
||||
const electronApp = injeca.provide('host:electron:app', () => app)
|
||||
const autoUpdater = injeca.provide('services:auto-updater', () => setupAutoUpdater())
|
||||
const autoUpdater = injeca.provide('services:auto-updater', {
|
||||
dependsOn: { appConfig },
|
||||
build: ({ dependsOn }) => setupAutoUpdater({
|
||||
getStoredUpdateLane: () => dependsOn.appConfig.get()?.updateChannel,
|
||||
setStoredUpdateLane: (lane) => {
|
||||
const currentConfig = dependsOn.appConfig.get()
|
||||
dependsOn.appConfig.update({
|
||||
language: currentConfig?.language ?? 'en',
|
||||
updateChannel: lane,
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const i18n = injeca.provide('libs:i18n', {
|
||||
dependsOn: { appConfig },
|
||||
|
||||
@@ -11,6 +11,10 @@ const isDevState = vi.hoisted(() => ({
|
||||
value: false,
|
||||
}))
|
||||
|
||||
const stdEnvState = vi.hoisted(() => ({
|
||||
isWindows: false,
|
||||
}))
|
||||
|
||||
const updaterState = vi.hoisted(() => ({
|
||||
instance: createUpdaterMock(),
|
||||
}))
|
||||
@@ -42,6 +46,12 @@ vi.mock('@electron-toolkit/utils', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('std-env', () => ({
|
||||
get isWindows() {
|
||||
return stdEnvState.isWindows
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@guiiai/logg', () => ({
|
||||
useLogg: () => ({
|
||||
useGlobalConfig: () => ({
|
||||
@@ -69,6 +79,21 @@ vi.mock('~build/git', () => ({
|
||||
}))
|
||||
|
||||
describe('setupAutoUpdater', () => {
|
||||
const defaultReleases = [
|
||||
{ tag_name: 'v0.9.0-beta.6', draft: false, prerelease: true },
|
||||
]
|
||||
|
||||
function mockGitHubReleasesFetch(releases = defaultReleases) {
|
||||
const fetchSpy = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: async () => releases,
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchSpy)
|
||||
return fetchSpy
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
@@ -76,41 +101,49 @@ describe('setupAutoUpdater', () => {
|
||||
appMock.getVersion.mockReturnValue('0.9.0-beta.4')
|
||||
appMock.getPath.mockImplementation((name: string) => name === 'logs' ? '/tmp/airi/logs' : `/tmp/${name}`)
|
||||
isDevState.value = false
|
||||
stdEnvState.isWindows = false
|
||||
delete process.env.UPDATE_SERVER_URL
|
||||
delete process.env.AIRI_UPDATE_CHANNEL
|
||||
mockGitHubReleasesFetch()
|
||||
})
|
||||
|
||||
it('does not query the GitHub Releases API during setup or manual checks', async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => [],
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchSpy)
|
||||
|
||||
it('resolves release tag from GitHub API and configures generic provider for checks', async () => {
|
||||
const fetchSpy = mockGitHubReleasesFetch([
|
||||
{ tag_name: 'v0.9.0-beta.6', draft: false, prerelease: true },
|
||||
{ tag_name: 'v0.9.0-beta.5', draft: false, prerelease: true },
|
||||
])
|
||||
const { setupAutoUpdater } = await import('./auto-updater')
|
||||
const service = setupAutoUpdater()
|
||||
|
||||
await Promise.resolve()
|
||||
await service.checkForUpdates()
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
||||
expect(updaterState.instance.setFeedURL).toHaveBeenCalledWith({
|
||||
provider: 'generic',
|
||||
url: 'https://github.com/moeru-ai/airi/releases/download/v0.9.0-beta.6',
|
||||
})
|
||||
expect(updaterState.instance.channel).toBe('latest-arm64')
|
||||
})
|
||||
|
||||
it('only uses setFeedURL when an explicit update server override is provided', async () => {
|
||||
it('ignores UPDATE_SERVER_URL in non-dev runtime', async () => {
|
||||
process.env.UPDATE_SERVER_URL = 'http://localhost:8787/stable'
|
||||
|
||||
const { setupAutoUpdater } = await import('./auto-updater')
|
||||
setupAutoUpdater()
|
||||
const service = setupAutoUpdater()
|
||||
await service.checkForUpdates()
|
||||
|
||||
expect(updaterState.instance.setFeedURL).toHaveBeenCalledWith({
|
||||
provider: 'generic',
|
||||
url: 'http://localhost:8787/stable',
|
||||
url: 'https://github.com/moeru-ai/airi/releases/download/v0.9.0-beta.6',
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the real updater in dev mode when an explicit update server override is provided', async () => {
|
||||
it('uses UPDATE_SERVER_URL only in dev mode for update-test harness', async () => {
|
||||
isDevState.value = true
|
||||
process.env.UPDATE_SERVER_URL = 'http://localhost:8787/stable'
|
||||
|
||||
const fetchSpy = mockGitHubReleasesFetch()
|
||||
const { setupAutoUpdater } = await import('./auto-updater')
|
||||
setupAutoUpdater()
|
||||
|
||||
@@ -119,6 +152,25 @@ describe('setupAutoUpdater', () => {
|
||||
url: 'http://localhost:8787/stable',
|
||||
})
|
||||
expect(updaterState.instance.forceDevUpdateConfig).toBe(true)
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('supports explicit stable lane selection for future dynamic channel switching', async () => {
|
||||
process.env.AIRI_UPDATE_CHANNEL = 'stable'
|
||||
mockGitHubReleasesFetch([
|
||||
{ tag_name: 'v0.9.0-beta.6', draft: false, prerelease: true },
|
||||
{ tag_name: 'v0.8.9', draft: false, prerelease: false },
|
||||
{ tag_name: 'v0.8.8', draft: false, prerelease: false },
|
||||
])
|
||||
|
||||
const { setupAutoUpdater } = await import('./auto-updater')
|
||||
const service = setupAutoUpdater()
|
||||
await service.checkForUpdates()
|
||||
|
||||
expect(updaterState.instance.setFeedURL).toHaveBeenCalledWith({
|
||||
provider: 'generic',
|
||||
url: 'https://github.com/moeru-ai/airi/releases/download/v0.8.9',
|
||||
})
|
||||
})
|
||||
|
||||
it('reports only authoritative diagnostics fields', async () => {
|
||||
@@ -149,21 +201,16 @@ describe('setupAutoUpdater', () => {
|
||||
})
|
||||
|
||||
it('uses silent relaunch install on Windows only', async () => {
|
||||
const originalPlatform = process.platform
|
||||
vi.stubEnv('TEST_PLATFORM', '')
|
||||
|
||||
stdEnvState.isWindows = true
|
||||
const { setupAutoUpdater } = await import('./auto-updater')
|
||||
const service = setupAutoUpdater()
|
||||
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' })
|
||||
await service.quitAndInstall()
|
||||
expect(updaterState.instance.quitAndInstall).toHaveBeenCalledWith(true, true)
|
||||
|
||||
stdEnvState.isWindows = false
|
||||
updaterState.instance.quitAndInstall.mockClear()
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' })
|
||||
await service.quitAndInstall()
|
||||
expect(updaterState.instance.quitAndInstall).toHaveBeenCalledWith()
|
||||
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,8 @@ import { isWindows } from 'std-env'
|
||||
import {
|
||||
autoUpdater as autoUpdaterEventa,
|
||||
electronAutoUpdaterStateChanged,
|
||||
electronGetUpdaterPreferences,
|
||||
electronSetUpdaterPreferences,
|
||||
} from '../../../shared/eventa'
|
||||
import { MockAutoUpdater } from './mock-auto-updater'
|
||||
|
||||
@@ -27,11 +29,82 @@ function getReleaseChannelName() {
|
||||
return process.arch === 'arm64' ? 'latest-arm64' : 'latest-x64'
|
||||
}
|
||||
|
||||
const GITHUB_RELEASES_API_URL = 'https://api.github.com/repos/moeru-ai/airi/releases?per_page=100'
|
||||
const GITHUB_RELEASE_DOWNLOAD_BASE_URL = 'https://github.com/moeru-ai/airi/releases/download'
|
||||
const UPDATE_CHANNEL_ENV_KEY = 'AIRI_UPDATE_CHANNEL'
|
||||
|
||||
export type UpdateLane = 'stable' | 'alpha' | 'beta' | 'nightly' | 'canary'
|
||||
interface GitHubReleaseRecord {
|
||||
tag_name?: string
|
||||
draft?: boolean
|
||||
prerelease?: boolean
|
||||
}
|
||||
|
||||
function getUpdateServerOverride() {
|
||||
// NOTICE: UPDATE_SERVER_URL is intentionally development-only for local update-test harness.
|
||||
// Production update routing must not depend on this variable.
|
||||
if (!is.dev)
|
||||
return undefined
|
||||
|
||||
const value = process.env.UPDATE_SERVER_URL?.trim()
|
||||
return value || undefined
|
||||
}
|
||||
|
||||
function normalizeLane(value: string | undefined): UpdateLane | undefined {
|
||||
if (!value)
|
||||
return undefined
|
||||
|
||||
switch (value.toLowerCase()) {
|
||||
case 'stable':
|
||||
case 'alpha':
|
||||
case 'beta':
|
||||
case 'nightly':
|
||||
case 'canary':
|
||||
return value.toLowerCase() as UpdateLane
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function laneFromVersion(version: string): UpdateLane {
|
||||
const prerelease = semver.prerelease(version)?.[0]?.toString().toLowerCase()
|
||||
return normalizeLane(prerelease) ?? 'stable'
|
||||
}
|
||||
|
||||
function getPreferredUpdateLane(params: { version: string, storedLane?: UpdateLane }): UpdateLane {
|
||||
return normalizeLane(process.env[UPDATE_CHANNEL_ENV_KEY]?.trim()) ?? params.storedLane ?? laneFromVersion(params.version)
|
||||
}
|
||||
|
||||
function getSemverFromTag(tag: string) {
|
||||
return semver.valid(tag) ?? semver.valid(tag.startsWith('v') ? tag.slice(1) : tag)
|
||||
}
|
||||
|
||||
function isTagInLane(tag: string, lane: UpdateLane) {
|
||||
const version = getSemverFromTag(tag)
|
||||
if (!version)
|
||||
return false
|
||||
|
||||
const prerelease = semver.prerelease(version)?.[0]?.toString().toLowerCase()
|
||||
if (lane === 'stable')
|
||||
return !prerelease
|
||||
|
||||
return prerelease === lane
|
||||
}
|
||||
|
||||
function selectLatestTagForLane(releases: GitHubReleaseRecord[], lane: UpdateLane) {
|
||||
const candidates = releases
|
||||
.filter(release => !release.draft && typeof release.tag_name === 'string' && isTagInLane(release.tag_name, lane))
|
||||
.map((release) => {
|
||||
const tag = release.tag_name as string
|
||||
const version = getSemverFromTag(tag)
|
||||
return version ? { tag, version } : null
|
||||
})
|
||||
.filter(Boolean) as Array<{ tag: string, version: string }>
|
||||
|
||||
candidates.sort((a, b) => semver.rcompare(a.version, b.version))
|
||||
return candidates[0]?.tag
|
||||
}
|
||||
|
||||
export interface AppUpdaterLike {
|
||||
on: (event: string, listener: (...args: any[]) => void) => any
|
||||
checkForUpdates: () => Promise<any>
|
||||
@@ -62,23 +135,37 @@ export interface AutoUpdater {
|
||||
checkForUpdates: () => Promise<void>
|
||||
downloadUpdate: () => Promise<void>
|
||||
quitAndInstall: () => Promise<void>
|
||||
getPreferredUpdateLane: () => UpdateLane | undefined
|
||||
setPreferredUpdateLane: (lane: UpdateLane | undefined) => Promise<void>
|
||||
subscribe: (callback: (state: AutoUpdaterState) => void) => () => void
|
||||
}
|
||||
|
||||
export interface AutoUpdaterOptions {
|
||||
getStoredUpdateLane?: () => UpdateLane | undefined
|
||||
setStoredUpdateLane?: (lane: UpdateLane | undefined) => void
|
||||
}
|
||||
|
||||
function isPrereleaseVersion(version: string) {
|
||||
return (semver.prerelease(version)?.length ?? 0) > 0
|
||||
}
|
||||
|
||||
export function setupAutoUpdater(): AutoUpdater {
|
||||
export function setupAutoUpdater(options: AutoUpdaterOptions = {}): AutoUpdater {
|
||||
const semaphore = new Semaphore(1)
|
||||
const isPrereleaseBuild = isPrereleaseVersion(app.getVersion())
|
||||
const appVersion = app.getVersion()
|
||||
const isPrereleaseBuild = isPrereleaseVersion(appVersion)
|
||||
const log = useLogg('auto-updater').useGlobalConfig()
|
||||
const autoUpdater = fromImported()
|
||||
const feedUrlOverride = getUpdateServerOverride()
|
||||
let storedPreferredLane = options.getStoredUpdateLane?.()
|
||||
const releaseChannelName = getReleaseChannelName()
|
||||
let activeFeedUrlOverride = feedUrlOverride
|
||||
let resolvedReleaseTag: string | undefined
|
||||
let prepareFeedPromise: Promise<void> | undefined
|
||||
|
||||
autoUpdater.allowPrerelease = isPrereleaseBuild
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.channel = getReleaseChannelName()
|
||||
if (activeFeedUrlOverride)
|
||||
autoUpdater.channel = releaseChannelName
|
||||
autoUpdater.forceDevUpdateConfig = !!feedUrlOverride && !app.isPackaged
|
||||
autoUpdater.logger = {
|
||||
info: (message: string) => log.log(message),
|
||||
@@ -87,19 +174,19 @@ export function setupAutoUpdater(): AutoUpdater {
|
||||
debug: (message: string) => log.debug(message),
|
||||
}
|
||||
|
||||
if (feedUrlOverride)
|
||||
autoUpdater.setFeedURL?.({ provider: 'generic', url: feedUrlOverride })
|
||||
if (activeFeedUrlOverride)
|
||||
autoUpdater.setFeedURL?.({ provider: 'generic', url: activeFeedUrlOverride })
|
||||
|
||||
const withDiagnostics = (next: AutoUpdaterState): AutoUpdaterState => ({
|
||||
...next,
|
||||
diagnostics: {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
channel: autoUpdater.channel || getReleaseChannelName(),
|
||||
channel: autoUpdater.channel || releaseChannelName,
|
||||
logFilePath: app.getPath('logs'),
|
||||
executablePath: process.execPath,
|
||||
isOverrideActive: !!feedUrlOverride,
|
||||
...(feedUrlOverride ? { feedUrl: feedUrlOverride } : {}),
|
||||
isOverrideActive: !!activeFeedUrlOverride,
|
||||
...(activeFeedUrlOverride ? { feedUrl: activeFeedUrlOverride } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -127,6 +214,74 @@ export function setupAutoUpdater(): AutoUpdater {
|
||||
log.withError(error).error(reason)
|
||||
}
|
||||
|
||||
function applyGenericFeedOverride(url: string, reason: string) {
|
||||
activeFeedUrlOverride = url
|
||||
autoUpdater.channel = releaseChannelName
|
||||
autoUpdater.setFeedURL?.({ provider: 'generic', url })
|
||||
log.warn(`[auto-updater] applied generic feed override (${reason}): ${url}`)
|
||||
}
|
||||
|
||||
function resetPreparedFeedForLaneChange() {
|
||||
if (feedUrlOverride)
|
||||
return
|
||||
|
||||
activeFeedUrlOverride = undefined
|
||||
resolvedReleaseTag = undefined
|
||||
prepareFeedPromise = undefined
|
||||
autoUpdater.channel = undefined
|
||||
}
|
||||
|
||||
async function resolveGitHubReleaseTagForLane(lane: UpdateLane) {
|
||||
const response = await fetch(GITHUB_RELEASES_API_URL, {
|
||||
headers: {
|
||||
accept: 'application/vnd.github+json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to fetch GitHub releases (${response.status} ${response.statusText})`)
|
||||
|
||||
const payload = await response.json()
|
||||
if (!Array.isArray(payload))
|
||||
throw new Error('Unexpected GitHub releases payload shape')
|
||||
|
||||
const tag = selectLatestTagForLane(payload as GitHubReleaseRecord[], lane)
|
||||
if (!tag)
|
||||
throw new Error(`No GitHub release found for update lane "${lane}"`)
|
||||
|
||||
return tag
|
||||
}
|
||||
|
||||
async function prepareGitHubGenericFeed() {
|
||||
if (activeFeedUrlOverride)
|
||||
return
|
||||
if (resolvedReleaseTag)
|
||||
return
|
||||
if (prepareFeedPromise) {
|
||||
await prepareFeedPromise
|
||||
return
|
||||
}
|
||||
|
||||
prepareFeedPromise = (async () => {
|
||||
const preferredLane = getPreferredUpdateLane({ version: appVersion, storedLane: storedPreferredLane })
|
||||
const tag = await resolveGitHubReleaseTagForLane(preferredLane)
|
||||
resolvedReleaseTag = tag
|
||||
applyGenericFeedOverride(`${GITHUB_RELEASE_DOWNLOAD_BASE_URL}/${tag}`, `github-release-lane:${preferredLane}`)
|
||||
})()
|
||||
|
||||
try {
|
||||
await prepareFeedPromise
|
||||
}
|
||||
finally {
|
||||
prepareFeedPromise = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdatesWithPreparedFeed() {
|
||||
await prepareGitHubGenericFeed()
|
||||
await autoUpdater.checkForUpdates()
|
||||
}
|
||||
|
||||
autoUpdater.on('error', error => broadcastUpdaterError(error, 'autoUpdater error'))
|
||||
autoUpdater.on('checking-for-update', () => broadcast({ status: 'checking' }))
|
||||
autoUpdater.on('update-available', (info: UpdateInfo) => broadcast({ status: 'available', info }))
|
||||
@@ -150,8 +305,7 @@ export function setupAutoUpdater(): AutoUpdater {
|
||||
},
|
||||
}))
|
||||
|
||||
void autoUpdater
|
||||
.checkForUpdates()
|
||||
void checkForUpdatesWithPreparedFeed()
|
||||
.catch(error => broadcastUpdaterError(error, 'checkForUpdates() failed'))
|
||||
|
||||
return {
|
||||
@@ -160,7 +314,7 @@ export function setupAutoUpdater(): AutoUpdater {
|
||||
},
|
||||
async checkForUpdates() {
|
||||
broadcast({ status: 'checking' })
|
||||
await autoUpdater.checkForUpdates().catch(error => broadcastUpdaterError(error, 'checkForUpdates() failed'))
|
||||
await checkForUpdatesWithPreparedFeed().catch(error => broadcastUpdaterError(error, 'checkForUpdates() failed'))
|
||||
},
|
||||
async downloadUpdate() {
|
||||
if (state.status === 'downloading' || state.status === 'downloaded')
|
||||
@@ -188,6 +342,17 @@ export function setupAutoUpdater(): AutoUpdater {
|
||||
semaphore.release()
|
||||
}
|
||||
},
|
||||
getPreferredUpdateLane() {
|
||||
return storedPreferredLane
|
||||
},
|
||||
async setPreferredUpdateLane(lane) {
|
||||
if (storedPreferredLane === lane)
|
||||
return
|
||||
|
||||
storedPreferredLane = lane
|
||||
options.setStoredUpdateLane?.(lane)
|
||||
resetPreparedFeedForLaneChange()
|
||||
},
|
||||
subscribe(callback) {
|
||||
hooks.add(callback)
|
||||
|
||||
@@ -226,6 +391,15 @@ export function createAutoUpdaterService(params: { context: MainContext, window:
|
||||
await service.downloadUpdate()
|
||||
return service.state
|
||||
}),
|
||||
defineInvokeHandler(context, electronGetUpdaterPreferences, async () => ({
|
||||
channel: service.getPreferredUpdateLane(),
|
||||
})),
|
||||
defineInvokeHandler(context, electronSetUpdaterPreferences, async (payload) => {
|
||||
await service.setPreferredUpdateLane(payload?.channel)
|
||||
return {
|
||||
channel: service.getPreferredUpdateLane(),
|
||||
}
|
||||
}),
|
||||
defineInvokeHandler(context, autoUpdaterEventa.quitAndInstall, async () => {
|
||||
await service.quitAndInstall()
|
||||
}),
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useElectronAutoUpdater } from '@proj-airi/electron-vueuse'
|
||||
import type { ElectronUpdaterChannel } from '../../shared/eventa'
|
||||
|
||||
import { useElectronAutoUpdater, useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { AboutContent, MarkdownRenderer } from '@proj-airi/stage-ui/components'
|
||||
import { useBreakpoints } from '@proj-airi/stage-ui/composables'
|
||||
import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { Button, DoubleCheckButton, Progress } from '@proj-airi/ui'
|
||||
import { Button, DoubleCheckButton, FieldSelect, Progress } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui'
|
||||
import { DrawerContent, DrawerDescription, DrawerHandle, DrawerOverlay, DrawerPortal, DrawerRoot, DrawerTitle } from 'vaul-vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { electronGetUpdaterPreferences, electronSetUpdaterPreferences } from '../../shared/eventa'
|
||||
|
||||
const analyticsStore = useSharedAnalyticsStore()
|
||||
const { buildInfo } = storeToRefs(analyticsStore)
|
||||
@@ -34,6 +38,14 @@ const links = [
|
||||
|
||||
const showChangelog = ref(false)
|
||||
const { isDesktop } = useBreakpoints()
|
||||
const updateChannelOptions = ['auto', 'stable', 'alpha', 'beta', 'nightly', 'canary'] as const
|
||||
const updateChannelSelectOptions = updateChannelOptions.map(channel => ({
|
||||
label: channel,
|
||||
value: channel,
|
||||
}))
|
||||
type UpdateChannelOption = typeof updateChannelOptions[number]
|
||||
const selectedUpdateChannel = ref<UpdateChannelOption>('auto')
|
||||
const isUpdateChannelUpdating = ref(false)
|
||||
|
||||
const isWindowsUpdater = computed(() => {
|
||||
return updateState.value.diagnostics?.platform === 'win32'
|
||||
@@ -50,6 +62,9 @@ const restartButtonLabel = computed(() => {
|
||||
return isWindowsUpdater.value ? 'Restart to update silently' : 'Restart to install update'
|
||||
})
|
||||
|
||||
const getUpdaterPreferences = useElectronEventaInvoke(electronGetUpdaterPreferences)
|
||||
const setUpdaterPreferences = useElectronEventaInvoke(electronSetUpdaterPreferences)
|
||||
|
||||
function handleDownloadClick() {
|
||||
if (updateState.value.info?.releaseNotes)
|
||||
showChangelog.value = true
|
||||
@@ -62,6 +77,26 @@ function confirmDownload() {
|
||||
downloadUpdate()
|
||||
}
|
||||
|
||||
async function refreshUpdaterChannelPreference() {
|
||||
const preferences = await getUpdaterPreferences()
|
||||
selectedUpdateChannel.value = preferences?.channel ?? 'auto'
|
||||
}
|
||||
|
||||
async function setUpdateChannelPreference(channel: UpdateChannelOption) {
|
||||
if (isUpdateChannelUpdating.value)
|
||||
return
|
||||
|
||||
isUpdateChannelUpdating.value = true
|
||||
try {
|
||||
const nextChannel = channel === 'auto' ? undefined : channel as ElectronUpdaterChannel
|
||||
const preferences = await setUpdaterPreferences({ channel: nextChannel })
|
||||
selectedUpdateChannel.value = preferences?.channel ?? 'auto'
|
||||
}
|
||||
finally {
|
||||
isUpdateChannelUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure releaseNotes is a string for the renderer
|
||||
const releaseNotesContent = computed(() => {
|
||||
const notes = updateState.value.info?.releaseNotes
|
||||
@@ -70,6 +105,10 @@ const releaseNotesContent = computed(() => {
|
||||
}
|
||||
return typeof notes === 'string' ? notes : ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void refreshUpdaterChannelPreference()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -88,6 +127,18 @@ const releaseNotesContent = computed(() => {
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<div :class="['mb-12', 'rounded-2xl', 'bg-white/50 dark:bg-black/20', 'p-6', 'backdrop-blur-sm']">
|
||||
<FieldSelect
|
||||
:model-value="selectedUpdateChannel"
|
||||
:disabled="isUpdateChannelUpdating"
|
||||
label="Update lane"
|
||||
description="Choose which release lane updater checks against. Auto follows the current app prerelease lane."
|
||||
placeholder="Choose update lane"
|
||||
:options="updateChannelSelectOptions"
|
||||
layout="vertical"
|
||||
:class="['mb-6']"
|
||||
@update:model-value="setUpdateChannelPreference($event as UpdateChannelOption)"
|
||||
/>
|
||||
|
||||
<!-- Build Info -->
|
||||
<div :class="['flex flex-wrap items-center justify-between gap-4', 'mb-6', 'border-b border-neutral-200/50 dark:border-neutral-800/50', 'pb-6']">
|
||||
<div>
|
||||
|
||||
@@ -33,6 +33,15 @@ export const electronGetServerChannelConfig = defineInvokeEventa<ElectronServerC
|
||||
export const electronApplyServerChannelConfig = defineInvokeEventa<ElectronServerChannelConfig, Partial<ElectronServerChannelConfig>>('eventa:invoke:electron:server-channel:apply-config')
|
||||
export const electronGetServerChannelQrPayload = defineInvokeEventa<ServerChannelQrPayload>('eventa:invoke:electron:server-channel:get-qr-payload')
|
||||
|
||||
export type ElectronUpdaterChannel = 'stable' | 'alpha' | 'beta' | 'nightly' | 'canary'
|
||||
|
||||
export interface ElectronUpdaterPreferences {
|
||||
channel?: ElectronUpdaterChannel
|
||||
}
|
||||
|
||||
export const electronGetUpdaterPreferences = defineInvokeEventa<ElectronUpdaterPreferences>('eventa:invoke:electron:auto-updater:get-preferences')
|
||||
export const electronSetUpdaterPreferences = defineInvokeEventa<ElectronUpdaterPreferences, ElectronUpdaterPreferences>('eventa:invoke:electron:auto-updater:set-preferences')
|
||||
|
||||
export const electronPluginList = defineInvokeEventa<PluginRegistrySnapshot>('eventa:invoke:electron:plugins:list')
|
||||
export const electronPluginSetEnabled = defineInvokeEventa<PluginRegistrySnapshot, { name: string, enabled: boolean, path?: string }>('eventa:invoke:electron:plugins:set-enabled')
|
||||
export const electronPluginLoadEnabled = defineInvokeEventa<PluginRegistrySnapshot>('eventa:invoke:electron:plugins:load-enabled')
|
||||
|
||||
Reference in New Issue
Block a user