11<script setup lang="ts">
2+ import type { BugReportDialogSubmitPayload } from ' @proj-airi/stage-ui/components'
3+
24import type { ElectronUpdaterChannel } from ' ../../shared/eventa'
35
46import { 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'
68import { useBreakpoints } from ' @proj-airi/stage-ui/composables'
79import { 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'
912import { storeToRefs } from ' pinia'
1013import { DialogContent , DialogDescription , DialogOverlay , DialogPortal , DialogRoot , DialogTitle } from ' reka-ui'
1114import { DrawerContent , DrawerDescription , DrawerHandle , DrawerOverlay , DrawerPortal , DrawerRoot , DrawerTitle } from ' vaul-vue'
1215import { computed , onMounted , ref } from ' vue'
16+ import { useI18n } from ' vue-i18n'
1317
1418import { electronGetUpdaterPreferences , electronSetUpdaterPreferences } from ' ../../shared/eventa'
1519
1620const analyticsStore = useSharedAnalyticsStore ()
1721const { buildInfo } = storeToRefs (analyticsStore )
22+ const { t } = useI18n ()
23+ const { copy : copyToClipboard, isSupported : isClipboardSupported } = useClipboard ()
1824
1925const {
2026 state : updateState,
@@ -29,37 +35,63 @@ const isLatestVersion = computed(() => {
2935 return updateState .value .status === ' not-available' && ! isDisabled .value
3036})
3137const 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
3354const 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
3960const showChangelog = ref (false )
61+ const showBugReportDialog = ref (false )
4062const { isDesktop } = useBreakpoints ()
4163const 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+ })))
4668type UpdateChannelOption = typeof updateChannelOptions [number ]
4769const selectedUpdateChannel = ref <UpdateChannelOption >(' auto' )
4870const 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
5080const isWindowsUpdater = computed (() => {
5181 return updateState .value .diagnostics ?.platform === ' win32'
5282})
5383
5484const 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
6191const 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
6597const 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+
80151async 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