feat: 新增「猫咪设置 > 窗口设置 > 窗口圆角」配置项 (#644)

This commit is contained in:
ayangweb
2025-09-17 11:22:43 +08:00
committed by GitHub
parent 7a997a4bfd
commit 0eac8e489a
12 changed files with 175 additions and 71 deletions

View File

@@ -84,7 +84,7 @@ useEventListener('click', (event) => {
<ConfigProvider
:locale="zhCN"
:theme="{
algorithm: generalStore.isDark ? darkAlgorithm : defaultAlgorithm,
algorithm: generalStore.appearance.isDark ? darkAlgorithm : defaultAlgorithm,
}"
>
<RouterView v-if="isRestored" />

View File

@@ -35,7 +35,7 @@ const MESSAGE_KEY = 'updatable'
const { pause, resume } = useIntervalFn(checkUpdate, 1000 * 60 * 60 * 24)
watch(() => generalStore.autoCheckUpdate, (value) => {
watch(() => generalStore.update.autoCheck, (value) => {
pause()
if (!value) return

View File

@@ -68,7 +68,7 @@ export function useModel() {
const size = await appWindow.size()
catStore.scale = round((size.width / width) * 100)
catStore.window.scale = round((size.width / width) * 100)
}
const handlePress = (key: string) => {
@@ -76,7 +76,7 @@ export function useModel() {
if (!path) return
if (catStore.singleMode) {
if (catStore.model.single) {
const dirName = nth(path.split(sep()), -2)!
const filterKeys = Object.entries(modelStore.pressedKeys).filter(([, value]) => {
@@ -127,7 +127,7 @@ export function useModel() {
const ratio = isXAxis ? xRatio : yRatio
let value = max - (ratio * (max - min))
if (isXAxis && catStore.mouseMirror) {
if (isXAxis && catStore.model.mouseMirror) {
value *= -1
}

View File

@@ -14,16 +14,16 @@ export function useSharedMenu() {
const items = options.map((item) => {
return CheckMenuItem.new({
text: item === 100 ? '默认' : `${item}%`,
checked: catStore.scale === item,
checked: catStore.window.scale === item,
action: () => {
catStore.scale = item
catStore.window.scale = item
},
})
})
if (!options.includes(catStore.scale)) {
if (!options.includes(catStore.window.scale)) {
items.unshift(CheckMenuItem.new({
text: `${catStore.scale}%`,
text: `${catStore.window.scale}%`,
checked: true,
enabled: false,
}))
@@ -38,16 +38,16 @@ export function useSharedMenu() {
const items = options.map((item) => {
return CheckMenuItem.new({
text: `${item}%`,
checked: catStore.opacity === item,
checked: catStore.window.opacity === item,
action: () => {
catStore.opacity = item
catStore.window.opacity = item
},
})
})
if (!options.includes(catStore.opacity)) {
if (!options.includes(catStore.window.opacity)) {
items.unshift(CheckMenuItem.new({
text: `${catStore.opacity}%`,
text: `${catStore.window.opacity}%`,
checked: true,
enabled: false,
}))
@@ -64,17 +64,17 @@ export function useSharedMenu() {
action: () => showWindow('preference'),
}),
MenuItem.new({
text: catStore.visible ? '隐藏猫咪' : '显示猫咪',
text: catStore.window.visible ? '隐藏猫咪' : '显示猫咪',
action: () => {
catStore.visible = !catStore.visible
catStore.window.visible = !catStore.window.visible
},
}),
PredefinedMenuItem.new({ item: 'Separator' }),
CheckMenuItem.new({
text: '窗口穿透',
checked: catStore.penetrable,
checked: catStore.window.passThrough,
action: () => {
catStore.penetrable = !catStore.penetrable
catStore.window.passThrough = !catStore.window.passThrough
},
}),
Submenu.new({

View File

@@ -24,11 +24,11 @@ export function useTray() {
const catStore = useCatStore()
const { getSharedMenu } = useSharedMenu()
watch([() => catStore.visible, () => catStore.penetrable], () => {
watch([() => catStore.window.visible, () => catStore.window.passThrough], () => {
updateTrayMenu()
})
watchDebounced([() => catStore.scale, () => catStore.opacity], () => {
watchDebounced([() => catStore.window.scale, () => catStore.window.opacity], () => {
updateTrayMenu()
}, { debounce: 200 })

View File

@@ -78,7 +78,7 @@ watch(() => modelStore.currentModel, async (model) => {
}
}, { deep: true, immediate: true })
watch([() => catStore.scale, modelSize], async ([scale, modelSize]) => {
watch([() => catStore.window.scale, modelSize], async ([scale, modelSize]) => {
if (!modelSize) return
const { width, height } = modelSize
@@ -103,17 +103,17 @@ watch([modelStore.pressedKeys, stickActive], ([keys, stickActive]) => {
handleKeyChange(false, stickActive.right || hasRight)
}, { deep: true })
watch(() => catStore.visible, async (value) => {
watch(() => catStore.window.visible, async (value) => {
value ? showWindow() : hideWindow()
})
watch(() => catStore.penetrable, (value) => {
watch(() => catStore.window.passThrough, (value) => {
appWindow.setIgnoreCursorEvents(value)
}, { immediate: true })
watch(() => catStore.alwaysOnTop, setAlwaysOnTop, { immediate: true })
watch(() => catStore.window.alwaysOnTop, setAlwaysOnTop, { immediate: true })
watch(() => generalStore.taskbarVisibility, setTaskbarVisibility, { immediate: true })
watch(() => generalStore.app.taskbarVisible, setTaskbarVisibility, { immediate: true })
function handleMouseDown() {
appWindow.startDragging()
@@ -137,17 +137,20 @@ function handleMouseMove(event: MouseEvent) {
if (buttons !== 2 || !shiftKey) return
const delta = (movementX + movementY) * 0.5
const nextScale = Math.max(10, Math.min(catStore.scale + delta, 500))
const nextScale = Math.max(10, Math.min(catStore.window.scale + delta, 500))
catStore.scale = round(nextScale)
catStore.window.scale = round(nextScale)
}
</script>
<template>
<div
class="relative size-screen overflow-hidden children:(absolute size-full)"
:class="{ '-scale-x-100': catStore.mirrorMode }"
:style="{ opacity: catStore.opacity / 100 }"
:class="{ '-scale-x-100': catStore.model.mirror }"
:style="{
opacity: catStore.window.opacity / 100,
borderRadius: `${catStore.window.radius}%`,
}"
@contextmenu="handleContextmenu"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"

View File

@@ -14,21 +14,21 @@ const catStore = useCatStore()
description="启用后,模型将水平镜像翻转。"
title="镜像模式"
>
<Switch v-model:checked="catStore.mirrorMode" />
<Switch v-model:checked="catStore.model.mirror" />
</ProListItem>
<ProListItem
description="启用后,每只手只显示最后按下的一个按键。"
title="单键模式"
>
<Switch v-model:checked="catStore.singleMode" />
<Switch v-model:checked="catStore.model.single" />
</ProListItem>
<ProListItem
description="启用后,鼠标将镜像跟随手部移动。"
title="鼠标镜像"
>
<Switch v-model:checked="catStore.mouseMirror" />
<Switch v-model:checked="catStore.model.mouseMirror" />
</ProListItem>
</ProList>
@@ -37,14 +37,14 @@ const catStore = useCatStore()
description="启用后,窗口不影响对其他应用程序的操作。"
title="窗口穿透"
>
<Switch v-model:checked="catStore.penetrable" />
<Switch v-model:checked="catStore.window.passThrough" />
</ProListItem>
<ProListItem
description="启用后,窗口始终显示在其他应用程序上方。"
title="窗口置顶"
>
<Switch v-model:checked="catStore.alwaysOnTop" />
<Switch v-model:checked="catStore.window.alwaysOnTop" />
</ProListItem>
<ProListItem
@@ -52,14 +52,20 @@ const catStore = useCatStore()
title="窗口尺寸"
>
<InputNumber
v-model:value="catStore.scale"
v-model:value="catStore.window.scale"
addon-after="%"
class="w-28"
:min="1"
>
<template #addonAfter>
%
</template>
</InputNumber>
/>
</ProListItem>
<ProListItem title="窗口圆角">
<InputNumber
v-model:value="catStore.window.radius"
addon-after="%"
class="w-28"
:min="0"
/>
</ProListItem>
<ProListItem
@@ -67,7 +73,7 @@ const catStore = useCatStore()
vertical
>
<Slider
v-model:value="catStore.opacity"
v-model:value="catStore.window.opacity"
class="m-0!"
:max="100"
:min="10"

View File

@@ -11,23 +11,23 @@ const appWindow = getCurrentWebviewWindow()
onMounted(() => {
appWindow.onThemeChanged(async ({ payload }) => {
if (generalStore.theme !== 'auto') return
if (generalStore.appearance.theme !== 'auto') return
generalStore.isDark = payload === 'dark'
generalStore.appearance.isDark = payload === 'dark'
})
})
watch(() => generalStore.theme, async (value) => {
watch(() => generalStore.appearance.theme, async (value) => {
let nextTheme = value === 'auto' ? null : value
await appWindow.setTheme(nextTheme)
nextTheme = nextTheme ?? (await appWindow.theme())
generalStore.isDark = nextTheme === 'dark'
generalStore.appearance.isDark = nextTheme === 'dark'
}, { immediate: true })
watch(() => generalStore.isDark, (value) => {
watch(() => generalStore.appearance.isDark, (value) => {
if (value) {
document.documentElement.classList.add('dark')
} else {
@@ -38,7 +38,7 @@ watch(() => generalStore.isDark, (value) => {
<template>
<ProListItem title="主题模式">
<Select v-model:value="generalStore.theme">
<Select v-model:value="generalStore.appearance.theme">
<SelectOption value="auto">
跟随系统
</SelectOption>

View File

@@ -12,7 +12,7 @@ import { useGeneralStore } from '@/stores/general'
const generalStore = useGeneralStore()
watch(() => generalStore.autostart, async (value) => {
watch(() => generalStore.app.autostart, async (value) => {
const enabled = await isEnabled()
if (value && !enabled) {
@@ -30,14 +30,14 @@ watch(() => generalStore.autostart, async (value) => {
<ProList title="应用设置">
<ProListItem title="开机自启动">
<Switch v-model:checked="generalStore.autostart" />
<Switch v-model:checked="generalStore.app.autostart" />
</ProListItem>
<ProListItem
description="启用后,即可通过 OBS Studio 捕获窗口。"
title="显示任务栏图标"
>
<Switch v-model:checked="generalStore.taskbarVisibility" />
<Switch v-model:checked="generalStore.app.taskbarVisible" />
</ProListItem>
</ProList>
@@ -47,7 +47,7 @@ watch(() => generalStore.autostart, async (value) => {
<ProList title="更新设置">
<ProListItem title="自动检查更新">
<Switch v-model:checked="generalStore.autoCheckUpdate" />
<Switch v-model:checked="generalStore.update.autoCheck" />
</ProListItem>
</ProList>
</template>

View File

@@ -13,7 +13,7 @@ const { visibleCat, visiblePreference, mirrorMode, penetrable, alwaysOnTop } = s
const catStore = useCatStore()
useTauriShortcut(visibleCat, () => {
catStore.visible = !catStore.visible
catStore.window.visible = !catStore.window.visible
})
useTauriShortcut(visiblePreference, () => {
@@ -21,15 +21,15 @@ useTauriShortcut(visiblePreference, () => {
})
useTauriShortcut(mirrorMode, () => {
catStore.mirrorMode = !catStore.mirrorMode
catStore.model.mirror = !catStore.model.mirror
})
useTauriShortcut(penetrable, () => {
catStore.penetrable = !catStore.penetrable
catStore.window.passThrough = !catStore.window.passThrough
})
useTauriShortcut(alwaysOnTop, () => {
catStore.alwaysOnTop = !catStore.alwaysOnTop
catStore.window.alwaysOnTop = !catStore.window.alwaysOnTop
})
</script>

View File

@@ -1,29 +1,76 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { reactive, ref } from 'vue'
export interface CatStore {
model: {
mirror: boolean
single: boolean
mouseMirror: boolean
}
window: {
visible: boolean
passThrough: boolean
alwaysOnTop: boolean
scale: number
opacity: number
radius: number
}
}
export const useCatStore = defineStore('cat', () => {
const visible = ref(false)
/* ------------ 废弃字段(后续删除) ------------ */
/** @deprecated 请使用 `model.mirror` */
const mirrorMode = ref(false)
/** @deprecated 请使用 `model.single` */
const singleMode = ref(false)
/** @deprecated 请使用 `model.mouseMirror` */
const mouseMirror = ref(false)
/** @deprecated 请使用 `window.passThrough` */
const penetrable = ref(false)
/** @deprecated 请使用 `window.alwaysOnTop` */
const alwaysOnTop = ref(true)
/** @deprecated 请使用 `window.scale` */
const scale = ref(100)
/** @deprecated 请使用 `window.opacity` */
const opacity = ref(100)
const model = reactive<CatStore['model']>({
mirror: false,
single: false,
mouseMirror: false,
})
const window = reactive<CatStore['window']>({
visible: true,
passThrough: false,
alwaysOnTop: false,
scale: 100,
opacity: 100,
radius: 0,
})
const init = () => {
visible.value = true
model.mirror = mirrorMode.value
model.single = singleMode.value
model.mouseMirror = mouseMirror.value
window.visible = true
window.passThrough = penetrable.value
window.alwaysOnTop = alwaysOnTop.value
window.scale = scale.value
window.opacity = opacity.value
}
return {
visible,
mirrorMode,
singleMode,
mouseMirror,
penetrable,
alwaysOnTop,
scale,
opacity,
model,
window,
init,
}
})

View File

@@ -1,20 +1,68 @@
import type { Theme } from '@tauri-apps/api/window'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { reactive, ref } from 'vue'
export interface GeneralStore {
app: {
autostart: boolean
taskbarVisible: boolean
}
appearance: {
theme: 'auto' | Theme
isDark: boolean
}
update: {
autoCheck: boolean
}
}
export const useGeneralStore = defineStore('general', () => {
/* ------------ 废弃字段(后续删除) ------------ */
/** @deprecated 请使用 `update.autoCheck` */
const autoCheckUpdate = ref(false)
/** @deprecated 请使用 `app.autostart` */
const autostart = ref(false)
/** @deprecated 请使用 `app.taskbarVisible` */
const taskbarVisibility = ref(false)
/** @deprecated 请使用 `appearance.theme` */
const theme = ref<'auto' | Theme>('auto')
/** @deprecated 请使用 `appearance.isDark` */
const isDark = ref(false)
const app = reactive<GeneralStore['app']>({
autostart: false,
taskbarVisible: false,
})
const appearance = reactive<GeneralStore['appearance']>({
theme: 'auto',
isDark: false,
})
const update = reactive<GeneralStore['update']>({
autoCheck: false,
})
const init = () => {
app.autostart = autostart.value
app.taskbarVisible = taskbarVisibility.value
appearance.theme = theme.value
appearance.isDark = isDark.value
update.autoCheck = autoCheckUpdate.value
}
return {
autoCheckUpdate,
autostart,
taskbarVisibility,
theme,
isDark,
app,
appearance,
update,
init,
}
})