feat(stage-tamagotchi): system tray (#32)
* fix(tamagotchi): cannot use default select copy and paste * style(tamagotchi): scrollbar * fix(tamagotchi): set motion * feat(tamagotchi): system tray * fix: linter issue * fix: typecheck * fix: electron declaration
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -107,6 +107,7 @@
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-vite": "^2.3.0",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"unocss-preset-scrollbar": "^3.2.0",
|
||||
"unplugin-auto-import": "^19.1.0",
|
||||
"unplugin-vue-components": "^28.4.0",
|
||||
"unplugin-vue-macros": "^2.14.2",
|
||||
|
||||
@@ -1,24 +1,54 @@
|
||||
import { join } from 'node:path'
|
||||
import { env, platform } from 'node:process'
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import { app, BrowserWindow, dialog, ipcMain, Menu, screen, shell } from 'electron'
|
||||
import { inertia } from 'popmotion'
|
||||
import { nativeImage, shell } from 'electron/common'
|
||||
import { app, BrowserWindow, dialog, ipcMain, Tray } from 'electron/main'
|
||||
|
||||
import trayIconMacos from '../../build/icon-tray-macos.png?asset'
|
||||
import icon from '../../build/icon.png?asset'
|
||||
import { createI18n } from './locales'
|
||||
import { createApplicationMenu, createTrayMenu } from './menu'
|
||||
|
||||
// FIXME: electron i18n
|
||||
|
||||
let globalMouseTracker: ReturnType<typeof setInterval> | null = null
|
||||
let mainWindow: BrowserWindow
|
||||
let currentAnimationX: { stop: () => void } | null = null
|
||||
let currentAnimationY: { stop: () => void } | null = null
|
||||
let isDragging = false
|
||||
let lastMousePosition = { x: 0, y: 0 }
|
||||
let lastMouseTime = Date.now()
|
||||
let currentVelocity = { x: 0, y: 0 }
|
||||
let dragOffset = { x: 0, y: 0 }
|
||||
let tray: Tray
|
||||
|
||||
function createWindow(): void {
|
||||
const i18n = createI18n()
|
||||
|
||||
function showQuitDialog() {
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: i18n.t('menu.quit'),
|
||||
message: i18n.t('quitDialog.message'),
|
||||
buttons: [i18n.t('quitDialog.buttons.quit'), i18n.t('quitDialog.buttons.cancel')],
|
||||
}).then((result) => {
|
||||
if (result.response === 0) {
|
||||
mainWindow.webContents.send('before-quit')
|
||||
setTimeout(() => {
|
||||
app.quit()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function rebuildTrayMenu() {
|
||||
if (mainWindow.isVisible()) {
|
||||
tray.setContextMenu(createTrayMenu(i18n, mainWindow.isVisible(), () => {
|
||||
mainWindow.webContents.send('before-hide')
|
||||
setTimeout(() => {
|
||||
mainWindow.hide()
|
||||
rebuildTrayMenu()
|
||||
}, 2000)
|
||||
}, createSettingsWindow, showQuitDialog))
|
||||
return
|
||||
}
|
||||
tray.setContextMenu(createTrayMenu(i18n, mainWindow.isVisible(), () => {
|
||||
mainWindow.show()
|
||||
mainWindow.webContents.send('after-show')
|
||||
rebuildTrayMenu()
|
||||
}, createSettingsWindow, showQuitDialog))
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 300 * 1.5,
|
||||
@@ -57,115 +87,11 @@ function createWindow(): void {
|
||||
else {
|
||||
mainWindow.loadFile(join(import.meta.dirname, '..', '..', 'out', 'renderer', 'index.html'))
|
||||
}
|
||||
|
||||
ipcMain.on('start-window-drag', (_) => {
|
||||
isDragging = true
|
||||
const mousePos = screen.getCursorScreenPoint()
|
||||
const [windowX, windowY] = mainWindow.getPosition()
|
||||
|
||||
// Calculate the offset between cursor and window position
|
||||
dragOffset = {
|
||||
x: mousePos.x - windowX,
|
||||
y: mousePos.y - windowY,
|
||||
}
|
||||
|
||||
// Stop any existing animations
|
||||
if (currentAnimationX) {
|
||||
currentAnimationX.stop()
|
||||
currentAnimationX = null
|
||||
}
|
||||
if (currentAnimationY) {
|
||||
currentAnimationY.stop()
|
||||
currentAnimationY = null
|
||||
}
|
||||
|
||||
// Initialize last position for velocity tracking
|
||||
lastMousePosition = { x: mousePos.x, y: mousePos.y }
|
||||
lastMouseTime = Date.now()
|
||||
currentVelocity = { x: 0, y: 0 }
|
||||
|
||||
// Start global mouse tracking
|
||||
if (!globalMouseTracker) {
|
||||
globalMouseTracker = setInterval(() => {
|
||||
const mousePos = screen.getCursorScreenPoint()
|
||||
if (isDragging) {
|
||||
handleWindowMove(mousePos.x, mousePos.y)
|
||||
}
|
||||
}, 16) // ~60fps
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('end-window-drag', () => {
|
||||
isDragging = false
|
||||
if (globalMouseTracker) {
|
||||
clearInterval(globalMouseTracker)
|
||||
globalMouseTracker = null
|
||||
}
|
||||
|
||||
// Apply inertia animation when drag ends
|
||||
const [currentX, currentY] = mainWindow.getPosition()
|
||||
let latestX = currentX
|
||||
let latestY = currentY
|
||||
|
||||
const inertiaConfig = {
|
||||
power: 0.4, // Reduced from 0.6 for stronger resistance
|
||||
timeConstant: 250, // Reduced from 400 for quicker deceleration
|
||||
modifyTarget: (v: number) => v,
|
||||
min: 0,
|
||||
max: Infinity,
|
||||
}
|
||||
|
||||
// Clamp velocity to reasonable values
|
||||
const clampVelocity = (v: number) => {
|
||||
const maxVelocity = 500 // Reduced from 800 for less momentum
|
||||
const minVelocity = -500
|
||||
return Math.min(Math.max(v, minVelocity), maxVelocity)
|
||||
}
|
||||
|
||||
// Reduce velocity amplification and clamp values
|
||||
const amplifiedVelocity = {
|
||||
x: clampVelocity(currentVelocity.x * 0.2), // Reduced from 0.3 for less momentum
|
||||
y: clampVelocity(currentVelocity.y * 0.2),
|
||||
}
|
||||
|
||||
// Ignore very small movements
|
||||
if (Math.abs(amplifiedVelocity.x) > 35 || Math.abs(amplifiedVelocity.y) > 35) {
|
||||
currentAnimationX = inertia({
|
||||
from: currentX,
|
||||
velocity: amplifiedVelocity.x,
|
||||
...inertiaConfig,
|
||||
onUpdate: (x) => {
|
||||
latestX = Math.round(x)
|
||||
mainWindow.setPosition(latestX, Math.round(latestY))
|
||||
},
|
||||
onComplete: () => {
|
||||
currentAnimationX = null
|
||||
},
|
||||
})
|
||||
|
||||
currentAnimationY = inertia({
|
||||
from: currentY,
|
||||
velocity: amplifiedVelocity.y,
|
||||
...inertiaConfig,
|
||||
onUpdate: (y) => {
|
||||
latestY = Math.round(y)
|
||||
mainWindow.setPosition(Math.round(latestX), latestY)
|
||||
},
|
||||
onComplete: () => {
|
||||
currentAnimationY = null
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('move-window', (_, cursorX: number, cursorY: number) => {
|
||||
handleWindowMove(cursorX, cursorY)
|
||||
})
|
||||
}
|
||||
|
||||
let settingsWindow: BrowserWindow | null = null
|
||||
|
||||
function createSettingsWindow(): void {
|
||||
function createSettingsWindow() {
|
||||
if (settingsWindow) {
|
||||
settingsWindow.show()
|
||||
return
|
||||
@@ -210,30 +136,7 @@ function createSettingsWindow(): void {
|
||||
// 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)
|
||||
createApplicationMenu(i18n, showQuitDialog, createSettingsWindow)
|
||||
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('com.github.moeru-ai.airi-tamagotchi')
|
||||
@@ -246,19 +149,7 @@ app.whenReady().then(() => {
|
||||
})
|
||||
|
||||
// IPC test
|
||||
// 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('quit', showQuitDialog)
|
||||
|
||||
ipcMain.on('open-settings', () => createSettingsWindow())
|
||||
|
||||
@@ -271,44 +162,19 @@ app.whenReady().then(() => {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
const trayIcon = platform === 'darwin'
|
||||
? nativeImage.createFromPath(trayIconMacos).resize({ width: 16, height: 16 })
|
||||
: nativeImage.createFromPath(icon).resize({ width: 16, height: 16 })
|
||||
tray = new Tray(trayIcon)
|
||||
tray.setToolTip('Airi')
|
||||
rebuildTrayMenu()
|
||||
|
||||
app.dock.hide()
|
||||
|
||||
ipcMain.on('locale-changed', (_, language: string) => {
|
||||
i18n.setLocale(language)
|
||||
rebuildTrayMenu()
|
||||
createApplicationMenu(i18n, showQuitDialog, createSettingsWindow)
|
||||
})
|
||||
})
|
||||
|
||||
// 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.
|
||||
|
||||
function handleWindowMove(cursorX: number, cursorY: number) {
|
||||
if (!isDragging)
|
||||
return
|
||||
|
||||
// Calculate actual velocity based on mouse movement
|
||||
const currentTime = Date.now()
|
||||
const deltaTime = currentTime - lastMouseTime
|
||||
|
||||
if (deltaTime > 0) {
|
||||
// Smooth out velocity calculation with some averaging
|
||||
const newVelocityX = (cursorX - lastMousePosition.x) / deltaTime * 1000
|
||||
const newVelocityY = (cursorY - lastMousePosition.y) / deltaTime * 1000
|
||||
|
||||
currentVelocity = {
|
||||
x: currentVelocity.x * 0.8 + newVelocityX * 0.2, // Smooth velocity
|
||||
y: currentVelocity.y * 0.8 + newVelocityY * 0.2,
|
||||
}
|
||||
}
|
||||
|
||||
// Update window position based on cursor position and offset
|
||||
const newX = cursorX - dragOffset.x
|
||||
const newY = cursorY - dragOffset.y
|
||||
mainWindow.setPosition(Math.round(newX), Math.round(newY))
|
||||
|
||||
lastMousePosition = { x: cursorX, y: cursorY }
|
||||
lastMouseTime = currentTime
|
||||
}
|
||||
|
||||
@@ -4,10 +4,15 @@ export default {
|
||||
quit: 'Quit',
|
||||
about: 'About',
|
||||
toggleDevTools: 'Toggle Developer Tools',
|
||||
show: 'Show airi',
|
||||
hide: 'Hide airi',
|
||||
},
|
||||
quitDialog: {
|
||||
title: 'Quit',
|
||||
message: 'Are you sure you want to quit?',
|
||||
buttons: ['Quit', 'Cancel'],
|
||||
buttons: {
|
||||
quit: 'Quit',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('createI18n', () => {
|
||||
|
||||
it('should return key if the key is not found', () => {
|
||||
const { t } = createI18n()
|
||||
// @ts-expect-error for test
|
||||
expect(t('menu.not.found')).toBe('menu.not.found')
|
||||
})
|
||||
|
||||
@@ -21,7 +22,7 @@ describe('createI18n', () => {
|
||||
|
||||
it('should return the correct locale in array', () => {
|
||||
const { t } = createI18n()
|
||||
expect(t('quitDialog.buttons.0')).toBe('Quit')
|
||||
expect(t('quitDialog.buttons.1')).toBe('Cancel')
|
||||
expect(t('quitDialog.buttons.quit')).toBe('Quit')
|
||||
expect(t('quitDialog.buttons.cancel')).toBe('Cancel')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,11 +7,17 @@ const locales = {
|
||||
'zh-CN': zhCN,
|
||||
}
|
||||
|
||||
type Message = typeof locales['en-US']
|
||||
type KeyOfExcludeSymbol<T> = Exclude<keyof T, symbol>
|
||||
type ValueOf<T> = T[KeyOfExcludeSymbol<T>]
|
||||
type PathOf<T, Root extends boolean = true> = T extends Array<any> ? number : T extends string ? '' : Root extends true ? `${KeyOfExcludeSymbol<T>}${PathOf<ValueOf<T>, false>}` : `.${KeyOfExcludeSymbol<T>}${PathOf<ValueOf<T>, false>}`
|
||||
type LocalePath = PathOf<Message>
|
||||
|
||||
export function createI18n() {
|
||||
let locale = 'en-US'
|
||||
let messages = locales['en-US']
|
||||
|
||||
function t(key: string) {
|
||||
function t(key: LocalePath) {
|
||||
const path = key.split('.')
|
||||
let current = messages
|
||||
let result = ''
|
||||
@@ -44,3 +50,5 @@ export function createI18n() {
|
||||
locale,
|
||||
}
|
||||
}
|
||||
|
||||
export type I18n = ReturnType<typeof createI18n>
|
||||
|
||||
@@ -4,10 +4,15 @@ export default {
|
||||
quit: '退出',
|
||||
about: '关于',
|
||||
toggleDevTools: '切换开发者工具',
|
||||
show: '显示 airi',
|
||||
hide: '隐藏 airi',
|
||||
},
|
||||
quitDialog: {
|
||||
title: '退出',
|
||||
message: '确定要退出吗?',
|
||||
buttons: ['退出', '取消'],
|
||||
buttons: {
|
||||
quit: '退出',
|
||||
cancel: '取消',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { I18n } from './locales'
|
||||
|
||||
import { Menu } from 'electron/main'
|
||||
|
||||
export function createBasicMenu(
|
||||
{ t }: I18n,
|
||||
createSettingsWindow: () => void,
|
||||
onQuitClick: () => void,
|
||||
): Array<(Electron.MenuItemConstructorOptions)> {
|
||||
return [
|
||||
{
|
||||
role: 'about',
|
||||
label: t('menu.about'),
|
||||
},
|
||||
{
|
||||
label: t('menu.settings'),
|
||||
click: createSettingsWindow,
|
||||
},
|
||||
{
|
||||
label: t('menu.quit'),
|
||||
click: onQuitClick,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function createTrayMenu(
|
||||
i18n: I18n,
|
||||
isVisible: boolean,
|
||||
onVisibleChange: () => void,
|
||||
createSettingsWindow: () => void,
|
||||
onQuitClick: () => void,
|
||||
) {
|
||||
const menu = createBasicMenu(i18n, createSettingsWindow, onQuitClick)
|
||||
if (isVisible) {
|
||||
menu.push({
|
||||
label: i18n.t('menu.hide'),
|
||||
click: onVisibleChange,
|
||||
})
|
||||
}
|
||||
else {
|
||||
menu.push({
|
||||
label: i18n.t('menu.show'),
|
||||
click: onVisibleChange,
|
||||
})
|
||||
}
|
||||
return Menu.buildFromTemplate(menu)
|
||||
}
|
||||
|
||||
export function createApplicationMenu(
|
||||
i18n: I18n,
|
||||
onQuitClick: () => void,
|
||||
createSettingsWindow: () => void,
|
||||
) {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'airi',
|
||||
role: 'appMenu',
|
||||
submenu: createBasicMenu(i18n, createSettingsWindow, onQuitClick),
|
||||
},
|
||||
{
|
||||
role: 'editMenu',
|
||||
},
|
||||
])
|
||||
Menu.setApplicationMenu(menu)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { watch } from 'vue'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
@@ -11,6 +11,11 @@ watch(() => settings.language, (language) => {
|
||||
i18n.locale.value = language
|
||||
window.electron.ipcRenderer.send('locale-changed', language)
|
||||
})
|
||||
|
||||
// FIXME: store settings to file
|
||||
onMounted(() => {
|
||||
window.electron.ipcRenderer.send('locale-changed', settings.language)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -34,7 +34,7 @@ onTokenLiteral(async () => {
|
||||
<template>
|
||||
<div py="1" flex="~ col" rounded="lg" overflow-hidden>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div ref="chatHistoryRef" v-auto-animate h-full w-full max-h="30vh" flex="~ col" overflow-scroll>
|
||||
<div ref="chatHistoryRef" v-auto-animate max-h="30vh" flex="~ col" scrollbar="~ w-2 track-color-transparent thumb-color-pink-300 rounded" h-full w-full overflow-scroll>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div v-for="(message, index) in messages" :key="index" mb-2>
|
||||
<div v-if="message.role === 'assistant'" flex mr="12">
|
||||
|
||||
@@ -89,7 +89,7 @@ onMounted(() => {
|
||||
<template>
|
||||
<div>
|
||||
<div relative w-full flex gap-1>
|
||||
<TamagotchiChatHistory absolute left-0 top-0 transform="translate-y-[-100%]" w-full />
|
||||
<TamagotchiChatHistory transform="translate-y-[-100%]" absolute left-0 top-0 w-full />
|
||||
<div flex flex-1>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
|
||||
@@ -60,7 +60,7 @@ const modeIndicatorClass = computed(() => {
|
||||
>
|
||||
<div
|
||||
v-if="windowStore.controlMode === WindowControlMode.MOVE"
|
||||
class="drag-region absolute left-0 top-0 z-999 h-full w-full flex items-center justify-center"
|
||||
class="drag-region absolute left-0 top-0 z-999 h-full w-full flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<div class="absolute h-32 w-full flex items-center justify-center b-2 b-pink bg-white">
|
||||
<div class="wall absolute top-0 h-8" />
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
transformerDirectives,
|
||||
transformerVariantGroup,
|
||||
} from 'unocss'
|
||||
import { presetScrollbar } from 'unocss-preset-scrollbar'
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
@@ -30,6 +31,7 @@ export default defineConfig({
|
||||
...createExternalPackageIconLoader('@proj-airi/lobe-icons'),
|
||||
},
|
||||
}),
|
||||
presetScrollbar(),
|
||||
],
|
||||
transformers: [
|
||||
transformerDirectives(),
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Emotion } from '../../constants/emotions'
|
||||
import { generateSpeech } from '@xsai/generate-speech'
|
||||
import { createUnElevenLabs } from '@xsai/providers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useMarkdown } from '../../composables/markdown'
|
||||
@@ -12,7 +12,7 @@ import { useQueue } from '../../composables/queue'
|
||||
import { useDelayMessageQueue, useEmotionsMessageQueue, useMessageContentQueue } from '../../composables/queues'
|
||||
import { llmInferenceEndToken } from '../../constants'
|
||||
import { Voice, voiceMap } from '../../constants/elevenlabs'
|
||||
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionThinkMotionName } from '../../constants/emotions'
|
||||
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionAngryMotionName, EmotionHappyMotionName, EmotionThinkMotionName } from '../../constants/emotions'
|
||||
import { useAudioContext, useSpeakingStore } from '../../stores/audio'
|
||||
import { useChatStore } from '../../stores/chat'
|
||||
import { useSettings } from '../../stores/settings'
|
||||
@@ -21,9 +21,10 @@ import VRMScene from '../Scenes/VRM.vue'
|
||||
|
||||
import '../../utils/live2d-zip-loader'
|
||||
|
||||
const live2DViewerRef = ref<{ setMotion: (motionName: string) => Promise<void> }>()
|
||||
const vrmViewerRef = ref<{ setExpression: (expression: string) => void }>()
|
||||
|
||||
const motion = ref('')
|
||||
|
||||
const { stageView, elevenLabsApiKey, elevenlabsVoiceEnglish, elevenlabsVoiceJapanese } = storeToRefs(useSettings())
|
||||
const { mouthOpenSize } = storeToRefs(useSpeakingStore())
|
||||
const { audioContext, calculateVolume } = useAudioContext()
|
||||
@@ -115,7 +116,7 @@ const emotionsQueue = useQueue<Emotion>({
|
||||
await vrmViewerRef.value!.setExpression(value)
|
||||
}
|
||||
else if (stageView.value === '2d') {
|
||||
await live2DViewerRef.value!.setMotion(EMOTION_EmotionMotionName_value[ctx.data])
|
||||
motion.value = EMOTION_EmotionMotionName_value[ctx.data]
|
||||
}
|
||||
},
|
||||
],
|
||||
@@ -160,7 +161,7 @@ onBeforeMessageComposed(async () => {
|
||||
})
|
||||
|
||||
onBeforeSend(async () => {
|
||||
live2DViewerRef.value?.setMotion(EmotionThinkMotionName)
|
||||
motion.value = EmotionThinkMotionName
|
||||
})
|
||||
|
||||
onTokenLiteral(async (literal) => {
|
||||
@@ -178,6 +179,21 @@ onStreamEnd(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
lipSyncStarted.value = false
|
||||
window.electron?.ipcRenderer.removeAllListeners('before-hide')
|
||||
window.electron?.ipcRenderer.removeAllListeners('after-show')
|
||||
window.electron?.ipcRenderer.removeAllListeners('before-quit')
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.electron?.ipcRenderer.on('before-hide', () => {
|
||||
motion.value = EmotionAngryMotionName
|
||||
})
|
||||
window.electron?.ipcRenderer.on('after-show', () => {
|
||||
motion.value = EmotionHappyMotionName
|
||||
})
|
||||
window.electron?.ipcRenderer.on('before-quit', () => {
|
||||
motion.value = EmotionThinkMotionName
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -186,7 +202,7 @@ onUnmounted(() => {
|
||||
<div h-full w-full>
|
||||
<Live2DScene
|
||||
v-if="stageView === '2d'"
|
||||
ref="live2DViewerRef"
|
||||
v-model:motion="motion"
|
||||
:mouth-open-size="mouthOpenSize"
|
||||
model="./assets/live2d/models/hiyori_pro_zh.zip"
|
||||
min-w="50% <lg:full" min-h="100 sm:100" h-full w-full flex-1
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import type { ElectronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron?: ElectronAPI
|
||||
api?: unknown
|
||||
}
|
||||
}
|
||||
Generated
+79
@@ -409,6 +409,9 @@ importers:
|
||||
markdown-it-link-attributes:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
unocss-preset-scrollbar:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(unocss@66.0.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)))
|
||||
unplugin-auto-import:
|
||||
specifier: ^19.1.0
|
||||
version: 19.1.0(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@12.7.0(typescript@5.7.3))
|
||||
@@ -4458,6 +4461,9 @@ packages:
|
||||
resolution: {integrity: sha512-nFRGop/guBa4jLkrgXjaRDm5JPz4x3YpP10m5IQkHpHwlnHUVn1L9smyPl04ohYWhYn9ZcAHgR28Ih2jwta8hw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@unocss/core@65.5.0':
|
||||
resolution: {integrity: sha512-XYWdS09M2XOjZNDotGhI2TIW/6duLNiyssopwjCbv4AlPklF0bZI86SKI55syYDBt6GRadoQbuvUkhSiTV/hzQ==}
|
||||
|
||||
'@unocss/core@66.0.0':
|
||||
resolution: {integrity: sha512-PdVbSMHNDDkr++9nkqzsZRAkaU84gxMTEgYbqI7dt2p1DXp/5tomVtmMsr2/whXGYKRiUc0xZ3p4Pzraz8TcXA==}
|
||||
|
||||
@@ -4469,6 +4475,9 @@ packages:
|
||||
resolution: {integrity: sha512-KTP6uK0loH9+PkUjL2F4eyuMcUZRiVYkg4zJfqVWNctE1yGkuTUzCvm6ORRvLakajAU8G/Zzvuo1pE94zyZQbw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@unocss/extractor-arbitrary-variants@65.5.0':
|
||||
resolution: {integrity: sha512-7K3gftOdkv9jbWvbkExTcx6/FDP2Xk/NSsOYTvR9oITLnLjmdQvp+9276WSnNfKF3frBl8ZcqpkC2EsuL2Yutw==}
|
||||
|
||||
'@unocss/extractor-arbitrary-variants@66.0.0':
|
||||
resolution: {integrity: sha512-vlkOIOuwBfaFBJcN6o7+obXjigjOlzVFN/jT6pG1WXbQDTRZ021jeF3i9INdb9D/0cQHSeDvNgi1TJ5oUxfiow==}
|
||||
|
||||
@@ -4487,6 +4496,9 @@ packages:
|
||||
'@unocss/preset-icons@66.0.0':
|
||||
resolution: {integrity: sha512-6ObwTvEGuPBbKWRoMMiDioHtwwQTFI5oojFLJ32Y8tW6TdXvBLkO88d7qpgQxEjgVt4nJrqF1WEfR4niRgBm0Q==}
|
||||
|
||||
'@unocss/preset-mini@65.5.0':
|
||||
resolution: {integrity: sha512-oD2INmEgTOxmFsVceflv4Zs67vz9PRbpg3+CMsJLWgfX4UdQ1H4jZms72+g3N1hhXBvOFwvGvqGaMnrVMRk54g==}
|
||||
|
||||
'@unocss/preset-mini@66.0.0':
|
||||
resolution: {integrity: sha512-d62eACnuKtR0dwCFOQXgvw5VLh5YSyK56xCzpHkh0j0GstgfDLfKTys0T/XVAAvdSvAy/8A8vhSNJ4PlIc9V2A==}
|
||||
|
||||
@@ -4511,6 +4523,10 @@ packages:
|
||||
'@unocss/reset@66.0.0':
|
||||
resolution: {integrity: sha512-YLFz/5yT7mFJC8JSmIUA5+bS3CBCJbtztOw+8rWzjQr/BEVSGuihWUUpI2Df6VVxXIXxKanZR6mIl59yvf+GEA==}
|
||||
|
||||
'@unocss/rule-utils@65.5.0':
|
||||
resolution: {integrity: sha512-xT4N0EY1dl1mqY5gTKD0H/Fg6xApe7xbfNTUwctOu02DMeJhqv9BTqfoAihH/hzGSI69+FtzVtz7hUxTypfehA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@unocss/rule-utils@66.0.0':
|
||||
resolution: {integrity: sha512-UJ51YHbwxYTGyj35ugsPlOT4gaa7tCbXdywZ3m5Nn0JgywwIqGmBFyiN9ZjHBHfJuDxmmPd6lxojoBscih/WMQ==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -10492,6 +10508,11 @@ packages:
|
||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
unocss-preset-scrollbar@3.2.0:
|
||||
resolution: {integrity: sha512-j8BOoh2RgPm2U8XqEjMQ+XQk4YWYPH4T+yzv3fndxS+VpdizQinMvHmfsZGLN3yMv7I4O5Qi8fVTlQDhETyzbA==}
|
||||
peerDependencies:
|
||||
unocss: '>= 0.31.13'
|
||||
|
||||
unocss@66.0.0:
|
||||
resolution: {integrity: sha512-SHstiv1s7zGPSjzOsADzlwRhQM+6817+OqQE3Fv+N/nn2QLNx1bi3WXybFfz5tWkzBtyTZlwdPmeecsIs1yOCA==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -14688,6 +14709,8 @@ snapshots:
|
||||
'@unocss/core': 66.0.0
|
||||
unconfig: 7.0.0
|
||||
|
||||
'@unocss/core@65.5.0': {}
|
||||
|
||||
'@unocss/core@66.0.0': {}
|
||||
|
||||
'@unocss/eslint-config@66.0.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)':
|
||||
@@ -14711,6 +14734,10 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@unocss/extractor-arbitrary-variants@65.5.0':
|
||||
dependencies:
|
||||
'@unocss/core': 65.5.0
|
||||
|
||||
'@unocss/extractor-arbitrary-variants@66.0.0':
|
||||
dependencies:
|
||||
'@unocss/core': 66.0.0
|
||||
@@ -14735,6 +14762,15 @@ snapshots:
|
||||
postcss: 8.4.49
|
||||
tinyglobby: 0.2.11
|
||||
|
||||
'@unocss/postcss@66.0.0(postcss@8.5.3)':
|
||||
dependencies:
|
||||
'@unocss/config': 66.0.0
|
||||
'@unocss/core': 66.0.0
|
||||
'@unocss/rule-utils': 66.0.0
|
||||
css-tree: 3.1.0
|
||||
postcss: 8.5.3
|
||||
tinyglobby: 0.2.11
|
||||
|
||||
'@unocss/preset-attributify@66.0.0':
|
||||
dependencies:
|
||||
'@unocss/core': 66.0.0
|
||||
@@ -14747,6 +14783,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@unocss/preset-mini@65.5.0':
|
||||
dependencies:
|
||||
'@unocss/core': 65.5.0
|
||||
'@unocss/extractor-arbitrary-variants': 65.5.0
|
||||
'@unocss/rule-utils': 65.5.0
|
||||
|
||||
'@unocss/preset-mini@66.0.0':
|
||||
dependencies:
|
||||
'@unocss/core': 66.0.0
|
||||
@@ -14786,6 +14828,11 @@ snapshots:
|
||||
|
||||
'@unocss/reset@66.0.0': {}
|
||||
|
||||
'@unocss/rule-utils@65.5.0':
|
||||
dependencies:
|
||||
'@unocss/core': 65.5.0
|
||||
magic-string: 0.30.17
|
||||
|
||||
'@unocss/rule-utils@66.0.0':
|
||||
dependencies:
|
||||
'@unocss/core': 66.0.0
|
||||
@@ -22675,6 +22722,11 @@ snapshots:
|
||||
|
||||
universalify@2.0.0: {}
|
||||
|
||||
unocss-preset-scrollbar@3.2.0(unocss@66.0.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))):
|
||||
dependencies:
|
||||
'@unocss/preset-mini': 65.5.0
|
||||
unocss: 66.0.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
|
||||
|
||||
unocss@66.0.0(postcss@8.4.49)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)):
|
||||
dependencies:
|
||||
'@unocss/astro': 66.0.0(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
|
||||
@@ -22702,6 +22754,33 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
unocss@66.0.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)):
|
||||
dependencies:
|
||||
'@unocss/astro': 66.0.0(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
|
||||
'@unocss/cli': 66.0.0
|
||||
'@unocss/core': 66.0.0
|
||||
'@unocss/postcss': 66.0.0(postcss@8.5.3)
|
||||
'@unocss/preset-attributify': 66.0.0
|
||||
'@unocss/preset-icons': 66.0.0
|
||||
'@unocss/preset-mini': 66.0.0
|
||||
'@unocss/preset-tagify': 66.0.0
|
||||
'@unocss/preset-typography': 66.0.0
|
||||
'@unocss/preset-uno': 66.0.0
|
||||
'@unocss/preset-web-fonts': 66.0.0
|
||||
'@unocss/preset-wind': 66.0.0
|
||||
'@unocss/preset-wind3': 66.0.0
|
||||
'@unocss/transformer-attributify-jsx': 66.0.0
|
||||
'@unocss/transformer-compile-class': 66.0.0
|
||||
'@unocss/transformer-directives': 66.0.0
|
||||
'@unocss/transformer-variant-group': 66.0.0
|
||||
'@unocss/vite': 66.0.0(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
|
||||
optionalDependencies:
|
||||
vite: 6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)
|
||||
transitivePeerDependencies:
|
||||
- postcss
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
unplugin-auto-import@19.1.0(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@12.7.0(typescript@5.7.3)):
|
||||
|
||||
Reference in New Issue
Block a user