Skip to content

Commit 628fe28

Browse files
committed
refactor(stage-tamagotchi): improved about, better structure
1 parent b1a4eac commit 628fe28

4 files changed

Lines changed: 196 additions & 109 deletions

File tree

apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ export function setupAutoUpdater(options: AutoUpdaterOptions = {}): AutoUpdater
411411
storedPreferredLane = lane
412412
options.setStoredUpdateLane?.(lane)
413413
resetPreparedFeedForLaneChange()
414+
// Keep UI state consistent with the newly selected lane.
415+
// A fresh check runs right after channel update from renderer.
416+
broadcast({ status: 'idle' })
414417
},
415418
subscribe(callback) {
416419
hooks.add(callback)

apps/stage-tamagotchi/src/renderer/pages/about.vue

Lines changed: 139 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
<script setup lang="ts">
2+
import type { BugReportDialogSubmitPayload } from '@proj-airi/stage-ui/components'
3+
24
import type { ElectronUpdaterChannel } from '../../shared/eventa'
35
46
import { useElectronAutoUpdater, useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
5-
import { AboutContent, MarkdownRenderer } from '@proj-airi/stage-ui/components'
7+
import { AboutContent, BugReportDialog, createBugReportPageContext, MarkdownRenderer } from '@proj-airi/stage-ui/components'
68
import { useBreakpoints } from '@proj-airi/stage-ui/composables'
79
import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
8-
import { Button, DoubleCheckButton, FieldSelect, Progress } from '@proj-airi/ui'
10+
import { Button, ContainerError, DoubleCheckButton, FieldSelect, Progress } from '@proj-airi/ui'
11+
import { useClipboard } from '@vueuse/core'
912
import { storeToRefs } from 'pinia'
1013
import { DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui'
1114
import { DrawerContent, DrawerDescription, DrawerHandle, DrawerOverlay, DrawerPortal, DrawerRoot, DrawerTitle } from 'vaul-vue'
1215
import { computed, onMounted, ref } from 'vue'
16+
import { useI18n } from 'vue-i18n'
1317
1418
import { electronGetUpdaterPreferences, electronSetUpdaterPreferences } from '../../shared/eventa'
1519
1620
const analyticsStore = useSharedAnalyticsStore()
1721
const { buildInfo } = storeToRefs(analyticsStore)
22+
const { t } = useI18n()
23+
const { copy: copyToClipboard, isSupported: isClipboardSupported } = useClipboard()
1824
1925
const {
2026
state: updateState,
@@ -29,37 +35,63 @@ const isLatestVersion = computed(() => {
2935
return updateState.value.status === 'not-available' && !isDisabled.value
3036
})
3137
const isError = computed(() => updateState.value.status === 'error')
38+
const updaterErrorMessage = computed(() => {
39+
const message = updateState.value.error?.message
40+
if (typeof message === 'string')
41+
return message
42+
43+
if (message == null)
44+
return ''
45+
46+
try {
47+
return JSON.stringify(message, null, 2)
48+
}
49+
catch {
50+
return String(message)
51+
}
52+
})
3253
3354
const links = [
34-
{ label: 'Home', href: 'https://airi.moeru.ai/docs/', icon: 'i-solar:home-smile-outline' },
35-
{ label: 'Documentations', href: 'https://airi.moeru.ai/docs/en/docs/overview/', icon: 'i-solar:document-add-outline' },
36-
{ label: 'GitHub', href: 'https://github.com/moeru-ai/airi', icon: 'i-simple-icons:github' },
55+
{ labelKey: 'tamagotchi.stage.about.links.home', href: 'https://airi.moeru.ai/docs/', icon: 'i-solar:home-smile-outline' },
56+
{ labelKey: 'tamagotchi.stage.about.links.documentation', href: 'https://airi.moeru.ai/docs/en/docs/overview/', icon: 'i-solar:document-add-outline' },
57+
{ labelKey: 'tamagotchi.stage.about.links.github', href: 'https://github.com/moeru-ai/airi', icon: 'i-simple-icons:github' },
3758
]
3859
3960
const showChangelog = ref(false)
61+
const showBugReportDialog = ref(false)
4062
const { isDesktop } = useBreakpoints()
4163
const updateChannelOptions = ['auto', 'stable', 'alpha', 'beta', 'nightly', 'canary'] as const
42-
const updateChannelSelectOptions = updateChannelOptions.map(channel => ({
43-
label: channel,
64+
const updateChannelSelectOptions = computed(() => updateChannelOptions.map(channel => ({
65+
label: t(`tamagotchi.stage.about.update.channels.${channel}`),
4466
value: channel,
45-
}))
67+
})))
4668
type UpdateChannelOption = typeof updateChannelOptions[number]
4769
const selectedUpdateChannel = ref<UpdateChannelOption>('auto')
4870
const isUpdateChannelUpdating = ref(false)
71+
const bugReportDescription = ref('')
72+
const includeBugReportTriageContext = ref(false)
73+
const uploadBugReportMediaFromLibrary = ref(false)
74+
const bugReportScreenshotFiles = ref<File[] | undefined>(undefined)
75+
const bugReportSending = ref(false)
76+
const bugReportSubmitError = ref<unknown>(undefined)
77+
const bugReportPageContext = ref(createBugReportPageContext())
78+
const bugReportScreenshotAttached = ref(false)
4979
5080
const isWindowsUpdater = computed(() => {
5181
return updateState.value.diagnostics?.platform === 'win32'
5282
})
5383
5484
const downloadedStatusText = computed(() => {
5585
if (isWindowsUpdater.value)
56-
return `Update ready to install silently (v${updateState.value.info?.version}).`
86+
return t('tamagotchi.stage.about.update.status.downloaded.windows', { version: updateState.value.info?.version ?? '' })
5787
58-
return `Update ready to install on restart (v${updateState.value.info?.version}).`
88+
return t('tamagotchi.stage.about.update.status.downloaded.restart', { version: updateState.value.info?.version ?? '' })
5989
})
6090
6191
const restartButtonLabel = computed(() => {
62-
return isWindowsUpdater.value ? 'Restart to update silently' : 'Restart to install update'
92+
return isWindowsUpdater.value
93+
? t('tamagotchi.stage.about.update.actions.restart-silent')
94+
: t('tamagotchi.stage.about.update.actions.restart-install')
6395
})
6496
6597
const getUpdaterPreferences = useElectronEventaInvoke(electronGetUpdaterPreferences)
@@ -77,6 +109,45 @@ function confirmDownload() {
77109
downloadUpdate()
78110
}
79111
112+
function openBugReportDialog() {
113+
const details = [
114+
`Current version: ${buildInfo.value.version}`,
115+
`Update status: ${updateState.value.status}`,
116+
updaterErrorMessage.value ? `Error: ${updaterErrorMessage.value}` : '',
117+
]
118+
.filter(Boolean)
119+
.join('\n')
120+
121+
bugReportDescription.value = details
122+
bugReportSubmitError.value = undefined
123+
showBugReportDialog.value = true
124+
}
125+
126+
function onBugReportRequestTriageContext() {
127+
includeBugReportTriageContext.value = true
128+
bugReportScreenshotAttached.value = true
129+
bugReportPageContext.value = createBugReportPageContext()
130+
}
131+
132+
async function onBugReportSubmit(payload: BugReportDialogSubmitPayload) {
133+
bugReportSending.value = true
134+
bugReportSubmitError.value = undefined
135+
136+
try {
137+
if (!isClipboardSupported.value)
138+
throw new Error('Clipboard API is unavailable')
139+
140+
await copyToClipboard(payload.formattedReport)
141+
showBugReportDialog.value = false
142+
}
143+
catch (error) {
144+
bugReportSubmitError.value = error
145+
}
146+
finally {
147+
bugReportSending.value = false
148+
}
149+
}
150+
80151
async function refreshUpdaterChannelPreference() {
81152
const preferences = await getUpdaterPreferences()
82153
selectedUpdateChannel.value = preferences?.channel ?? 'auto'
@@ -91,6 +162,7 @@ async function setUpdateChannelPreference(channel: UpdateChannelOption) {
91162
const nextChannel = channel === 'auto' ? undefined : channel as ElectronUpdaterChannel
92163
const preferences = await setUpdaterPreferences({ channel: nextChannel })
93164
selectedUpdateChannel.value = preferences?.channel ?? 'auto'
165+
await checkForUpdates()
94166
}
95167
finally {
96168
isUpdateChannelUpdating.value = false
@@ -123,16 +195,20 @@ onMounted(() => {
123195
]"
124196
>
125197
<div :class="['mx-auto max-w-[min(960px,calc(100%-2rem))]', 'px-6 py-20']">
126-
<AboutContent title="Project" highlight="AIRI" subtitle="Desktop ver." />
198+
<AboutContent
199+
title="Project"
200+
highlight="AIRI"
201+
:subtitle="t('tamagotchi.stage.about.subtitle')"
202+
/>
127203

128204
<!-- Main Content Card -->
129205
<div :class="['mb-12', 'rounded-2xl', 'bg-white/50 dark:bg-black/20', 'p-6', 'backdrop-blur-sm']">
130206
<FieldSelect
131207
:model-value="selectedUpdateChannel"
132-
:disabled="isUpdateChannelUpdating"
133-
label="Update lane"
134-
description="Choose which release lane updater checks against. Auto follows the current app prerelease lane."
135-
placeholder="Choose update lane"
208+
:disabled="isUpdateChannelUpdating || isBusy"
209+
:label="t('tamagotchi.stage.about.update.lane.label')"
210+
:description="t('tamagotchi.stage.about.update.lane.description')"
211+
:placeholder="t('tamagotchi.stage.about.update.lane.placeholder')"
136212
:options="updateChannelSelectOptions"
137213
layout="vertical"
138214
:class="['mb-6']"
@@ -143,7 +219,7 @@ onMounted(() => {
143219
<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']">
144220
<div>
145221
<div :class="['text-sm text-neutral-500 dark:text-neutral-400']">
146-
Current Version
222+
{{ t('tamagotchi.stage.about.current-version') }}
147223
</div>
148224
<div :class="['text-xl font-medium font-mono']">
149225
{{ buildInfo.version }}
@@ -169,7 +245,7 @@ onMounted(() => {
169245
variant="primary"
170246
:loading="isBusy"
171247
icon="i-solar:download-minimalistic-outline"
172-
label="Download Update"
248+
:label="t('tamagotchi.stage.about.update.actions.download')"
173249
@click="handleDownloadClick()"
174250
/>
175251
</div>
@@ -178,7 +254,7 @@ onMounted(() => {
178254
<!-- State: Downloading -->
179255
<div v-else-if="updateState.status === 'downloading'" :class="['flex flex-col gap-2']">
180256
<div :class="['flex justify-between text-sm']">
181-
<span>Downloading update...</span>
257+
<span>{{ t('tamagotchi.stage.about.update.status.downloading') }}</span>
182258
<span :class="['font-mono']">{{ updateState.progress?.percent.toFixed(1) }}%</span>
183259
</div>
184260
<Progress :progress="updateState.progress?.percent ?? 0" />
@@ -199,22 +275,26 @@ onMounted(() => {
199275
>
200276
{{ restartButtonLabel }}
201277
<template #confirm>
202-
Confirm Restart
278+
{{ t('tamagotchi.stage.about.update.actions.confirm-restart') }}
203279
</template>
204280
<template #cancel>
205-
Cancel
281+
{{ t('tamagotchi.stage.about.common.cancel') }}
206282
</template>
207283
</DoubleCheckButton>
208284
</div>
209285
</div>
210286

211287
<!-- State: Idle, Checking, Error, Disabled, Not Available -->
212288
<div v-else :class="['flex flex-col gap-4']">
213-
<div v-if="isError" :class="['text-sm text-red-600 dark:text-red-400']">
214-
Error: {{ updateState.error?.message }}
289+
<div v-if="isError" :class="['flex flex-col gap-2']">
290+
<ContainerError
291+
:message="updaterErrorMessage"
292+
height-preset="sm"
293+
@feedback="openBugReportDialog"
294+
/>
215295
</div>
216296
<div v-else-if="isLatestVersion" :class="['text-sm text-emerald-600 dark:text-emerald-400']">
217-
Up to date (v{{ buildInfo.version }}).
297+
{{ t('tamagotchi.stage.about.update.status.latest', { version: buildInfo.version }) }}
218298
</div>
219299

220300
<div :class="['flex flex-wrap gap-2']">
@@ -223,7 +303,15 @@ onMounted(() => {
223303
:loading="isBusy"
224304
:disabled="isDisabled"
225305
:icon="isLatestVersion ? 'i-solar:check-circle-outline' : isDisabled ? 'i-solar:forbidden-circle-outline' : 'i-solar:refresh-outline'"
226-
:label="isBusy ? 'Checking...' : isLatestVersion ? 'Latest version' : isDisabled ? 'Updates disabled in Dev' : isError ? 'Retry Check' : 'Check for updates'"
306+
:label="isBusy
307+
? t('tamagotchi.stage.about.update.actions.checking')
308+
: isLatestVersion
309+
? t('tamagotchi.stage.about.update.actions.latest-version')
310+
: isDisabled
311+
? t('tamagotchi.stage.about.update.actions.disabled-dev')
312+
: isError
313+
? t('tamagotchi.stage.about.update.actions.retry-check')
314+
: t('tamagotchi.stage.about.update.actions.check-for-updates')"
227315
@click="checkForUpdates()"
228316
/>
229317
</div>
@@ -234,7 +322,7 @@ onMounted(() => {
234322
<!-- Links -->
235323
<div>
236324
<div :class="['text-neutral-500 dark:text-neutral-400 mb-4']">
237-
Links
325+
{{ t('tamagotchi.stage.about.links.title') }}
238326
</div>
239327
<div :class="['flex flex-wrap gap-2']">
240328
<a
@@ -247,7 +335,7 @@ onMounted(() => {
247335
<Button
248336
variant="secondary-muted"
249337
:icon="link.icon"
250-
:label="link.label"
338+
:label="t(link.labelKey)"
251339
size="sm"
252340
/>
253341
</a>
@@ -261,22 +349,22 @@ onMounted(() => {
261349
<DialogOverlay class="fixed inset-0 z-[9999] bg-black/50 backdrop-blur-sm data-[state=closed]:animate-fadeOut data-[state=open]:animate-fadeIn" />
262350
<DialogContent class="fixed left-1/2 top-1/2 z-[9999] max-h-[85vh] max-w-2xl w-[90vw] flex flex-col rounded-2xl bg-white p-6 shadow-xl outline-none backdrop-blur-md -translate-x-1/2 -translate-y-1/2 data-[state=closed]:animate-contentHide data-[state=open]:animate-contentShow dark:bg-neutral-900">
263351
<DialogTitle class="mb-2 text-lg font-medium">
264-
Update Available
352+
{{ t('tamagotchi.stage.about.update.dialog.title') }}
265353
</DialogTitle>
266354
<DialogDescription class="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
267-
A new version (v{{ updateState.info?.version }}) is available.
355+
{{ t('tamagotchi.stage.about.update.dialog.description', { version: updateState.info?.version }) }}
268356
</DialogDescription>
269357

270358
<div class="min-h-0 flex-1 overflow-y-auto border border-neutral-200 rounded-lg bg-neutral-50 p-4 dark:border-neutral-800 dark:bg-neutral-950/50">
271-
<MarkdownRenderer :content="releaseNotesContent || '_No release notes provided._'" class="text-sm" />
359+
<MarkdownRenderer :content="releaseNotesContent || t('tamagotchi.stage.about.update.dialog.no-release-notes-markdown')" class="text-sm" />
272360
</div>
273361

274362
<div class="mt-6 flex justify-end gap-3">
275363
<Button variant="secondary" @click="showChangelog = false">
276-
Cancel
364+
{{ t('tamagotchi.stage.about.common.cancel') }}
277365
</Button>
278366
<Button variant="primary" icon="i-solar:download-minimalistic-outline" @click="confirmDownload">
279-
Confirm Download
367+
{{ t('tamagotchi.stage.about.update.actions.confirm-download') }}
280368
</Button>
281369
</div>
282370
</DialogContent>
@@ -291,28 +379,42 @@ onMounted(() => {
291379
<div class="flex flex-1 flex-col rounded-t-2xl bg-white p-4 dark:bg-neutral-900">
292380
<DrawerHandle class="mx-auto mb-4 h-1.5 w-12 rounded-full bg-neutral-300 dark:bg-neutral-700" />
293381
<DrawerTitle class="mb-2 text-lg font-medium">
294-
Update Available
382+
{{ t('tamagotchi.stage.about.update.dialog.title') }}
295383
</DrawerTitle>
296384
<DrawerDescription class="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
297-
A new version (v{{ updateState.info?.version }}) is available.
385+
{{ t('tamagotchi.stage.about.update.dialog.description', { version: updateState.info?.version }) }}
298386
</DrawerDescription>
299387

300388
<div class="min-h-0 flex-1 overflow-y-auto border border-neutral-200 rounded-lg bg-neutral-50 p-4 dark:border-neutral-800 dark:bg-neutral-950/50">
301-
<MarkdownRenderer :content="releaseNotesContent || '_No release notes provided._'" class="text-sm" />
389+
<MarkdownRenderer :content="releaseNotesContent || t('tamagotchi.stage.about.update.dialog.no-release-notes-markdown')" class="text-sm" />
302390
</div>
303391

304392
<div class="mt-4 flex gap-3">
305393
<Button variant="secondary" block @click="showChangelog = false">
306-
Cancel
394+
{{ t('tamagotchi.stage.about.common.cancel') }}
307395
</Button>
308396
<Button variant="primary" block icon="i-solar:download-minimalistic-outline" @click="confirmDownload">
309-
Download
397+
{{ t('tamagotchi.stage.about.update.actions.download-short') }}
310398
</Button>
311399
</div>
312400
</div>
313401
</DrawerContent>
314402
</DrawerPortal>
315403
</DrawerRoot>
404+
405+
<BugReportDialog
406+
v-model="showBugReportDialog"
407+
v-model:description="bugReportDescription"
408+
v-model:include-triage-context="includeBugReportTriageContext"
409+
v-model:upload-media-from-library="uploadBugReportMediaFromLibrary"
410+
v-model:screenshot-files="bugReportScreenshotFiles"
411+
:sending="bugReportSending"
412+
:submit-error="bugReportSubmitError"
413+
:page-context="bugReportPageContext"
414+
:screenshot-attached="bugReportScreenshotAttached"
415+
@request-triage-context="onBugReportRequestTriageContext"
416+
@submit="onBugReportSubmit"
417+
/>
316418
</div>
317419
</template>
318420

0 commit comments

Comments
 (0)