diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..0b6b9a649 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,39 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Main Process", + "type": "node", + "request": "launch", + "cwd": "${workspaceRoot}", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", + "windows": { + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" + }, + "runtimeArgs": ["--sourcemap"], + "env": { + "REMOTE_DEBUGGING_PORT": "9222" + } + }, + { + "name": "Debug Renderer Process", + "port": 9222, + "request": "attach", + "type": "chrome", + "webRoot": "${workspaceFolder}/src/renderer", + "timeout": 60000, + "presentation": { + "hidden": true + } + } + ], + "compounds": [ + { + "name": "Debug All", + "configurations": ["Debug Main Process", "Debug Renderer Process"], + "presentation": { + "order": 1 + } + } + ] +} diff --git a/apps/stage-tamagotchi-electron/build/entitlements.mac.plist b/apps/stage-tamagotchi-electron/build/entitlements.mac.plist new file mode 100644 index 000000000..38c887b21 --- /dev/null +++ b/apps/stage-tamagotchi-electron/build/entitlements.mac.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/apps/stage-tamagotchi-electron/build/icon.icns b/apps/stage-tamagotchi-electron/build/icon.icns new file mode 100644 index 000000000..bb55db7e3 Binary files /dev/null and b/apps/stage-tamagotchi-electron/build/icon.icns differ diff --git a/apps/stage-tamagotchi-electron/build/icon.ico b/apps/stage-tamagotchi-electron/build/icon.ico new file mode 100644 index 000000000..896bc6789 Binary files /dev/null and b/apps/stage-tamagotchi-electron/build/icon.ico differ diff --git a/apps/stage-tamagotchi-electron/build/icon.png b/apps/stage-tamagotchi-electron/build/icon.png new file mode 100644 index 000000000..7aeb7eccb Binary files /dev/null and b/apps/stage-tamagotchi-electron/build/icon.png differ diff --git a/apps/stage-tamagotchi-electron/dev-app-update.yml b/apps/stage-tamagotchi-electron/dev-app-update.yml new file mode 100644 index 000000000..03ae20e30 --- /dev/null +++ b/apps/stage-tamagotchi-electron/dev-app-update.yml @@ -0,0 +1,3 @@ +provider: generic +url: https://example.com/auto-updates +updaterCacheDirName: stage-tamagotchi-electron-updater diff --git a/apps/stage-tamagotchi-electron/electron-builder.yml b/apps/stage-tamagotchi-electron/electron-builder.yml new file mode 100644 index 000000000..ae958338a --- /dev/null +++ b/apps/stage-tamagotchi-electron/electron-builder.yml @@ -0,0 +1,43 @@ +appId: com.electron.app +productName: stage-tamagotchi-electron +directories: + buildResources: build +files: + - '!**/.vscode/*' + - '!src/*' + - '!electron.vite.config.{js,ts,mjs,cjs}' + - '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' + - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' + - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' +asarUnpack: + - resources/** +win: + executableName: stage-tamagotchi-electron +nsis: + artifactName: ${name}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always +mac: + entitlementsInherit: build/entitlements.mac.plist + extendInfo: + - NSCameraUsageDescription: Application requests access to the device's camera. + - NSMicrophoneUsageDescription: Application requests access to the device's microphone. + - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. + - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. + notarize: false +dmg: + artifactName: ${name}-${version}.${ext} +linux: + target: + - AppImage + - snap + - deb + maintainer: electronjs.org + category: Utility +appImage: + artifactName: ${name}-${version}.${ext} +npmRebuild: false +publish: + provider: generic + url: https://example.com/auto-updates diff --git a/apps/stage-tamagotchi-electron/electron.vite.config.ts b/apps/stage-tamagotchi-electron/electron.vite.config.ts new file mode 100644 index 000000000..3dc68659f --- /dev/null +++ b/apps/stage-tamagotchi-electron/electron.vite.config.ts @@ -0,0 +1,86 @@ +import { join, resolve } from 'node:path' + +import VueI18n from '@intlify/unplugin-vue-i18n/vite' +import Vue from '@vitejs/plugin-vue' +import UnoCss from 'unocss/vite' +import VueMacros from 'unplugin-vue-macros/vite' +import VueRouter from 'unplugin-vue-router/vite' +import Yaml from 'unplugin-yaml/vite' +import VitePluginVueDevTools from 'vite-plugin-vue-devtools' +import Layouts from 'vite-plugin-vue-layouts' + +import { Download } from '@proj-airi/unplugin-fetch' +import { DownloadLive2DSDK } from '@proj-airi/unplugin-live2d-sdk' +import { templateCompilerOptions } from '@tresjs/core' +import { defineConfig, externalizeDepsPlugin } from 'electron-vite' + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + }, + preload: { + plugins: [externalizeDepsPlugin()], + }, + renderer: { + optimizeDeps: { + exclude: [ + '@proj-airi/stage-ui/*', + '@proj-airi/drizzle-duckdb-wasm', + '@proj-airi/drizzle-duckdb-wasm/*', + ], + }, + resolve: { + alias: { + '@proj-airi/stage-ui/components/scenarios/settings/model-settings': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src', 'components', 'Scenarios', 'Settings', 'ModelSettings')), + '@proj-airi/stage-ui/components/scenes': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src', 'components', 'Scenes')), + '@proj-airi/stage-ui/stores': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src', 'stores')), + '@proj-airi/stage-ui': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src')), + '@proj-airi/i18n': resolve(join(import.meta.dirname, '..', '..', 'packages', 'i18n', 'src')), + }, + }, + plugins: [ + Yaml(), + + VueMacros({ + plugins: { + vue: Vue({ + include: [/\.vue$/, /\.md$/], + ...templateCompilerOptions, + }), + vueJsx: false, + }, + betterDefine: false, + }), + + VueRouter({ + dts: resolve(import.meta.dirname, 'src/renderer/typed-router.d.ts'), + routesFolder: 'src/renderer/pages', + }), + + VitePluginVueDevTools(), + + // https://github.com/JohnCampionJr/vite-plugin-vue-layouts + Layouts(), + + UnoCss(), + + // https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n + VueI18n({ + runtimeOnly: true, + compositionOnly: true, + fullInstall: true, + }), + + DownloadLive2DSDK(), + Download('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', 'hiyori_free_zh.zip', 'assets/live2d/models'), + Download('https://dist.ayaka.moe/live2d-models/hiyori_pro_zh.zip', 'hiyori_pro_zh.zip', 'assets/live2d/models'), + Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-A/AvatarSample_A.vrm', 'AvatarSample_A.vrm', 'assets/vrm/models/AvatarSample-A'), + Download('https://dist.ayaka.moe/vrm-models/VRoid-Hub/AvatarSample-B/AvatarSample_B.vrm', 'AvatarSample_B.vrm', 'assets/vrm/models/AvatarSample-B'), + ], + server: { + watch: { + ignored: ['**/src-tauri/**'], + }, + }, + }, +}) diff --git a/apps/stage-tamagotchi-electron/package.json b/apps/stage-tamagotchi-electron/package.json new file mode 100644 index 000000000..cdea23160 --- /dev/null +++ b/apps/stage-tamagotchi-electron/package.json @@ -0,0 +1,142 @@ +{ + "name": "@proj-airi/stage-tamagotchi-electron", + "version": "1.0.0", + "description": "An Electron application with Vue and TypeScript", + "author": "example.com", + "homepage": "https://electron-vite.org", + "main": "./out/main/index.js", + "scripts": { + "lint": "eslint --cache .", + "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", + "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", + "typecheck": "pnpm run typecheck:node && pnpm run typecheck:web", + "start": "electron-vite preview", + "dev": "electron-vite dev", + "build": "pnpm run typecheck && electron-vite build", + "postinstall": "electron-builder install-app-deps", + "build:unpack": "pnpm run build && electron-builder --dir", + "build:win": "pnpm run build && electron-builder --win", + "build:mac": "pnpm run build && electron-builder --mac", + "build:linux": "pnpm run build && electron-builder --linux" + }, + "dependencies": { + "@date-fns/utc": "^2.1.1", + "@electron-toolkit/preload": "^3.0.2", + "@electron-toolkit/utils": "^4.0.0", + "@formkit/auto-animate": "^0.8.4", + "@huggingface/transformers": "^3.7.2", + "@moeru/std": "catalog:", + "@proj-airi/audio": "workspace:^", + "@proj-airi/ccc": "workspace:^", + "@proj-airi/drizzle-duckdb-wasm": "catalog:", + "@proj-airi/font-cjkfonts-allseto": "workspace:^", + "@proj-airi/font-xiaolai": "workspace:^", + "@proj-airi/i18n": "workspace:^", + "@proj-airi/stage-ui": "workspace:^", + "@proj-airi/ui": "workspace:^", + "@ricky0123/vad-web": "^0.0.26", + "@stdlib/string-base-kebabcase": "^0.2.2", + "@tresjs/cientos": "^4.3.1", + "@tresjs/core": "^4.3.6", + "@types/audioworklet": "^0.0.83", + "@unbird/eventa": "^0.0.5", + "@vueuse/core": "^13.9.0", + "@vueuse/shared": "^13.9.0", + "@xsai-ext/providers-cloud": "catalog:", + "@xsai-ext/providers-local": "catalog:", + "@xsai-ext/shared-providers": "catalog:", + "@xsai/generate-speech": "catalog:", + "@xsai/generate-text": "catalog:", + "@xsai/model": "catalog:", + "@xsai/shared": "catalog:", + "@xsai/shared-chat": "catalog:", + "@xsai/stream-text": "catalog:", + "@xsai/utils-chat": "catalog:", + "colorjs.io": "^0.5.2", + "culori": "^4.0.2", + "date-fns": "^4.1.0", + "drizzle-kit": "^0.31.4", + "drizzle-orm": "^0.44.5", + "jszip": "^3.10.1", + "localforage": "^1.10.0", + "mediabunny": "^1.13.0", + "node-vibrant": "^4.0.3", + "nprogress": "^0.2.0", + "ofetch": "^1.4.1", + "onnxruntime-web": "^1.22.0", + "pinia": "^3.0.3", + "popmotion": "^11.0.5", + "rehype-stringify": "^10.0.1", + "reka-ui": "^2.5.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "shiki": "^3.12.2", + "three": "^0.179.1", + "unified": "^11.0.5", + "unspeech": "^0.1.7", + "valibot": "1.0.0-beta.9", + "vaul-vue": "^0.4.1", + "vue": "^3.5.21", + "vue-demi": "^0.14.10", + "vue-i18n": "^11.1.11", + "vue-router": "^4.5.1", + "xsschema": "catalog:", + "yauzl": "^3.2.0", + "zod": "^4.1.5" + }, + "devDependencies": { + "@electron-toolkit/tsconfig": "^1.0.1", + "@fontsource-variable/dm-sans": "^5.2.6", + "@fontsource-variable/jura": "^5.2.6", + "@fontsource-variable/quicksand": "^5.2.8", + "@fontsource-variable/urbanist": "^5.2.6", + "@fontsource/dm-mono": "^5.2.6", + "@fontsource/dm-serif-display": "^5.2.6", + "@fontsource/gugi": "^5.2.6", + "@fontsource/kiwi-maru": "^5.2.6", + "@fontsource/m-plus-rounded-1c": "^5.2.8", + "@fontsource/sniglet": "^5.2.6", + "@iconify-json/carbon": "^1.2.13", + "@iconify-json/eos-icons": "^1.2.4", + "@iconify-json/lucide": "^1.2.66", + "@iconify-json/mingcute": "^1.2.5", + "@iconify-json/ph": "^1.2.2", + "@iconify-json/simple-icons": "^1.2.50", + "@iconify-json/solar": "^1.2.4", + "@iconify-json/svg-spinners": "^1.2.4", + "@iconify-json/vscode-icons": "^1.2.30", + "@iconify/utils": "^3.0.1", + "@intlify/unplugin-vue-i18n": "^6.0.8", + "@proj-airi/lobe-icons": "^1.0.10", + "@proj-airi/ui-transitions": "workspace:^", + "@proj-airi/unplugin-fetch": "^0.1.6", + "@proj-airi/unplugin-live2d-sdk": "^0.1.5", + "@shikijs/markdown-it": "^3.12.2", + "@types/culori": "^4.0.1", + "@types/nprogress": "^0.2.3", + "@types/three": "^0.179.0", + "@types/yauzl": "^2.10.3", + "@unocss/reset": "^66.5.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vue-macros/volar": "^3.0.0-beta.22", + "@vueuse/motion": "^3.0.3", + "@xsai-transformers/embed": "^0.0.7", + "cac": "^6.7.14", + "csstype": "^3.1.3", + "electron": "^37.2.3", + "electron-builder": "^25.1.8", + "electron-vite": "^4.0.0", + "execa": "^9.6.0", + "less": "^4.4.1", + "unocss-preset-scrollbar": "^3.2.0", + "unplugin-info": "^1.2.4", + "unplugin-vue-macros": "^2.14.5", + "unplugin-vue-router": "^0.15.0", + "unplugin-yaml": "^3.0.5", + "vite": "catalog:rolldown-vite", + "vite-bundle-visualizer": "^1.2.1", + "vite-plugin-vue-devtools": "^8.0.1", + "vite-plugin-vue-layouts": "^0.11.0", + "vue-tsc": "^3.0.6" + } +} diff --git a/apps/stage-tamagotchi-electron/resources/icon.png b/apps/stage-tamagotchi-electron/resources/icon.png new file mode 100644 index 000000000..bcbd07040 Binary files /dev/null and b/apps/stage-tamagotchi-electron/resources/icon.png differ diff --git a/apps/stage-tamagotchi-electron/src/main/index.ts b/apps/stage-tamagotchi-electron/src/main/index.ts new file mode 100644 index 000000000..902d05c67 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/main/index.ts @@ -0,0 +1,80 @@ +import { join } from 'node:path' +import { env, platform } from 'node:process' + +import { electronApp, is, optimizer } from '@electron-toolkit/utils' +import { app, BrowserWindow, ipcMain, shell } from 'electron' + +import icon from '../../resources/icon.png?asset' + +function createWindow(): void { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 900, + height: 670, + show: false, + autoHideMenuBar: true, + ...(platform === 'linux' ? { icon } : {}), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false, + }, + }) + + mainWindow.on('ready-to-show', () => { + mainWindow.show() + }) + + mainWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + + // HMR for renderer base on electron-vite cli. + // Load the remote URL for development or the local html file for production. + if (is.dev && env.ELECTRON_RENDERER_URL) { + mainWindow.loadURL(env.ELECTRON_RENDERER_URL) + } + else { + mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + // Set app user model id for windows + electronApp.setAppUserModelId('com.electron') + + // Default open or close DevTools by F12 in development + // and ignore CommandOrControl + R in production. + // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils + app.on('browser-window-created', (_, window) => { + optimizer.watchWindowShortcuts(window) + }) + + // IPC test + // eslint-disable-next-line no-console + ipcMain.on('ping', () => console.log('pong')) + + createWindow() + + app.on('activate', () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) + createWindow() + }) +}) + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', () => { + if (platform !== 'darwin') { + app.quit() + } +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/apps/stage-tamagotchi-electron/src/preload/index.d.ts b/apps/stage-tamagotchi-electron/src/preload/index.d.ts new file mode 100644 index 000000000..e85d4c813 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/preload/index.d.ts @@ -0,0 +1,8 @@ +import type { ElectronAPI } from '@electron-toolkit/preload' + +declare global { + interface Window { + electron: ElectronAPI + api: unknown + } +} diff --git a/apps/stage-tamagotchi-electron/src/preload/index.ts b/apps/stage-tamagotchi-electron/src/preload/index.ts new file mode 100644 index 000000000..c6004a70e --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/preload/index.ts @@ -0,0 +1,26 @@ +import { contextIsolated } from 'node:process' + +import { electronAPI } from '@electron-toolkit/preload' +import { contextBridge } from 'electron' + +// Custom APIs for renderer +const api = {} + +// Use `contextBridge` APIs to expose Electron APIs to +// renderer only if context isolation is enabled, otherwise +// just add to the DOM global. +if (contextIsolated) { + try { + contextBridge.exposeInMainWorld('electron', electronAPI) + contextBridge.exposeInMainWorld('api', api) + } + catch (error) { + console.error(error) + } +} +else { + // @ts-expect-error (define in dts) + window.electron = electronAPI + // @ts-expect-error (define in dts) + window.api = api +} diff --git a/apps/stage-tamagotchi-electron/src/renderer/App.vue b/apps/stage-tamagotchi-electron/src/renderer/App.vue new file mode 100644 index 000000000..2859e9b21 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/App.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/bindings/tauri-plugins/window-pass-through-on-hover.ts b/apps/stage-tamagotchi-electron/src/renderer/bindings/tauri-plugins/window-pass-through-on-hover.ts new file mode 100644 index 000000000..924d33824 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/bindings/tauri-plugins/window-pass-through-on-hover.ts @@ -0,0 +1,97 @@ +// @ts-nocheck + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async startPassThrough() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:window-pass-through-on-hover|start_pass_through") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async stopPassThrough() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:window-pass-through-on-hover|stop_pass_through") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +} +} + +/** user-defined events **/ + + + +/** user-defined constants **/ + + + +/** user-defined types **/ + + + +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/apps/stage-tamagotchi-electron/src/renderer/bindings/tauri-plugins/window-router-link.ts b/apps/stage-tamagotchi-electron/src/renderer/bindings/tauri-plugins/window-router-link.ts new file mode 100644 index 000000000..e050370ed --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/bindings/tauri-plugins/window-router-link.ts @@ -0,0 +1,89 @@ +// @ts-nocheck + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async go(route: string, windowLabel: string | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:window-router-link|go", { route, windowLabel }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +} +} + +/** user-defined events **/ + + + +/** user-defined constants **/ + + + +/** user-defined types **/ + + + +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/apps/stage-tamagotchi-electron/src/renderer/components/ChatHistory.vue b/apps/stage-tamagotchi-electron/src/renderer/components/ChatHistory.vue new file mode 100644 index 000000000..f152df780 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/components/ChatHistory.vue @@ -0,0 +1,143 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/components/IconAnimation.vue b/apps/stage-tamagotchi-electron/src/renderer/components/IconAnimation.vue new file mode 100644 index 000000000..b44726579 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/components/IconAnimation.vue @@ -0,0 +1,84 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/components/InteractiveArea.vue b/apps/stage-tamagotchi-electron/src/renderer/components/InteractiveArea.vue new file mode 100644 index 000000000..04c8a02fc --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/components/InteractiveArea.vue @@ -0,0 +1,180 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/components/Settings/CheckBar.vue b/apps/stage-tamagotchi-electron/src/renderer/components/Settings/CheckBar.vue new file mode 100644 index 000000000..27b6ddc92 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/components/Settings/CheckBar.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/components/Tauri/WindowRouterLink.vue b/apps/stage-tamagotchi-electron/src/renderer/components/Tauri/WindowRouterLink.vue new file mode 100644 index 000000000..df2a2f4bb --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/components/Tauri/WindowRouterLink.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/LoadingComponent.vue b/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/LoadingComponent.vue new file mode 100644 index 000000000..052fd93ff --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/LoadingComponent.vue @@ -0,0 +1,36 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/LoadingComponentDetail.vue b/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/LoadingComponentDetail.vue new file mode 100644 index 000000000..ba4672f56 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/LoadingComponentDetail.vue @@ -0,0 +1,3 @@ + diff --git a/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/LoadingModules.vue b/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/LoadingModules.vue new file mode 100644 index 000000000..9f1b36c35 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/LoadingModules.vue @@ -0,0 +1,76 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/index.vue b/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/index.vue new file mode 100644 index 000000000..073036603 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/components/Widgets/ResourceStatusIsland/index.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/components/Window/TitleBar.vue b/apps/stage-tamagotchi-electron/src/renderer/components/Window/TitleBar.vue new file mode 100644 index 000000000..014d34660 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/components/Window/TitleBar.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/composables/icon-animation.ts b/apps/stage-tamagotchi-electron/src/renderer/composables/icon-animation.ts new file mode 100644 index 000000000..3149a8e5b --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/composables/icon-animation.ts @@ -0,0 +1,29 @@ +import { useSettings } from '@proj-airi/stage-ui/stores/settings' +import { computed, onMounted, onUnmounted, ref } from 'vue' + +export function useIconAnimation(icon: string) { + const iconAnimationStarted = ref(false) + const showAnimationComponent = ref(false) + const animationIcon = ref(icon) + + const settingsStore = useSettings() + const showIconAnimation = computed(() => showAnimationComponent.value && !settingsStore.disableTransitions && settingsStore.usePageSpecificTransitions) + + onMounted(() => { + showAnimationComponent.value = true + requestAnimationFrame(() => { + iconAnimationStarted.value = true + }) + }) + + onUnmounted(() => { + iconAnimationStarted.value = false + showAnimationComponent.value = false + }) + + return { + iconAnimationStarted, + showIconAnimation, + animationIcon, + } +} diff --git a/apps/stage-tamagotchi-electron/src/renderer/composables/runtime.ts b/apps/stage-tamagotchi-electron/src/renderer/composables/runtime.ts new file mode 100644 index 000000000..bf3d1fa52 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/composables/runtime.ts @@ -0,0 +1,25 @@ +import { computedAsync } from '@vueuse/core' +import { computed, ref } from 'vue' + +export function useAppRuntime() { + const isInitialized = ref(false) + + const platform = computedAsync(async () => { + const res = 'electron' + if (!isInitialized.value) { + isInitialized.value = true + } + + return res + }, 'web') + + const isTauri = computed(() => { + return platform.value !== 'web' + }) + + return { + platform, + isInitialized, + isTauri, + } +} diff --git a/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-global-shortcuts.ts b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-global-shortcuts.ts new file mode 100644 index 000000000..a802211b2 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-global-shortcuts.ts @@ -0,0 +1,34 @@ +// import { useMagicKeys } from '@vueuse/core' +// import { until, whenever } from '@vueuse/shared' +// import { storeToRefs } from 'pinia' +// import { watch } from 'vue' + +// import { useShortcutsStore } from '../stores/shortcuts' +// import { useAppRuntime } from './runtime' +// import { useTauriRdevEventTarget } from './tauri-rdev' + +// export function useTauriGlobalShortcuts() { +// const { shortcuts } = storeToRefs(useShortcutsStore()) +// const { platform, isInitialized } = useAppRuntime() +// const eventTarget = useTauriRdevEventTarget() +// const keys = useMagicKeys({ target: eventTarget }) + +// watch(shortcuts, async () => { +// await until(isInitialized).toBeTruthy() +// if (platform.value === 'web') { +// return +// } + +// for (const handler of shortcuts.value) { +// if (!handler.shortcut) { +// return +// } + +// whenever(keys[handler.shortcut], async () => { +// handler.handle().catch((error) => { +// console.error('Error handling shortcut', error) +// }) +// }) +// } +// }, { immediate: true }) +// } diff --git a/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-positioner.ts b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-positioner.ts new file mode 100644 index 000000000..5181635b7 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-positioner.ts @@ -0,0 +1,42 @@ +// import type { Position } from '@tauri-apps/plugin-positioner' + +// import { computedAsync, until } from '@vueuse/core' + +// import { useAppRuntime } from './runtime' +// import { untilImported } from './tauri' + +// export function useTauriPositioner() { +// const { platform, isInitialized } = useAppRuntime() + +// const tauriPositionerApi = computedAsync(async () => { +// await until(isInitialized).toBeTruthy() + +// if (platform.value !== 'web') { +// return untilImported(() => import('@tauri-apps/plugin-positioner'), console.warn) +// } +// }) + +// async function ensureImported() { +// await until(isInitialized).toBeTruthy() + +// if (platform.value === 'web') { +// console.warn('Attempted to use Tauri positioner in web platform') +// return +// } + +// await until(tauriPositionerApi).toBeTruthy() +// const imported = await tauriPositionerApi.value +// if (!imported) { +// throw new Error('Tauri positioner API not available') +// } +// } + +// async function moveWindow(to: Position) { +// await ensureImported() +// return tauriPositionerApi.value?.moveWindow(to) +// } + +// return { +// moveWindow, +// } +// } diff --git a/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-rdev.ts b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-rdev.ts new file mode 100644 index 000000000..452b37b2c --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-rdev.ts @@ -0,0 +1,117 @@ +// import type { AiriTamagotchiEvents } from './tauri' + +// import { onMounted, onUnmounted } from 'vue' + +// import { KeyCode, mapKeyCode, mapKeyKey } from '../tauri/rdev' +// import { useTauriEvent } from './tauri' + +// export function useTauriRdevEventTarget(): EventTarget { +// const unListenFuncs: (() => void)[] = [] +// const eventTarget = new EventTarget() + +// const { listen } = useTauriEvent() + +// async function setup() { +// window.addEventListener('keyup', (event) => { +// event.preventDefault() +// event.stopPropagation() + +// const e = new KeyboardEvent('keyup', { +// key: event.key, +// code: event.code, +// metaKey: event.metaKey, +// ctrlKey: event.ctrlKey, +// altKey: event.altKey, +// shiftKey: event.shiftKey, +// }) +// eventTarget.dispatchEvent(e) +// }) + +// window.addEventListener('keydown', (event) => { +// event.preventDefault() +// event.stopPropagation() + +// const e = new KeyboardEvent('keydown', { +// key: event.key, +// code: event.code, +// metaKey: event.metaKey, +// ctrlKey: event.ctrlKey, +// altKey: event.altKey, +// shiftKey: event.shiftKey, +// }) +// eventTarget.dispatchEvent(e) +// }) + +// unListenFuncs.push(await listen('tauri-plugins:tauri-plugin-rdev:mousemove', (event) => { +// if (event.payload.event_type.MouseMove) { +// const { x, y } = event.payload.event_type.MouseMove +// const e = new MouseEvent('mousemove', { +// clientX: x, +// clientY: y, +// }) +// eventTarget.dispatchEvent(e) +// } +// })) + +// unListenFuncs.push(await listen('tauri-plugins:tauri-plugin-rdev:keyup', (event) => { +// if (typeof event.payload.event_type.KeyRelease === 'object' && 'Unknown' in event.payload.event_type.KeyRelease) { +// if (event.payload.event_type.KeyRelease.Unknown === 62) { +// event.payload.event_type = { KeyRelease: KeyCode.ControlLeft } +// } +// } +// if (typeof event.payload.event_type.KeyRelease !== 'string') { +// console.warn('unknown key release event:', event.payload.event_type.KeyRelease) +// return +// } + +// const e = new KeyboardEvent('keyup', { +// key: mapKeyKey[event.payload.event_type.KeyRelease], +// code: mapKeyCode[event.payload.event_type.KeyRelease], +// metaKey: mapKeyKey[event.payload.event_type.KeyRelease] === 'Meta', +// ctrlKey: mapKeyKey[event.payload.event_type.KeyRelease] === 'Control', +// altKey: mapKeyKey[event.payload.event_type.KeyRelease] === 'Alt', +// shiftKey: mapKeyKey[event.payload.event_type.KeyRelease] === 'Shift', +// }) + +// eventTarget.dispatchEvent(e) +// })) + +// unListenFuncs.push(await listen('tauri-plugins:tauri-plugin-rdev:keydown', (event) => { +// if (typeof event.payload.event_type.KeyPress === 'object' && 'Unknown' in event.payload.event_type.KeyPress) { +// if (event.payload.event_type.KeyPress.Unknown === 62) { +// event.payload.event_type = { KeyPress: KeyCode.ControlLeft } +// } +// } +// if (typeof event.payload.event_type.KeyPress !== 'string') { +// console.warn('unknown key release event:', event.payload.event_type.KeyPress) +// return +// } + +// const e = new KeyboardEvent('keydown', { +// key: mapKeyKey[event.payload.event_type.KeyPress], +// code: mapKeyCode[event.payload.event_type.KeyPress], +// metaKey: mapKeyKey[event.payload.event_type.KeyPress] === 'Meta', +// ctrlKey: mapKeyKey[event.payload.event_type.KeyPress] === 'Control', +// altKey: mapKeyKey[event.payload.event_type.KeyPress] === 'Alt', +// shiftKey: mapKeyKey[event.payload.event_type.KeyPress] === 'Shift', +// }) + +// eventTarget.dispatchEvent(e) +// })) +// } + +// function cleanup() { +// unListenFuncs.forEach(unListen => unListen()) +// unListenFuncs.length = 0 +// } + +// onMounted(() => setup()) +// onUnmounted(() => cleanup()) + +// if (import.meta.hot) { // For better DX +// import.meta.hot.on('vite:beforeUpdate', () => cleanup()) +// import.meta.hot.on('vite:afterUpdate', async () => await setup()) +// } + +// return eventTarget +// } diff --git a/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-window-pass-through-on-hover.ts b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-window-pass-through-on-hover.ts new file mode 100644 index 000000000..92031a6f8 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-window-pass-through-on-hover.ts @@ -0,0 +1,134 @@ +// import type { MaybeRefOrGetter } from 'vue' + +// import type { AiriTamagotchiEvents, Point, WindowFrame } from './tauri' + +// import { onMounted, onUnmounted, ref, toValue, watch } from 'vue' + +// import { startClickThrough, stopClickThrough } from '../utils/windows' +// import { useAppRuntime } from './runtime' +// import { useTauriCore, useTauriEvent } from './tauri' + +// export function useTauriPointAndWindowFrame() { +// const { listen } = useTauriEvent() +// const { invoke } = useTauriCore() + +// const unListenFuncs = ref<(() => void)[]>([]) + +// const mousePos = ref({ x: 0, y: 0 }) +// const windowFrame = ref({ +// origin: { x: 0, y: 0 }, +// size: { width: 0, height: 0 }, +// }) + +// function _onCursorPosition(event: { payload: Point }) { +// mousePos.value = event.payload +// } + +// function _onWindowFrame(event: { payload: WindowFrame }) { +// windowFrame.value = event.payload +// } + +// function addListeners() { +// listen('tauri-plugins:tauri-plugin-window-pass-through-on-hover:cursor-position', (event) => { +// _onCursorPosition(event) +// }).then((fn) => { +// unListenFuncs.value.push(fn) +// }) + +// listen('tauri-plugins:tauri-plugin-window-pass-through-on-hover:window-frame', (event) => { +// _onWindowFrame(event) +// }).then((fn) => { +// unListenFuncs.value.push(fn) +// }) +// } + +// onMounted(() => { +// addListeners() +// invoke('plugin:window-pass-through-on-hover|start_tracing_cursor') +// }) + +// onUnmounted(() => { +// unListenFuncs.value.forEach(fn => fn?.()) +// unListenFuncs.value.length = 0 + +// invoke('plugin:window-pass-through-on-hover|stop_tracing_cursor') +// }) + +// if (import.meta.hot) { // For better DX +// import.meta.hot.on('vite:beforeUpdate', () => { +// unListenFuncs.value.forEach(fn => fn?.()) +// unListenFuncs.value.length = 0 + +// invoke('plugin:window-pass-through-on-hover|stop_tracing_cursor') +// }) + +// import.meta.hot.on('vite:afterUpdate', () => { +// addListeners() +// invoke('plugin:window-pass-through-on-hover|start_tracing_cursor') +// }) +// } + +// return { +// mousePos, +// windowFrame, +// } +// } + +// export function useTauriWindowClickThrough(live2DLookAtDefault: MaybeRefOrGetter<{ x: number, y: number }>) { +// const { platform, isInitialized } = useAppRuntime() +// const { mousePos, windowFrame } = useTauriPointAndWindowFrame() + +// const live2dLookAtX = ref(toValue(live2DLookAtDefault).x) +// const live2dLookAtY = ref(toValue(live2DLookAtDefault).y) + +// const isCursorInside = ref(false) + +// function updateLive2DLookAt() { +// if (platform.value === 'macos') { +// live2dLookAtX.value = mousePos.value.x - windowFrame.value.origin.x +// live2dLookAtY.value = windowFrame.value.size.height - mousePos.value.y + windowFrame.value.origin.y + +// return +// } + +// live2dLookAtX.value = mousePos.value.x - windowFrame.value.origin.x +// live2dLookAtY.value = mousePos.value.y - windowFrame.value.origin.y +// } + +// watch(mousePos, () => { +// updateLive2DLookAt() +// }) + +// function updateIsCursorInside() { +// isCursorInside.value +// = mousePos.value.x >= windowFrame.value.origin.x +// && mousePos.value.x <= windowFrame.value.origin.x + windowFrame.value.size.width +// && mousePos.value.y >= windowFrame.value.origin.y +// && mousePos.value.y <= windowFrame.value.origin.y + windowFrame.value.size.height +// } + +// watch([mousePos, windowFrame], () => { +// updateIsCursorInside() +// }) + +// watch(isInitialized, async (initialized) => { +// if (initialized) { +// updateLive2DLookAt() +// updateIsCursorInside() +// } +// }) + +// onMounted(async () => { +// await startClickThrough() +// }) + +// onUnmounted(async () => { +// await stopClickThrough() +// }) + +// return { +// isCursorInside, +// live2dLookAtX, +// live2dLookAtY, +// } +// } diff --git a/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-window-state.ts b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-window-state.ts new file mode 100644 index 000000000..4bb1e1f0c --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri-window-state.ts @@ -0,0 +1,77 @@ +// import { computedAsync, until } from '@vueuse/core' + +// import { useAppRuntime } from './runtime' +// import { untilImported } from './tauri' + +// export enum StateFlags { +// SIZE = 1, +// POSITION = 2, +// MAXIMIZED = 4, +// VISIBLE = 8, +// DECORATIONS = 16, +// FULLSCREEN = 32, +// ALL = 63, +// } + +// export function useTauriWindowState() { +// const { platform, isInitialized } = useAppRuntime() + +// const tauriWindowStateApi = computedAsync(async () => { +// await until(isInitialized).toBeTruthy() + +// if (platform.value !== 'web') { +// return untilImported(() => import('@tauri-apps/plugin-window-state'), console.warn) +// } +// }) + +// async function ensureImported() { +// await until(isInitialized).toBeTruthy() + +// if (platform.value === 'web') { +// console.warn('Attempted to save window state in web platform') +// return +// } + +// await until(tauriWindowStateApi).toBeTruthy() +// const imported = await tauriWindowStateApi.value +// if (!imported) { +// throw new Error('Tauri window state API not available') +// } +// } + +// async function saveWindowState(stateFlag?: StateFlags) { +// await ensureImported() +// if (stateFlag != null) { +// return tauriWindowStateApi.value?.saveWindowState(stateFlag) +// } +// else { +// return tauriWindowStateApi.value?.saveWindowState(tauriWindowStateApi.value.StateFlags.ALL) +// } +// } + +// async function restoreState(stateFlag?: StateFlags, windowLabel = 'main') { +// await ensureImported() +// if (stateFlag != null) { +// return tauriWindowStateApi.value?.restoreState(windowLabel, stateFlag) +// } +// else { +// return tauriWindowStateApi.value?.restoreState(windowLabel, tauriWindowStateApi.value.StateFlags.ALL) +// } +// } + +// async function restoreStateCurrent(stateFlag?: StateFlags) { +// await ensureImported() +// if (stateFlag != null) { +// return tauriWindowStateApi.value?.restoreStateCurrent(stateFlag) +// } +// else { +// return tauriWindowStateApi.value?.restoreStateCurrent(tauriWindowStateApi.value.StateFlags.ALL) +// } +// } + +// return { +// saveWindowState, +// restoreState, +// restoreStateCurrent, +// } +// } diff --git a/apps/stage-tamagotchi-electron/src/renderer/composables/tauri.ts b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri.ts new file mode 100644 index 000000000..f08fdaf34 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/composables/tauri.ts @@ -0,0 +1,340 @@ +// import type { InvokeArgs, InvokeOptions } from '@tauri-apps/api/core' +// import type { EventCallback, EventName, UnlistenFn } from '@tauri-apps/api/event' +// import type { Monitor } from '@tauri-apps/api/window' + +// import type { InvokeMethods, InvokeMethodShape } from '../tauri/invoke' +// import type { KeyCode } from '../tauri/rdev' + +// import { withRetry } from '@moeru/std' +// import { computedAsync, until } from '@vueuse/core' + +// import { useAppRuntime } from './runtime' + +// export async function untilNoError(fn: () => Promise, onError?: (err?: unknown | null) => void): Promise { +// const fnRetry = withRetry(fn, { retryDelay: 5000, retry: 5, onError }) +// return await fnRetry() +// } + +// export async function untilImported(fn: () => Promise, onError?: (err?: unknown | null) => void): Promise { +// return await untilNoError(fn, onError) +// } + +// export interface Point { +// x: number +// y: number +// } + +// export interface Size { +// width: number +// height: number +// } + +// export interface WindowFrame { +// origin: Point +// size: Size +// } + +// interface Events { +// 'tauri://resize': unknown +// 'tauri://move': { x: number, y: number } +// 'tauri://close-requested': unknown +// 'tauri://destroyed': unknown +// 'tauri://focus': unknown +// 'tauri://blur': unknown +// 'tauri://scale-change': unknown +// 'tauri://theme-changed': unknown +// 'tauri://window-created': unknown +// 'tauri://webview-created': unknown +// 'tauri://drag-enter': unknown +// 'tauri://drag-over': unknown +// 'tauri://drag-drop': unknown +// 'tauri://drag-leave': unknown +// } + +// export interface AiriTamagotchiEvents extends Events { +// // from main +// 'tauri-main:main:window-mode:fade-on-hover': true +// 'tauri-main:main:window-mode:move': true +// 'tauri-main:main:window-mode:resize': true + +// // from tauri-plugin-window-pass-through-on-hover +// 'tauri-plugins:tauri-plugin-window-pass-through-on-hover:cursor-position': Point +// 'tauri-plugins:tauri-plugin-window-pass-through-on-hover:window-frame': WindowFrame +// 'tauri-plugins:tauri-plugin-window-pass-through-on-hover:pass-through-enabled': boolean + +// // from tauri-plugin-ipc-audio-transcription-ort +// 'tauri-plugins:tauri-plugin-ipc-audio-vad-ort:load-model-silero-vad-progress': [boolean, string, number, number, number] +// // from tauri-plugin-ipc-audio-vad-ort +// 'tauri-plugins:tauri-plugin-ipc-audio-transcription-ort:load-model-whisper-progress': [boolean, string, number, number, number] + +// // from tauri-plugin-rdev +// 'tauri-plugins:tauri-plugin-rdev:keydown': { time: { secs_since_epoch: number, nanos_since_epoch: number }, name: string, event_type: { KeyPress: KeyCode | { Unknown: number } } } // similar to 'keydown' events from DOM elements +// 'tauri-plugins:tauri-plugin-rdev:keyup': { time: { secs_since_epoch: number, nanos_since_epoch: number }, name: string, event_type: { KeyRelease: KeyCode | { Unknown: number } } } // similar to 'keyup' events from DOM elements +// 'tauri-plugins:tauri-plugin-rdev:mousedown': { time: { secs_since_epoch: number, nanos_since_epoch: number }, name: string, event_type: { ButtonPress: string } } // similar to 'mousedown' events from DOM elements +// 'tauri-plugins:tauri-plugin-rdev:mouseup': { time: { secs_since_epoch: number, nanos_since_epoch: number }, name: string, event_type: { ButtonRelease: string } } // similar to 'mouseup' events from DOM elements +// 'tauri-plugins:tauri-plugin-rdev:mousemove': { time: { secs_since_epoch: number, nanos_since_epoch: number }, name: string, event_type: { MouseMove: { x: number, y: number } } } // similar to 'mousemove' events from DOM elements + +// // MCP +// 'mcp_plugin_destroyed': undefined +// } + +// export interface DisplayInfo { +// monitors: Monitor[] +// primaryMonitor: Monitor +// } + +// export enum PlacementStrategy { +// Center = 'Center', +// TopLeft = 'TopLeft', +// TopRight = 'TopRight', +// BottomLeft = 'BottomLeft', +// BottomRight = 'BottomRight', +// NearCursor = 'NearCursor', +// RestorePrevious = 'RestorePrevious', +// AvoidOverlap = 'AvoidOverlap', +// } + +// export interface WindowPlacementRequest { +// window_size: Size +// preferred_position?: Point +// placement_strategy: PlacementStrategy +// } + +// export interface WindowPlacementResult { +// position: Point +// target_monitor_id: number +// is_constrained: boolean +// } + +// export function useTauriEvent() { +// const { platform, isInitialized } = useAppRuntime() + +// const tauriEventApi = computedAsync(async () => { +// await until(isInitialized).toBeTruthy() + +// if (platform.value !== 'web') { +// return untilImported(() => import('@tauri-apps/api/event'), console.warn) +// } +// }) + +// async function _listen(event: E, callback: EventCallback) { +// await until(isInitialized).toBeTruthy() + +// if (platform.value === 'web') { +// return () => {} +// } + +// await until(tauriEventApi).toBeTruthy() +// const imported = await tauriEventApi.value +// if (!imported) { +// throw new Error('Tauri event API not available') +// } + +// return await imported.listen(event as EventName, callback) +// } + +// async function listen(event: E, callback: EventCallback) { +// let cleanupListener: UnlistenFn | undefined + +// _listen(event, callback).then((listener) => { +// cleanupListener = listener +// }) + +// return () => { +// cleanupListener?.() +// } +// } + +// return { +// listen, +// } +// } + +// export function useTauriCore = InvokeMethods>() { +// const { platform, isInitialized } = useAppRuntime() + +// const tauriCoreApi = computedAsync(async () => { +// await until(isInitialized).toBeTruthy() + +// if (platform.value !== 'web') { +// return untilImported(() => import('@tauri-apps/api/core'), console.warn) +// } +// }) + +// const tauriCoreApiInvoke = computedAsync(async () => { +// await until(isInitialized).toBeTruthy() + +// if (platform.value !== 'web') { +// return untilImported(() => import('../tauri/invoke'), console.warn) +// } +// }) + +// async function invoke( +// command: C, +// args?: IM[C]['args'], +// options?: IM[C]['options'], +// ): Promise { +// await until(isInitialized).toBeTruthy() + +// if (platform.value === 'web') { +// console.warn(`Attempted to invoke Tauri command "${String(command)}" in web platform`) +// return +// } + +// await until(tauriCoreApiInvoke).toBeTruthy() +// const imported = await tauriCoreApiInvoke.value +// if (!imported) { +// throw new Error('Tauri core API not available') +// } + +// return await imported.invoke(command, args as InvokeArgs | undefined, options as InvokeOptions | undefined) +// } + +// return { +// invoke, +// core: tauriCoreApi, +// } +// } + +// export function useTauriDpi() { +// const { platform, isInitialized } = useAppRuntime() + +// const tauriDpiApi = computedAsync(async () => { +// await until(isInitialized).toBeTruthy() + +// if (platform.value !== 'web') { +// return untilImported(() => import('@tauri-apps/api/window'), console.warn) +// } +// }) + +// async function createLogicalPosition(x: number, y: number) { +// const imported = await tauriDpiApi.value +// if (!imported) { +// throw new Error('Tauri DPI API not available') +// } + +// return new imported.LogicalPosition(x, y) +// } + +// return { +// createLogicalPosition, +// } +// } + +// export function useTauriWindow() { +// const { platform, isInitialized } = useAppRuntime() +// const { createLogicalPosition } = useTauriDpi() + +// const tauriWindowApi = computedAsync(async () => { +// await until(isInitialized).toBeTruthy() + +// if (platform.value !== 'web') { +// return untilImported(() => import('@tauri-apps/api/window'), console.warn) +// } +// }) + +// async function _ensureImported() { +// await until(isInitialized).toBeTruthy() +// if (platform.value === 'web') { +// throw new Error('Tauri Window API is not available in web platform') +// } + +// await until(tauriWindowApi).toBeTruthy() +// const imported = await tauriWindowApi.value +// if (!imported) { +// throw new Error('Tauri Window API not available') +// } + +// return imported +// } + +// async function getCurrentMonitor() { +// try { +// return await _ensureImported().then(imported => imported.currentMonitor()) +// } +// catch (error) { +// console.error('Failed to get current monitor:', error) +// return undefined +// } +// } + +// async function getAvailableMonitors() { +// try { +// return await _ensureImported().then(imported => imported.availableMonitors()) +// } +// catch (error) { +// console.error('Failed to get available monitors:', error) +// return [] +// } +// } + +// async function getPrimaryMonitor() { +// try { +// return await _ensureImported().then(imported => imported.primaryMonitor()) +// } +// catch (error) { +// console.error('Failed to get primary monitor:', error) +// return undefined +// } +// } + +// async function setPosition(x: number, y: number) { +// try { +// const imported = await _ensureImported() +// const window = imported.getCurrentWindow() +// return await window.setPosition(await createLogicalPosition(x, y)) +// } +// catch (error) { +// console.error('Failed to set window position:', error) +// } +// } + +// async function getPosition() { +// try { +// const imported = await _ensureImported() +// const window = imported.getCurrentWindow() +// return await window.innerPosition() +// } +// catch (error) { +// console.error('Failed to get window position:', error) +// return undefined +// } +// } + +// async function closeWindow(label?: string) { +// try { +// const imported = await _ensureImported() +// if (!label) { +// const window = imported.getCurrentWindow() +// return await window.close() +// } +// else { +// const windows = await imported.getAllWindows() +// const targetWindow = windows.find(win => win.label === label) +// if (targetWindow) { +// return await targetWindow.close() +// } +// else { +// console.warn(`No window found with label: ${label}`) +// } +// } +// } +// catch (error) { +// console.error('Failed to close window:', error) +// } +// } + +// return { +// getAvailableMonitors, +// getCurrentMonitor, +// getPrimaryMonitor, +// setPosition, +// getPosition, +// closeWindow, +// } +// } + +// export function createTauriEventTarget(): EventTarget { +// const eventTarget = new EventTarget() +// return eventTarget +// } diff --git a/apps/stage-tamagotchi-electron/src/renderer/composables/theme-color.ts b/apps/stage-tamagotchi-electron/src/renderer/composables/theme-color.ts new file mode 100644 index 000000000..145ecc84e --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/composables/theme-color.ts @@ -0,0 +1,46 @@ +import Color from 'colorjs.io' + +import { withRetry } from '@moeru/std' +import { useDark } from '@vueuse/core' + +export function themeColorFromPropertyOf(colorFromClass: string, property: string): () => Promise { + return async () => { + const fetchUntilWidgetMounted = withRetry(() => { + const widgets = document.querySelector(colorFromClass) as HTMLDivElement | undefined + if (!widgets) + throw new Error('Widgets element not found') + + return widgets + }, { retry: 10, retryDelay: 1000 }) + + const widgets = await fetchUntilWidgetMounted() + return window.getComputedStyle(widgets).getPropertyValue(property) + } +} + +export function themeColorFromValue(value: string | { light: string, dark: string }): () => Promise { + return async () => { + if (typeof value === 'string') { + return value + } + else { + const dark = useDark() + return dark.value ? value.dark : value.light + } + } +} + +export function useThemeColor(colorFrom: () => string | Promise) { + async function updateThemeColor() { + if (!('document' in globalThis && globalThis.document != null)) + return + if (!('window' in globalThis && globalThis.window != null)) + return + + document.querySelector('meta[name="theme-color"]')?.setAttribute('content', new Color(await colorFrom()).to('srgb').toString({ format: 'hex' })) + } + + return { + updateThemeColor, + } +} diff --git a/apps/stage-tamagotchi-electron/src/renderer/composables/use-rdev-mouse.ts b/apps/stage-tamagotchi-electron/src/renderer/composables/use-rdev-mouse.ts new file mode 100644 index 000000000..25abb9777 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/composables/use-rdev-mouse.ts @@ -0,0 +1,30 @@ +// import type { AiriTamagotchiEvents } from './tauri' + +// import { createSharedComposable } from '@vueuse/core' +// import { ref } from 'vue' + +// import { useTauriEvent } from './tauri' + +// export const useRdevMouse = createSharedComposable(() => { +// const mouseX = ref(0) +// const mouseY = ref(0) + +// const { listen } = useTauriEvent() + +// async function setup() { +// await listen('tauri-plugins:tauri-plugin-rdev:mousemove', (event) => { +// if (event.payload.event_type.MouseMove) { +// const { x, y } = event.payload.event_type.MouseMove +// mouseX.value = x +// mouseY.value = y +// } +// }) +// } + +// setup() + +// return { +// mouseX, +// mouseY, +// } +// }) diff --git a/apps/stage-tamagotchi-electron/src/renderer/electron.d.ts b/apps/stage-tamagotchi-electron/src/renderer/electron.d.ts new file mode 100644 index 000000000..40155b250 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/electron.d.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + electron: import('@electron-toolkit/preload').ElectronAPI + } +} + +export {} diff --git a/apps/stage-tamagotchi-electron/src/renderer/index.html b/apps/stage-tamagotchi-electron/src/renderer/index.html new file mode 100644 index 000000000..65c64030d --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/index.html @@ -0,0 +1,26 @@ + + + + + AIRI + + + + + + + +
+ + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/layouts/default.vue b/apps/stage-tamagotchi-electron/src/renderer/layouts/default.vue new file mode 100644 index 000000000..77beeecd0 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/layouts/default.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/layouts/plain.vue b/apps/stage-tamagotchi-electron/src/renderer/layouts/plain.vue new file mode 100644 index 000000000..a78fb6fd3 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/layouts/plain.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/layouts/settings.vue b/apps/stage-tamagotchi-electron/src/renderer/layouts/settings.vue new file mode 100644 index 000000000..8982c61cd --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/layouts/settings.vue @@ -0,0 +1,164 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/layouts/stage.vue b/apps/stage-tamagotchi-electron/src/renderer/layouts/stage.vue new file mode 100644 index 000000000..a78fb6fd3 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/layouts/stage.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/main.ts b/apps/stage-tamagotchi-electron/src/renderer/main.ts new file mode 100644 index 000000000..88c8fe621 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/main.ts @@ -0,0 +1,49 @@ +import type { Plugin } from 'vue' + +import Tres from '@tresjs/core' + +import { autoAnimatePlugin } from '@formkit/auto-animate/vue' +import { MotionPlugin } from '@vueuse/motion' +import { createPinia } from 'pinia' +import { setupLayouts } from 'virtual:generated-layouts' +import { createApp } from 'vue' +import { createRouter, createWebHashHistory } from 'vue-router' +import { routes } from 'vue-router/auto-routes' + +import App from './App.vue' + +import { i18n } from './modules/i18n' + +import '@unocss/reset/tailwind.css' +import 'uno.css' +import './styles/main.css' +// Fonts +import '@proj-airi/font-cjkfonts-allseto/index.css' +import '@proj-airi/font-xiaolai/index.css' +import '@fontsource-variable/dm-sans' +import '@fontsource-variable/jura' +import '@fontsource-variable/quicksand' +import '@fontsource-variable/urbanist' +import '@fontsource/dm-mono' +import '@fontsource/dm-serif-display' +import '@fontsource/gugi' +import '@fontsource/kiwi-maru' +import '@fontsource/m-plus-rounded-1c' +import '@fontsource/sniglet' + +const pinia = createPinia() + +const router = createRouter({ + history: createWebHashHistory(), + routes: setupLayouts(routes), +}) + +createApp(App) + .use(MotionPlugin) + // TODO: Fix autoAnimatePlugin type error + .use(autoAnimatePlugin as unknown as Plugin) + .use(router) + .use(pinia) + .use(i18n) + .use(Tres) + .mount('#app') diff --git a/apps/stage-tamagotchi-electron/src/renderer/modules/i18n.ts b/apps/stage-tamagotchi-electron/src/renderer/modules/i18n.ts new file mode 100644 index 000000000..1e9f4e920 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/modules/i18n.ts @@ -0,0 +1,42 @@ +import messages from '@proj-airi/i18n/locales' + +import { createI18n } from 'vue-i18n' + +const languageRemap: Record = { + 'zh-CN': 'zh-Hans', + 'zh-TW': 'zh-Hans', // TODO: remove this when zh-Hant is supported + 'zh-HK': 'zh-Hans', // TODO: remove this when zh-Hant is supported + 'zh-Hant': 'zh-Hans', // TODO: remove this when zh-Hant is supported + 'en-US': 'en', + 'en-GB': 'en', + 'en-AU': 'en', + 'en': 'en', + 'es-ES': 'es', + 'es-MX': 'es', + 'es-AR': 'es', + 'es': 'es', + 'vi': 'vi', + 'vi-VN': 'vi', + 'ru': 'ru', + 'ru-RU': 'ru', +} + +function getLocale() { + let language = localStorage.getItem('settings/language') + const languages = Object.keys(messages!) + + if (languageRemap[language || 'en'] != null) { + language = languageRemap[language || 'en'] + } + if (language && languages.includes(language)) + return language + + return 'en' +} + +export const i18n = createI18n({ + legacy: false, + locale: getLocale(), + fallbackLocale: 'en', + messages, +}) diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/chat.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/chat.vue new file mode 100644 index 000000000..fdc032ca7 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/chat.vue @@ -0,0 +1,22 @@ + + + + + +meta: + layout: stage + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/devtools/use-magic-keys.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/devtools/use-magic-keys.vue new file mode 100644 index 000000000..7f0015da4 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/devtools/use-magic-keys.vue @@ -0,0 +1,139 @@ + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/index.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/index.vue new file mode 100644 index 000000000..014940842 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/index.vue @@ -0,0 +1,386 @@ + + + + + + + +meta: + layout: stage + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/onboarding.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/onboarding.vue new file mode 100644 index 000000000..7d9442972 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/onboarding.vue @@ -0,0 +1,34 @@ + + + + + +meta: + layout: plain + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardCreate.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardCreate.vue new file mode 100644 index 000000000..f2aaec808 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardCreate.vue @@ -0,0 +1,22 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardCreationDialog.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardCreationDialog.vue new file mode 100644 index 000000000..24cb70581 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardCreationDialog.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardDetailDialog.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardDetailDialog.vue new file mode 100644 index 000000000..2834002bc --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardDetailDialog.vue @@ -0,0 +1,352 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardListItem.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardListItem.vue new file mode 100644 index 000000000..fac051292 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/CardListItem.vue @@ -0,0 +1,101 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/DeleteCardDialog.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/DeleteCardDialog.vue new file mode 100644 index 000000000..ea9f81166 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/components/DeleteCardDialog.vue @@ -0,0 +1,73 @@ + + + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/index.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/index.vue new file mode 100644 index 000000000..28048a288 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/airi-card/index.vue @@ -0,0 +1,313 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/index.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/index.vue new file mode 100644 index 000000000..93c0092eb --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/index.vue @@ -0,0 +1,161 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/memory/index.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/memory/index.vue new file mode 100644 index 000000000..9151042be --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/memory/index.vue @@ -0,0 +1,45 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/models/index.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/models/index.vue new file mode 100644 index 000000000..63ee2428f --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/models/index.vue @@ -0,0 +1,89 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + pageSpecificAvailable: true + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/consciousness.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/consciousness.vue new file mode 100644 index 000000000..7acfd4337 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/consciousness.vue @@ -0,0 +1,216 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/hearing.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/hearing.vue new file mode 100644 index 000000000..f97883d28 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/hearing.vue @@ -0,0 +1,650 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/index.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/index.vue new file mode 100644 index 000000000..f0037b0f2 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/index.vue @@ -0,0 +1,175 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + pageSpecificAvailable: true + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/mcp.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/mcp.vue new file mode 100644 index 000000000..63db14d29 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/mcp.vue @@ -0,0 +1,152 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/memory-long-term.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/memory-long-term.vue new file mode 100644 index 000000000..e40ede6e7 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/memory-long-term.vue @@ -0,0 +1,16 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/memory-short-term.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/memory-short-term.vue new file mode 100644 index 000000000..e40ede6e7 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/memory-short-term.vue @@ -0,0 +1,16 @@ + + + + + +meta: + layout: settings + stageTransition: + name: slide + diff --git a/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/speech.vue b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/speech.vue new file mode 100644 index 000000000..9878f8ac2 --- /dev/null +++ b/apps/stage-tamagotchi-electron/src/renderer/pages/settings/modules/speech.vue @@ -0,0 +1,502 @@ + + +