Skip to content

Commit 4a29598

Browse files
authored
feat(stage-*): refactor settings data page and add app data folder action (#1447)
------ Co-authored-by-agent: Codex
1 parent e952fe7 commit 4a29598

17 files changed

Lines changed: 598 additions & 275 deletions

File tree

apps/stage-tamagotchi/electron.vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ export default defineConfig({
207207
exclude: base => [
208208
...base,
209209
'**/settings/connection/index.vue',
210+
'**/settings/data/index.vue',
210211
'**/settings/system/general.vue',
211212
'**/settings/modules/mcp.vue',
212213
],
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { createContext, defineInvoke } from '@moeru/eventa'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { electronAppOpenUserDataFolder } from '../../../shared/eventa'
5+
import { createAppService } from './app'
6+
7+
const appMock = vi.hoisted(() => ({
8+
getPath: vi.fn(),
9+
quit: vi.fn(),
10+
}))
11+
12+
const shellMock = vi.hoisted(() => ({
13+
openPath: vi.fn(),
14+
}))
15+
16+
vi.mock('electron', () => ({
17+
app: appMock,
18+
shell: shellMock,
19+
}))
20+
21+
vi.mock('std-env', () => ({
22+
isLinux: false,
23+
isMacOS: false,
24+
isWindows: true,
25+
}))
26+
27+
describe('createAppService', () => {
28+
beforeEach(() => {
29+
vi.clearAllMocks()
30+
})
31+
32+
it('opens the Electron userData folder and returns its path', async () => {
33+
const context = createContext()
34+
appMock.getPath.mockReturnValue('/tmp/airi-user-data')
35+
shellMock.openPath.mockResolvedValue('')
36+
37+
createAppService({ context: context as never, window: {} as never })
38+
39+
const openUserDataFolder = defineInvoke(context, electronAppOpenUserDataFolder)
40+
41+
await expect(openUserDataFolder()).resolves.toEqual({ path: '/tmp/airi-user-data' })
42+
expect(appMock.getPath).toHaveBeenCalledWith('userData')
43+
expect(shellMock.openPath).toHaveBeenCalledWith('/tmp/airi-user-data')
44+
})
45+
46+
it('throws when Electron fails to open the userData folder', async () => {
47+
const context = createContext()
48+
appMock.getPath.mockReturnValue('/tmp/airi-user-data')
49+
shellMock.openPath.mockResolvedValue('Failed to open path')
50+
51+
createAppService({ context: context as never, window: {} as never })
52+
53+
const openUserDataFolder = defineInvoke(context, electronAppOpenUserDataFolder)
54+
55+
await expect(openUserDataFolder()).rejects.toThrow('Failed to open path')
56+
expect(appMock.getPath).toHaveBeenCalledWith('userData')
57+
expect(shellMock.openPath).toHaveBeenCalledWith('/tmp/airi-user-data')
58+
})
59+
})

apps/stage-tamagotchi/src/main/services/electron/app.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@ import type { createContext } from '@moeru/eventa/adapters/electron/main'
22
import type { BrowserWindow } from 'electron'
33

44
import { defineInvokeHandler } from '@moeru/eventa'
5-
import { app } from 'electron'
5+
import { app, shell } from 'electron'
66
import { isLinux, isMacOS, isWindows } from 'std-env'
77

8-
import { electron, electronAppQuit } from '../../../shared/eventa'
8+
import { electron, electronAppOpenUserDataFolder, electronAppQuit } from '../../../shared/eventa'
99

1010
export function createAppService(params: { context: ReturnType<typeof createContext>['context'], window: BrowserWindow }) {
1111
defineInvokeHandler(params.context, electron.app.isMacOS, () => isMacOS)
1212
defineInvokeHandler(params.context, electron.app.isWindows, () => isWindows)
1313
defineInvokeHandler(params.context, electron.app.isLinux, () => isLinux)
14+
defineInvokeHandler(params.context, electronAppOpenUserDataFolder, async () => {
15+
const path = app.getPath('userData')
16+
const openResult = await shell.openPath(path)
17+
if (openResult) {
18+
throw new Error(openResult)
19+
}
20+
return { path }
21+
})
1422
defineInvokeHandler(params.context, electronAppQuit, () => app.quit())
1523
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<script setup lang="ts">
2+
import type { DataSettingsStatusEmits } from '@proj-airi/stage-pages/pages/settings/data/status'
3+
4+
import { defineInvoke } from '@moeru/eventa'
5+
import { createContext } from '@moeru/eventa/adapters/electron/renderer'
6+
import { createDataSettingsStatusHelpers } from '@proj-airi/stage-pages/pages/settings/data/status'
7+
import { isElectronWindow } from '@proj-airi/stage-shared'
8+
import { Button } from '@proj-airi/ui'
9+
import { useI18n } from 'vue-i18n'
10+
11+
import { electronAppOpenUserDataFolder } from '../../../../../shared/eventa'
12+
13+
const emit = defineEmits<DataSettingsStatusEmits>()
14+
const { t } = useI18n()
15+
const { handleActionError } = createDataSettingsStatusHelpers(emit)
16+
17+
async function triggerOpenDesktopUserDataFolder() {
18+
if (typeof window === 'undefined' || !isElectronWindow(window))
19+
return
20+
21+
try {
22+
const { context } = createContext(window.electron.ipcRenderer)
23+
const openUserDataFolder = defineInvoke(context, electronAppOpenUserDataFolder)
24+
25+
await openUserDataFolder()
26+
}
27+
catch (error) {
28+
handleActionError(error)
29+
}
30+
}
31+
</script>
32+
33+
<template>
34+
<div :class="['border-2 border-sky-200/80 rounded-xl bg-sky-50/80 p-4 shadow-sm', 'dark:border-sky-500/40 dark:bg-sky-500/10']">
35+
<div :class="['grid grid-cols-1 items-start gap-3 md:grid-cols-[minmax(0,1fr)_auto]']">
36+
<div :class="['flex flex-col gap-1 md:max-w-[560px]']">
37+
<div :class="['text-lg text-sky-700 font-medium dark:text-sky-200']">
38+
{{ t('settings.pages.data.sections.desktop-folder.title') }}
39+
</div>
40+
<p :class="['text-sm text-sky-700/80 dark:text-sky-200/80']">
41+
{{ t('settings.pages.data.sections.desktop-folder.description') }}
42+
</p>
43+
</div>
44+
<div :class="['flex flex-col items-start gap-2']">
45+
<Button variant="secondary" @click="triggerOpenDesktopUserDataFolder">
46+
{{ t('settings.pages.data.sections.desktop-folder.open') }}
47+
</Button>
48+
</div>
49+
</div>
50+
</div>
51+
</template>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script setup lang="ts">
2+
import type { DataSettingsStatusEmits } from '@proj-airi/stage-pages/pages/settings/data/status'
3+
4+
import { createDataSettingsStatusHelpers } from '@proj-airi/stage-pages/pages/settings/data/status'
5+
import { useDataMaintenance } from '@proj-airi/stage-ui/composables/use-data-maintenance'
6+
import { DoubleCheckButton } from '@proj-airi/ui'
7+
import { useI18n } from 'vue-i18n'
8+
9+
const emit = defineEmits<DataSettingsStatusEmits>()
10+
const { t } = useI18n()
11+
const { resetDesktopApplicationState } = useDataMaintenance()
12+
const { emitStatus, handleActionError } = createDataSettingsStatusHelpers(emit)
13+
14+
async function resetDesktopState() {
15+
try {
16+
await resetDesktopApplicationState()
17+
emitStatus(t('settings.pages.data.status.desktop_reset'))
18+
}
19+
catch (error) {
20+
handleActionError(error)
21+
}
22+
}
23+
</script>
24+
25+
<template>
26+
<div :class="['border-2 border-amber-300/80 rounded-xl bg-amber-50/80 p-4 shadow-sm', 'dark:border-amber-500/60 dark:bg-amber-500/10']">
27+
<div :class="['grid grid-cols-1 items-start gap-3 md:grid-cols-[minmax(0,1fr)_auto]']">
28+
<div :class="['flex flex-col gap-1 md:max-w-[560px]']">
29+
<div :class="['text-lg text-amber-700 font-medium dark:text-amber-200']">
30+
{{ t('settings.pages.data.sections.desktop.title') }}
31+
</div>
32+
<p :class="['text-sm text-amber-700/80 dark:text-amber-200/80']">
33+
{{ t('settings.pages.data.sections.desktop.description') }}
34+
</p>
35+
</div>
36+
<div :class="['flex flex-col items-start gap-2']">
37+
<DoubleCheckButton variant="caution" @confirm="resetDesktopState">
38+
{{ t('settings.pages.data.sections.desktop.reset') }}
39+
<template #confirm>
40+
{{ t('settings.pages.data.confirmations.yes') }}
41+
</template>
42+
<template #cancel>
43+
{{ t('settings.pages.card.cancel') }}
44+
</template>
45+
</DoubleCheckButton>
46+
</div>
47+
</div>
48+
</div>
49+
</template>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script setup lang="ts">
2+
import ChatsSection from '@proj-airi/stage-pages/pages/settings/data/components/chats-section.vue'
3+
import DangerSection from '@proj-airi/stage-pages/pages/settings/data/components/danger-section.vue'
4+
import ModelsModulesSection from '@proj-airi/stage-pages/pages/settings/data/components/models-modules-section.vue'
5+
import StatusBanner from '@proj-airi/stage-pages/pages/settings/data/components/status-banner.vue'
6+
7+
import { createDataSettingsStatusState } from '@proj-airi/stage-pages/pages/settings/data/status'
8+
9+
import DesktopFolderSection from './components/desktop-folder-section.vue'
10+
import DesktopResetSection from './components/desktop-reset-section.vue'
11+
12+
const { statusMessage, statusTone, handleStatus } = createDataSettingsStatusState()
13+
</script>
14+
15+
<template>
16+
<div :class="['flex flex-col gap-4 pb-4']">
17+
<StatusBanner v-if="statusMessage" :message="statusMessage" :tone="statusTone" />
18+
<ChatsSection @status="handleStatus" />
19+
<ModelsModulesSection @status="handleStatus" />
20+
<DesktopFolderSection @status="handleStatus" />
21+
<DesktopResetSection @status="handleStatus" />
22+
<DangerSection @status="handleStatus" />
23+
</div>
24+
</template>
25+
26+
<route lang="yaml">
27+
meta:
28+
layout: settings
29+
titleKey: settings.pages.data.title
30+
subtitleKey: settings.title
31+
descriptionKey: settings.pages.data.description
32+
icon: i-solar:database-bold-duotone
33+
settingsEntry: true
34+
order: 7
35+
stageTransition:
36+
name: slide
37+
pageSpecificAvailable: true
38+
</route>

apps/stage-tamagotchi/src/shared/eventa.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ export interface ElectronWindowLifecycleState {
226226
export const electronWindowLifecycleChanged = defineEventa<ElectronWindowLifecycleState>('eventa:event:electron:window:lifecycle-changed')
227227
export const electronGetWindowLifecycleState = defineInvokeEventa<ElectronWindowLifecycleState>('eventa:invoke:electron:window:get-lifecycle-state')
228228
export const electronWindowSetAlwaysOnTop = defineInvokeEventa<void, boolean>('eventa:invoke:electron:window:set-always-on-top')
229+
export const electronAppOpenUserDataFolder = defineInvokeEventa<{ path: string }>('eventa:invoke:electron:app:open-user-data-folder')
229230
export const electronAppQuit = defineInvokeEventa<void>('eventa:invoke:electron:app:quit')
230231

231232
export type StageThreeRuntimeTraceEnvelope

apps/stage-tamagotchi/tsconfig.web.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
"useDefineForClassFields": true,
1414
"baseUrl": "./src/renderer",
1515
"paths": {
16+
"@proj-airi/stage-pages/*.vue": [
17+
"../../packages/stage-pages/src/*.vue"
18+
],
19+
"@proj-airi/stage-pages/*": [
20+
"../../packages/stage-pages/src/*.ts"
21+
],
1622
"@proj-airi/stage-ui/*": [
1723
"../../packages/stage-ui/src/*"
1824
],

packages/i18n/src/locales/en/settings.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ pages:
224224
title: Delete all data
225225
description: Wipe every local setting, provider config, and model.
226226
delete: Delete all data
227+
desktop-folder:
228+
title: Open app data folder
229+
description: Open AIRI's data folder in your file manager.
230+
open: Open folder
227231
desktop:
228232
title: Reset desktop settings & states
229233
description: Clear AIRI desktop settings and runtime state.

packages/i18n/src/locales/zh-Hans/settings.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ pages:
217217
title: 删除所有数据
218218
description: 清除每个本地设置、提供商配置和模型。
219219
delete: 删除所有数据
220+
desktop-folder:
221+
title: 打开应用数据文件夹
222+
description: 在文件管理器中打开 AIRI 的数据文件夹。
223+
open: 打开文件夹
220224
desktop:
221225
title: 重置桌面设置和状态
222226
description: 清除 AIRI 桌面设置和运行状态。

0 commit comments

Comments
 (0)