feat: 全新升级偏好设置 UI (#89)

This commit is contained in:
ayangweb
2025-04-25 22:38:44 +08:00
committed by GitHub
parent 474bc5202a
commit 0b1be2d787
22 changed files with 234 additions and 139 deletions

View File

@@ -21,6 +21,7 @@ export default antfu({
},
],
'vue/attributes-order': ['error', { alphabetical: true }],
'vue/max-attributes-per-line': 'error',
},
ignores: ['**/*.toml'],
})

View File

@@ -21,6 +21,7 @@
"release": "release-it"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-log": "~2.3.1",
"@tauri-apps/plugin-opener": "~2.2.6",

3
pnpm-lock.yaml generated
View File

@@ -13,6 +13,9 @@ importers:
.:
dependencies:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.5.13(typescript@5.6.3))
'@tauri-apps/api':
specifier: ^2.5.0
version: 2.5.0

View File

@@ -10,40 +10,30 @@
"DisplayInfo": "demomodel2.cdi3.json",
"Expressions": [
{
"Name": "live2d_expression0.exp3.json",
"Name": "默认喵",
"File": "live2d_expression0.exp3.json"
},
{
"Name": "live2d_expression1.exp3.json",
"File": "live2d_expression1.exp3.json"
"Name": "社会喵",
"File": "live2d_expression1.exp3.json",
"Description": "喵喵我叼根小烟耍个帅气俏皮的wink~超有范儿!但直播里用这招,怕是会让铲屎官和平台瞪大眼,本喵还是低调点,偷偷耍酷好啦!"
},
{
"Name": "live2d_expression2.exp3.json",
"Name": "天使喵",
"File": "live2d_expression2.exp3.json"
}
],
"Motions": {
"CAT_motion": [
{
"Name": "雷霆喵",
"File": "live2d_motion1.motion3.json",
"Sound": "live2d_motion1.flac",
"FadeInTime": 0,
"FadeOutTime": 0
},
{
"File": "live2d_motion2.motion3.json",
"FadeInTime": 0,
"FadeOutTime": 0
}
],
"CAT_motion_lock": [
{
"File": "live2d_motion1.motion3.json",
"Sound": "live2d_motion1.flac",
"FadeInTime": 0,
"FadeOutTime": 0
},
{
"Name": "摇摆喵",
"File": "live2d_motion2.motion3.json",
"FadeInTime": 0,
"FadeOutTime": 0

View File

@@ -10,34 +10,31 @@
"DisplayInfo": "demomodel.cdi3.json",
"Expressions": [
{
"Key": "",
"Name": "去掉表情",
"Name": "默认喵",
"File": "live2d_expression0.exp3.json"
},
{
"Key": "",
"Name": "戴上墨镜",
"File": "live2d_expression1.exp3.json"
"Name": "社会喵",
"File": "live2d_expression1.exp3.json",
"Description": "喵喵我叼根小烟耍个帅气俏皮的wink~超有范儿!但直播里用这招,怕是会让铲屎官和平台瞪大眼,本喵还是低调点,偷偷耍酷好啦!"
},
{
"Key": "",
"Name": "升天",
"Name": "天使喵",
"File": "live2d_expression2.exp3.json"
}
],
"Motions": {
"CAT_motion": [
{
"Key": "",
"Name": "打雷",
"Name": "雷霆喵",
"File": "live2d_motion1.motion3.json",
"Sound": "live2d_motion1.flac",
"FadeInTime": 0,
"FadeOutTime": 0
},
{
"Key": "",
"Name": "左手摇摆",
"Name": "摇摆喵",
"File": "live2d_motion2.motion3.json",
"FadeInTime": 0,
"FadeOutTime": 0

View File

@@ -31,8 +31,6 @@
"label": "preference",
"title": "偏好设置",
"url": "index.html/#/preference",
"width": 600,
"height": 450,
"visible": false,
"resizable": false,
"maximizable": false,

View File

@@ -22,17 +22,23 @@ const hasDescription = computed(() => {
<template>
<Flex
:align="vertical ? void 0 : 'center'"
class="b b-color-2 rounded-lg b-solid p-4"
class="b b-color-2 rounded-lg b-solid bg-white p-4"
gap="middle"
justify="space-between"
:vertical="vertical"
>
<Flex align="center">
<slot name="icon">
<div class="text-4" :class="icon" />
<div
class="text-4"
:class="icon"
/>
</slot>
<Flex :class="{ 'ml-4': hasIcon }" vertical>
<Flex
:class="{ 'ml-4': hasIcon }"
vertical
>
<div class="text-sm font-medium">
{{ title }}
</div>

View File

@@ -7,12 +7,22 @@ const { title } = defineProps<{
</script>
<template>
<Flex class="mb-4" gap="small" vertical>
<div class="text-base font-medium" data-tauri-drag-region>
<Flex
class="mb-4"
gap="small"
vertical
>
<div
class="text-base font-medium"
data-tauri-drag-region
>
{{ title }}
</div>
<Flex gap="middle" vertical>
<Flex
gap="middle"
vertical
>
<slot />
</Flex>
</FLex>

View File

@@ -107,12 +107,24 @@ async function handleOk() {
</script>
<template>
<Modal v-model:open="open" cancel-text="稍后更新" centered :closable="false" :mask-closable="false" title="发现新版本🥳" @ok="handleOk">
<Modal
v-model:open="open"
cancel-text="稍后更新"
centered
:closable="false"
:mask-closable="false"
title="发现新版本🥳"
@ok="handleOk"
>
<template #okText>
{{ loading ? downloadProgress : "立即更新" }}
</template>
<Flex class="pt-1" gap="small" vertical>
<Flex
class="pt-1"
gap="small"
vertical
>
<Flex align="center">
<span>更新版本</span>
<span>

View File

@@ -5,19 +5,28 @@ import { watch } from 'vue'
import live2d from '../utils/live2d'
import { getCursorMonitor } from '../utils/monitor'
import { useTauriListen } from './useTauriListen'
import { LISTEN_KEY } from '@/constants'
import { useCatStore } from '@/stores/cat'
import { useModelStore } from '@/stores/model'
export function useModel() {
const carStore = useCatStore()
const catStore = useCatStore()
const modelStore = useModelStore()
watch(() => carStore.mode, handleLoad)
watch(() => catStore.mode, handleLoad)
useTauriListen<number>(LISTEN_KEY.PLAY_EXPRESSION, ({ payload }) => {
live2d.playExpressions(payload)
})
async function handleLoad() {
const data = await live2d.load(`/models/${carStore.mode}/cat.model3.json`)
const data = await live2d.load(`/models/${catStore.mode}/cat.model3.json`)
handleResize()
Object.assign(carStore, data)
Object.assign(modelStore, data)
}
function handleDestroy() {
@@ -49,7 +58,7 @@ export function useModel() {
}
async function handleMouseMove() {
if (carStore.mode !== 'standard' || !live2d.model) return
if (catStore.mode !== 'standard' || !live2d.model) return
const monitor = await getCursorMonitor()

View File

@@ -5,4 +5,5 @@ export const LISTEN_KEY = {
HIDE_WINDOW: 'hide-window',
DEVICE_CHANGED: 'device-changed',
UPDATE_APP: 'update-app',
PLAY_EXPRESSION: 'play-expression',
}

View File

@@ -1,35 +0,0 @@
<script setup lang="ts">
import { Flex } from 'ant-design-vue'
import { onMounted } from 'vue'
import UpdateApp from '@/components/update-app/index.vue'
import { useTray } from '@/composables/useTray'
import { preferenceRoutes } from '@/router'
import { isMac } from '@/utils/platform'
const { createTray } = useTray()
onMounted(async () => {
createTray()
})
</script>
<template>
<Flex class="h-screen">
<div class="h-full w-40 bg-color-8" :class="[isMac ? 'pt-8' : 'pt-4']" data-tauri-drag-region>
<Flex class="px-2" gap="small" vertical>
<RouterLink v-for="item in preferenceRoutes" :key="item.path" active-class="bg-primary! text-white! font-bold" class="h-10 flex items-center gap-2 rounded-lg hover:bg-color-6 px-4 text-color-1! transition" :to="item.path" @click.stop>
<div class="size-5" :class="item.meta?.icon" />
<span>{{ item.meta?.title }}</span>
</RouterLink>
</Flex>
</div>
<div class="flex-1 p-4" data-tauri-drag-region>
<RouterView />
</div>
</Flex>
<UpdateApp />
</template>

View File

@@ -38,7 +38,7 @@ watch(pressedKeys, handleKeyDown)
watch(() => catStore.penetrable, (value) => {
appWindow.setIgnoreCursorEvents(value)
})
}, { immediate: true })
function handleWindowDrag() {
appWindow.startDragging()
@@ -60,9 +60,16 @@ function resolveImageURL(key: string) {
<canvas id="live2dCanvas" />
<img v-for="key in pressedKeys" :key="key" :src="resolveImageURL(key)">
<img
v-for="key in pressedKeys"
:key="key"
:src="resolveImageURL(key)"
>
<div v-show="resizing" class="flex items-center justify-center bg-black">
<div
v-show="resizing"
class="flex items-center justify-center bg-black"
>
<span class="text-center text-5xl text-white">
重绘中...
</span>

View File

@@ -1,6 +0,0 @@
<script setup lang="ts">
</script>
<template>
敬请期待...
</template>

View File

@@ -21,18 +21,32 @@ function feedbackIssue() {
<template>
<ProList title="关于软件">
<ProListItem :description="`版本v${appStore.version}`" :title="appStore.name">
<Button type="primary" @click="handleUpdate">
<ProListItem
:description="`版本v${appStore.version}`"
:title="appStore.name"
>
<Button
type="primary"
@click="handleUpdate"
>
检查更新
</Button>
<template #icon>
<img class="size-12 drop-shadow" src="/images/logo.png">
<div class="b b-color-2 rounded-xl b-solid">
<img
class="size-12"
src="/images/logo.png"
>
</div>
</template>
</ProListItem>
<ProListItem title="开源地址">
<Button danger @click="feedbackIssue">
<Button
danger
@click="feedbackIssue"
>
反馈问题
</Button>

View File

@@ -24,17 +24,30 @@ const modeList: SelectProps['options'] = [
<template>
<ProList title="模式设置">
<ProListItem title="选择模式">
<Select v-model:value="catStore.mode" :options="modeList" title="选择模式" />
<Select
v-model:value="catStore.mode"
:options="modeList"
title="选择模式"
/>
</ProListItem>
</ProList>
<ProList title="窗口设置">
<ProListItem description="启用后,窗口不影响对其他应用程序的操作" title="窗口穿透">
<ProListItem
description="启用后,窗口不影响对其他应用程序的操作"
title="窗口穿透"
>
<Switch v-model:checked="catStore.penetrable" />
</ProListItem>
<ProListItem title="透明度" vertical>
<Slider v-model:value="catStore.opacity" class="m-0!" />
<ProListItem
title="透明度"
vertical
>
<Slider
v-model:value="catStore.opacity"
class="m-0!"
/>
</ProListItem>
<ProListItem title="镜像模式">

View File

@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<div>
敬请期待
</div>
</template>

View File

@@ -0,0 +1,96 @@
<script setup lang="ts">
import { Flex } from 'ant-design-vue'
import { computed, onMounted, ref } from 'vue'
import About from './components/about/index.vue'
import Cat from './components/cat/index.vue'
import General from './components/general/index.vue'
import Model from './components/model/index.vue'
import UpdateApp from '@/components/update-app/index.vue'
import { useTray } from '@/composables/useTray'
import { useAppStore } from '@/stores/app'
import { isMac } from '@/utils/platform'
const { createTray } = useTray()
const appStore = useAppStore()
const current = ref(0)
onMounted(async () => {
createTray()
})
const menus = [
{
label: '猫咪设置',
icon: 'i-solar:cat-bold',
component: Cat,
},
{
label: '通用设置',
icon: 'i-solar:settings-minimalistic-bold',
component: General,
},
{
label: '模型管理',
icon: 'i-solar:magic-stick-3-bold',
component: Model,
},
{
label: '关于',
icon: 'i-solar:info-circle-bold',
component: About,
},
]
const currentComponent = computed(() => {
return menus[current.value].component
})
</script>
<template>
<Flex class="h-screen">
<div
class="h-full w-30 flex flex-col items-center gap-4 bg-gradient-from-primary-1 bg-gradient-to-black/1 bg-gradient-linear"
:class="[isMac ? 'pt-8' : 'pt-4']"
data-tauri-drag-region
>
<div class="flex flex-col items-center gap-2">
<div class="b b-color-2 rounded-2xl b-solid">
<img
class="size-15"
src="/images/logo.png"
>
</div>
<span class="font-bold">{{ appStore.name }}</span>
</div>
<div class="flex flex-col gap-2">
<div
v-for="(item, index) in menus"
:key="item.label"
class="size-20 flex flex-col cursor-pointer items-center justify-center gap-2 rounded-lg hover:bg-color-7 text-color-3 transition"
:class="{ 'bg-white! text-primary-5 font-bold': current === index }"
@mousedown="current = index"
>
<div
class="size-8"
:class="item.icon"
/>
<span>{{ item.label }}</span>
</div>
</div>
</div>
<div
class="flex-1 bg-color-8 p-4"
data-tauri-drag-region
>
<component :is="currentComponent" />
</div>
</Flex>
<UpdateApp />
</template>

View File

@@ -1,46 +1,9 @@
// @unocss-include
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import Preference from '../layouts/preference/index.vue'
import General from '../pages/general/index.vue'
import Main from '../pages/main/index.vue'
export const preferenceRoutes: RouteRecordRaw[] = [
{
path: 'cat',
component: () => import('../pages/cat/index.vue'),
meta: {
title: '猫咪设置',
icon: 'i-solar:cat-outline',
},
},
{
path: 'general',
component: General,
meta: {
title: '通用设置',
icon: 'i-solar:settings-outline',
},
},
{
path: 'model',
component: () => import('../pages/model/index.vue'),
meta: {
title: '模型管理',
icon: 'i-solar:magic-stick-3-outline',
},
},
{
path: 'about',
component: () => import('../pages/about/index.vue'),
meta: {
title: '关于',
icon: 'i-solar:info-circle-outline',
},
},
]
import Preference from '../pages/preference/index.vue'
const routes: Readonly<RouteRecordRaw[]> = [
{
@@ -50,8 +13,6 @@ const routes: Readonly<RouteRecordRaw[]> = [
{
path: '/preference',
component: Preference,
redirect: '/preference/cat',
children: preferenceRoutes,
},
]

View File

@@ -2,22 +2,24 @@ import { defineStore } from 'pinia'
import { ref } from 'vue'
interface Motion {
Key: string
Name: string
File: string
Sound?: string
FadeInTime: number
FadeOutTime: number
Description?: string
}
type MotionGroup = Record<string, Motion[]>
interface Expression {
Key: string
Name: string
File: string
Description?: string
}
export const useModelStore = defineStore('model', () => {
const motions = ref<Motion[]>([])
const motions = ref<MotionGroup>({})
const expressions = ref<Expression[]>([])
return {

View File

@@ -39,7 +39,7 @@ class Live2d {
this.model = model
return {
motions: Object.values(definitions).flat(),
motions: definitions,
expressions: expressionManager?.definitions ?? [],
}
}
@@ -48,6 +48,14 @@ class Live2d {
this.model?.destroy()
}
public playMotion(group: string, index: number) {
return this.model?.motion(group, index)
}
public playExpressions(index: number) {
return this.model?.expression(index)
}
public setParameterValue(id: string, value: number | boolean) {
return this.model?.internalModel.coreModel.setParameterValueById(id, Number(value))
}