chore(tamagotchi): move settings a single window (#14)

* move settings to a new window
* use custom menu
* fix routing
This commit is contained in:
LemonNeko
2025-01-15 21:58:22 +08:00
committed by GitHub
parent 790189f1eb
commit ca02572fbe
10 changed files with 244 additions and 98 deletions
@@ -1,9 +1,6 @@
import { join, resolve } from 'node:path'
import { Download } from '@proj-airi/unplugin-download'
import { DownloadLive2DSDK } from '@proj-airi/unplugin-live2d-sdk'
import vue from '@vitejs/plugin-vue'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import unocss from 'unocss/vite'
import rendererConfig from './renderer.vite.config'
export default defineConfig({
main: {
@@ -12,25 +9,5 @@ export default defineConfig({
preload: {
plugins: [externalizeDepsPlugin()],
},
renderer: {
optimizeDeps: {
exclude: [
'@proj-airi/stage-ui/*',
],
},
resolve: {
alias: {
'@renderer': resolve(join('src', 'renderer', 'src')),
'@proj-airi/stage-ui': resolve(join(import.meta.dirname, '..', 'stage-ui', 'dist')),
'@proj-airi/stage-ui/stores': resolve(join(import.meta.dirname, '..', 'stage-ui', 'dist', 'stores')),
},
},
plugins: [
vue(),
unocss(),
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'),
],
},
renderer: rendererConfig,
})
@@ -0,0 +1,33 @@
import { join, resolve } from 'node:path'
import { Download } from '@proj-airi/unplugin-download'
import { DownloadLive2DSDK } from '@proj-airi/unplugin-live2d-sdk'
import Vue from '@vitejs/plugin-vue'
import UnoCss from 'unocss/vite'
import VueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite'
export default defineConfig({
optimizeDeps: {
exclude: [
'@proj-airi/stage-ui/*',
],
},
resolve: {
alias: {
'@renderer': resolve(join('src', 'renderer', 'src')),
'@proj-airi/stage-ui': resolve(join(import.meta.dirname, '..', 'stage-ui', 'dist')),
'@proj-airi/stage-ui/stores': resolve(join(import.meta.dirname, '..', 'stage-ui', 'dist', 'stores')),
},
},
plugins: [
Vue(),
UnoCss(),
VueRouter({
dts: resolve(import.meta.dirname, 'src/typed-router.d.ts'),
routesFolder: 'src/renderer/src/pages',
}),
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'),
],
})
+84 -2
View File
@@ -1,7 +1,7 @@
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 { app, BrowserWindow, dialog, ipcMain, Menu, shell } from 'electron'
import icon from '../../build/icon.png?asset'
function createWindow(): void {
@@ -50,10 +50,78 @@ function createWindow(): void {
})
}
let settingsWindow: BrowserWindow | null = null
function createSettingsWindow(): void {
if (settingsWindow) {
settingsWindow.show()
return
}
settingsWindow = new BrowserWindow({
width: 300,
height: 400,
show: false,
webPreferences: {
preload: join(import.meta.dirname, '..', 'preload', 'index.js'),
sandbox: false,
},
})
settingsWindow.on('ready-to-show', () => {
settingsWindow?.show()
})
settingsWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
settingsWindow.on('close', () => {
settingsWindow = null
})
settingsWindow.show()
if (is.dev && env.ELECTRON_RENDERER_URL) {
settingsWindow.loadURL(join(env.ELECTRON_RENDERER_URL, '#/settings'))
}
else {
settingsWindow.loadFile(join(import.meta.dirname, '..', '..', 'out', 'renderer', 'index.html'), {
hash: '/settings',
})
}
}
// 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(() => {
// Menu
const menu = Menu.buildFromTemplate([
{
label: 'airi',
role: 'appMenu',
submenu: [
{
role: 'about',
},
{
role: 'toggleDevTools',
},
{
label: 'Settings',
click: () => createSettingsWindow(),
},
{
label: 'Quit',
click: () => app.quit(),
},
],
},
])
Menu.setApplicationMenu(menu)
// Set app user model id for windows
electronApp.setAppUserModelId('com.github.moeru-ai.airi-tamagotchi')
@@ -65,7 +133,21 @@ app.whenReady().then(() => {
})
// IPC test
ipcMain.on('quit', () => app.quit())
// TODO: i18n
ipcMain.on('quit', () => {
dialog.showMessageBox({
type: 'info',
title: 'Quit',
message: 'Are you sure you want to quit?',
buttons: ['Quit', 'Cancel'],
}).then((result) => {
if (result.response === 0) {
app.quit()
}
})
})
ipcMain.on('open-settings', () => createSettingsWindow())
createWindow()
@@ -1,49 +1,7 @@
<script setup lang="ts">
import { WidgetStage } from '@proj-airi/stage-ui/components'
import { ref } from 'vue'
import InteractiveArea from './components/InteractiveArea.vue'
const dragDelay = ref(0)
const isDragging = ref(false)
function handleMouseDown() {
dragDelay.value = window.setTimeout(() => {
isDragging.value = true
}, 500)
}
function handleMouseUp() {
clearTimeout(dragDelay.value)
isDragging.value = false
}
function handleMouseLeave() {
isDragging.value = false
}
function handleMouseMove(event: MouseEvent) {
if (isDragging.value) {
window.electron.ipcRenderer.send('move-window', event.movementX, event.movementY)
}
}
import { RouterView } from 'vue-router'
</script>
<template>
<div relative max-h="[100vh]" max-w="[100vw]" p="2" flex="~ col" z-2 h-full overflow-hidden @mousedown="handleMouseDown" @mouseup="handleMouseUp" @mousemove="handleMouseMove" @mouseleave="handleMouseLeave">
<div relative h-full w-full items-end gap-2 class="view">
<WidgetStage h-full w-full flex-1 mb="<md:18" />
<InteractiveArea class="interaction-area block" pointer-events-none absolute bottom-0 w-full opacity-0 transition="opacity duration-250" />
</div>
</div>
<RouterView />
</template>
<style scoped>
.view {
&:hover {
.interaction-area {
opacity: 1;
pointer-events: auto;
}
}
}
</style>
@@ -7,11 +7,9 @@ import { useChatStore, useSettings } from '@proj-airi/stage-ui/stores'
// import { useDevicesList } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { DrawerContent, DrawerPortal, DrawerRoot, DrawerTrigger } from 'vaul-vue'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import TamagotchiChatHistory from './ChatHistory.vue'
import TamagotchiSettings from './Settings.vue'
const messageInput = ref('')
const listening = ref(false)
@@ -72,6 +70,10 @@ function handleTranscription(_buffer: Float32Array<ArrayBufferLike>) {
// selectedAudioDevice.value = found
// }
function openSettings() {
window.electron.ipcRenderer.send('open-settings')
}
watch(isAudioInputOn, async (value) => {
if (value === 'false') {
destroy()
@@ -103,26 +105,16 @@ onMounted(() => {
@submit="handleSend"
/>
</div>
<DrawerRoot should-scale-background>
<DrawerTrigger
class="px-4 py-2.5"
border="solid 2 pink-100 "
text="lg pink-400 hover:pink-600 placeholder:pink-400 placeholder:hover:pink-600"
bg="pink-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]" rounded-r-xl
>
<div i-solar:settings-bold-duotone />
</DrawerTrigger>
<DrawerPortal>
<DrawerContent
max-h="[90%]"
fixed bottom-0 left-0 right-0 z-50 mt-24 h-full flex flex-col rounded-t-lg bg="[#fffbff] dark:[#1f1a1d]"
>
<div class="flex flex-1 flex-col rounded-t-lg p-5" bg="[#fffbff] dark:[#1f1a1d]" gap-2>
<TamagotchiSettings />
</div>
</DrawerContent>
</DrawerPortal>
</DrawerRoot>
<div
class="px-4 py-2.5"
border="solid 2 pink-100 "
text="lg pink-400 hover:pink-600 placeholder:pink-400 placeholder:hover:pink-600"
bg="pink-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]"
flex items-center justify-center rounded-r-xl
@click="openSettings"
>
<div i-solar:settings-bold-duotone />
</div>
</div>
</div>
</template>
@@ -4,8 +4,11 @@ import { MotionPlugin } from '@vueuse/motion'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import App from './App.vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import { routes } from 'vue-router/auto-routes'
import App from './App.vue'
import '@unocss/reset/tailwind.css'
import 'uno.css'
import './main.css'
@@ -22,9 +25,15 @@ const i18n = createI18n({
},
})
const router = createRouter({
history: createWebHashHistory(),
routes,
})
createApp(App)
.use(MotionPlugin)
.use(autoAnimatePlugin)
.use(router)
.use(pinia)
.use(i18n)
.use(Tres)
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { WidgetStage } from '@proj-airi/stage-ui/components'
import { ref } from 'vue'
import InteractiveArea from '../components/InteractiveArea.vue'
const dragDelay = ref(0)
const isDragging = ref(false)
function handleMouseDown() {
dragDelay.value = window.setTimeout(() => {
isDragging.value = true
}, 500)
}
function handleMouseUp() {
clearTimeout(dragDelay.value)
isDragging.value = false
}
function handleMouseLeave() {
isDragging.value = false
}
function handleMouseMove(event: MouseEvent) {
if (isDragging.value) {
window.electron.ipcRenderer.send('move-window', event.movementX, event.movementY)
}
}
</script>
<template>
<div relative max-h="[100vh]" max-w="[100vw]" p="2" flex="~ col" z-2 h-full overflow-hidden @mousedown="handleMouseDown" @mouseup="handleMouseUp" @mousemove="handleMouseMove" @mouseleave="handleMouseLeave">
<div relative h-full w-full items-end gap-2 class="view">
<WidgetStage h-full w-full flex-1 mb="<md:18" />
<InteractiveArea class="interaction-area block" pointer-events-none absolute bottom-0 w-full opacity-0 transition="opacity duration-250" />
</div>
</div>
</template>
<style scoped>
.view {
&:hover {
.interaction-area {
opacity: 1;
pointer-events: auto;
}
}
}
</style>
@@ -65,10 +65,14 @@ onMounted(async () => {
supportedModels.value = await models(openAiApiBaseURL.value, openAiApiKey.value)
})
function handleQuit() {
window.electron.ipcRenderer.send('quit')
}
</script>
<template>
<div>
<div m-4>
<h2 text="slate-800/80" font-bold>
Settings
</h2>
@@ -197,5 +201,23 @@ onMounted(async () => {
</select>
</div>
</div>
<h2 text="slate-800/80" font-bold>
Other
</h2>
<div pb-2>
<div
grid="~ cols-[140px_1fr]" my-2 items-center gap-1.5 rounded-lg
bg="[#fff6fc]" p-2 text="pink-400" @click="handleQuit"
>
<div text="xs pink-500">
<span>
Quit
</span>
</div>
<div text="sm pink-500" text-right>
<div i-solar:exit-bold-duotone ml-auto />
</div>
</div>
</div>
</div>
</template>
+24
View File
@@ -0,0 +1,24 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
declare module 'vue-router/auto-routes' {
import type {
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'vue-router'
/**
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/settings': RouteRecordInfo<'/settings', '/settings', Record<never, never>, Record<never, never>>,
}
}
+2 -2
View File
@@ -5,9 +5,9 @@
"types": ["electron-vite/node"]
},
"include": [
"renderer.vite.config.*",
"electron.vite.config.*",
"src/main/**/*",
"src/preload/**/*",
"../../vite-plugins/**/*"
"src/preload/**/*"
]
}