From 0d18cf5671ebd0ffd56e59367aea4326f6bce655 Mon Sep 17 00:00:00 2001 From: zrz <1261482317@qq.com> Date: Tue, 14 Apr 2026 11:13:22 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E5=8A=A0=E5=85=A5=E9=BA=A6?= =?UTF-8?q?=E5=85=8B=E9=A3=8E=E8=AF=AD=E9=9F=B3=E5=93=8D=E5=BA=94=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useDevice.ts | 2 + src/composables/useMicrophone.ts | 224 ++++++++++++++++++ src/locales/en-US.json | 13 +- src/locales/pt-BR.json | 13 +- src/locales/vi-VN.json | 13 +- src/locales/zh-CN.json | 13 +- src/locales/zh-TW.json | 13 +- src/pages/preference/components/cat/index.vue | 53 +++++ src/stores/cat.ts | 8 + 9 files changed, 342 insertions(+), 10 deletions(-) create mode 100644 src/composables/useMicrophone.ts diff --git a/src/composables/useDevice.ts b/src/composables/useDevice.ts index 785f61ba..d88e597a 100644 --- a/src/composables/useDevice.ts +++ b/src/composables/useDevice.ts @@ -4,6 +4,7 @@ import { cursorPosition } from '@tauri-apps/api/window' import { INVOKE_KEY, LISTEN_KEY } from '../constants' +import { useMicrophone } from './useMicrophone' import { useModel } from './useModel' import { useTauriListen } from './useTauriListen' @@ -39,6 +40,7 @@ export function useDevice() { const releaseTimers = new Map() const catStore = useCatStore() const { handlePress, handleRelease, handleMouseChange, handleMouseMove } = useModel() + useMicrophone() const startListening = () => { invoke(INVOKE_KEY.START_DEVICE_LISTENING) diff --git a/src/composables/useMicrophone.ts b/src/composables/useMicrophone.ts new file mode 100644 index 00000000..35aaa021 --- /dev/null +++ b/src/composables/useMicrophone.ts @@ -0,0 +1,224 @@ +import { message } from 'ant-design-vue' +import { onUnmounted, ref, watch } from 'vue' + +import { useCatStore } from '@/stores/cat' +import { useModelStore } from '@/stores/model' +import live2d from '@/utils/live2d' + +export function useMicrophone() { + const catStore = useCatStore() + const modelStore = useModelStore() + + const audioContext = ref(null) + const analyser = ref(null) + const microphone = ref(null) + const mediaStream = ref(null) + const animationFrameId = ref(null) + + const volumeLevel = ref(0) // 当前音量级别 (0-100) + const frequencyData = ref(new Uint8Array(0)) + const timeDomainData = ref(new Uint8Array(0)) + + // 错误处理映射 + const errorMessages: Record = { + NotAllowedError: '麦克风权限被拒绝,请在系统设置中启用', + NotFoundError: '未找到可用的麦克风设备', + NotReadableError: '麦克风设备被其他应用占用', + OverconstrainedError: '无法满足音频约束条件', + SecurityError: '安全限制阻止访问麦克风', + AbortError: '音频设备访问被中止', + } + + // 初始化音频上下文和麦克风 + async function startMicrophone() { + try { + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + throw new Error('当前环境不支持麦克风访问') + } + + // 请求麦克风权限 + mediaStream.value = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: false, + noiseSuppression: false, + autoGainControl: false, + sampleRate: 44100, + }, + }) + + // 创建音频上下文 + audioContext.value = new (window.AudioContext || (window as any).webkitAudioContext)() + analyser.value = audioContext.value.createAnalyser() + + // 配置分析器 + analyser.value.fftSize = 2048 + analyser.value.smoothingTimeConstant = catStore.model.microphoneSmoothing / 100 + + microphone.value = audioContext.value.createMediaStreamSource(mediaStream.value) + microphone.value.connect(analyser.value) + + frequencyData.value = new Uint8Array(analyser.value.frequencyBinCount) + timeDomainData.value = new Uint8Array(analyser.value.fftSize) + + // 开始音频分析循环 + startAudioAnalysis() + } catch (error: any) { + const errorMessage = errorMessages[error.name] || `麦克风访问失败: ${error.message}` + message.error(errorMessage) + stopMicrophone() + // 自动禁用麦克风功能 + catStore.model.microphoneEnabled = false + } + } + + function stopMicrophone() { + if (animationFrameId.value) { + cancelAnimationFrame(animationFrameId.value) + animationFrameId.value = null + } + + if (mediaStream.value) { + mediaStream.value.getTracks().forEach(track => track.stop()) + mediaStream.value = null + } + + if (audioContext.value && audioContext.value.state !== 'closed') { + audioContext.value.close() + } + + audioContext.value = null + analyser.value = null + microphone.value = null + volumeLevel.value = 0 + + // 重置 Live2D 嘴部参数 + resetMouthParameters() + } + + function startAudioAnalysis() { + if (!analyser.value || !audioContext.value) return + + const analyseAudio = () => { + if (!analyser.value || !audioContext.value) { + return + } + + // 更新时间域数据(用于音量计算) + analyser.value.getByteTimeDomainData(timeDomainData.value) + + // 计算音量(RMS) + let sum = 0 + for (let i = 0; i < timeDomainData.value.length; i++) { + const value = (timeDomainData.value[i] - 128) / 128 + sum += value * value + } + const rms = Math.sqrt(sum / timeDomainData.value.length) + + // 应用灵敏度调整 + const sensitivity = catStore.model.microphoneSensitivity / 10 + let adjustedVolume = Math.min(rms * sensitivity * 3, 1) // 3倍增益 + adjustedVolume = adjustedVolume ** 0.7 // 非线性映射,更自然 + + // 转换为百分比 (0-100) + volumeLevel.value = adjustedVolume * 100 + + // 应用阈值过滤 + const threshold = catStore.model.microphoneThreshold / 100 + const effectiveVolume = volumeLevel.value / 100 >= threshold ? volumeLevel.value : 0 + + // 更新Live2D参数 + updateLive2DParameters(effectiveVolume) + + // 继续循环(限制在~60fps) + animationFrameId.value = requestAnimationFrame(analyseAudio) + } + + animationFrameId.value = requestAnimationFrame(analyseAudio) + } + + function resetMouthParameters() { + const mouthOpenRange = live2d.getParameterValueRange('ParamMouthOpenY') + if (mouthOpenRange) { + live2d.setParameterValue('ParamMouthOpenY', mouthOpenRange.min) + } + const mouthFormRange = live2d.getParameterValueRange('ParamMouthForm') + if (mouthFormRange) { + live2d.setParameterValue('ParamMouthForm', (mouthFormRange.min + mouthFormRange.max) / 2) + } + } + + function updateLive2DParameters(volume: number) { + if (!modelStore.currentModel) return + + // 映射到嘴部开合参数 ParamMouthOpenY + const mouthOpenRange = live2d.getParameterValueRange('ParamMouthOpenY') + if (mouthOpenRange) { + const { min, max } = mouthOpenRange + // volume是0-100,映射到参数范围 + const mouthOpenValue = min + (volume / 100) * (max - min) + live2d.setParameterValue('ParamMouthOpenY', mouthOpenValue) + } + + // 映射到嘴型参数 ParamMouthForm(基于频率特征) + if (analyser.value && audioContext.value) { + // 获取频率数据 + analyser.value.getByteFrequencyData(frequencyData.value) + + // 寻找主导频率(85-255Hz人声范围) + const sampleRate = audioContext.value.sampleRate + const frequencyResolution = sampleRate / analyser.value.fftSize + const minIndex = Math.floor(85 / frequencyResolution) + const maxIndex = Math.floor(255 / frequencyResolution) + + let maxValue = 0 + let dominantIndex = minIndex + + for (let i = minIndex; i < maxIndex; i++) { + if (frequencyData.value[i] > maxValue) { + maxValue = frequencyData.value[i] + dominantIndex = i + } + } + + const dominantFrequency = dominantIndex * frequencyResolution + + // 将频率映射到嘴型参数 (85-255Hz 映射到 0-1) + const normalizedFreq = Math.max(0, Math.min(1, (dominantFrequency - 85) / (255 - 85))) + + const mouthFormRange = live2d.getParameterValueRange('ParamMouthForm') + if (mouthFormRange) { + const { min, max } = mouthFormRange + const mouthFormValue = min + normalizedFreq * (max - min) + live2d.setParameterValue('ParamMouthForm', mouthFormValue) + } + } + } + + // 监听配置变化 + watch(() => catStore.model.microphoneEnabled, (enabled) => { + if (enabled) { + startMicrophone() + } else { + stopMicrophone() + } + }) + + // 监听平滑度变化 + watch(() => catStore.model.microphoneSmoothing, (smoothing) => { + if (analyser.value) { + analyser.value.smoothingTimeConstant = smoothing / 100 + } + }) + + // 清理 + onUnmounted(() => { + stopMicrophone() + }) + + return { + startMicrophone, + stopMicrophone, + volumeLevel, + isActive: () => !!mediaStream.value, + } +} diff --git a/src/locales/en-US.json b/src/locales/en-US.json index c5c263fc..bcd981c6 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -23,7 +23,12 @@ "motionSound": "Motion Sound", "autoReleaseDelay": "Auto Release Delay", "hideOnHover": "Hide on Hover", - "position": "Window Position" + "position": "Window Position", + "audioSettings": "Audio Settings", + "microphoneEnabled": "Microphone Input", + "microphoneSensitivity": "Microphone Sensitivity", + "microphoneThreshold": "Activation Threshold", + "microphoneSmoothing": "Smoothing" }, "hints": { "mirrorMode": "When enabled, the model will be mirrored horizontally.", @@ -35,7 +40,11 @@ "windowSize": "Move mouse to window edge, or hold Shift and right-drag to resize.", "autoReleaseDelay": "On Windows, some system keys cannot capture release events and will auto-release after timeout.", "hideOnHover": "When enabled, the window hides when mouse hovers over it.", - "position": "Takes effect after the app starts, or when this parameter, window size, model, or screen resolution changes." + "position": "Takes effect after the app starts, or when this parameter, window size, model, or screen resolution changes.", + "microphoneEnabled": "When enabled, the model will respond to microphone input volume.", + "microphoneSensitivity": "Adjust the sensitivity of microphone input.", + "microphoneThreshold": "Model actions are triggered when volume reaches this threshold.", + "microphoneSmoothing": "Control the smoothness of mouth movements to avoid jitter." }, "options": { "topLeft": "Top Left", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index 9dd1079d..94c62791 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -23,7 +23,12 @@ "motionSound": "Som de Ação", "autoReleaseDelay": "Atraso de Liberação Automática", "hideOnHover": "Ocultar ao Passar o Mouse", - "position": "Posição da Janela" + "position": "Posição da Janela", + "audioSettings": "Configurações de Áudio", + "microphoneEnabled": "Entrada de Microfone", + "microphoneSensitivity": "Sensibilidade do Microfone", + "microphoneThreshold": "Limiar de Ativação", + "microphoneSmoothing": "Suavização" }, "hints": { "mirrorMode": "Quando ativado, o modelo será invertido horizontalmente.", @@ -35,7 +40,11 @@ "windowSize": "Mova o mouse para a borda da janela ou segure Shift e arraste com o botão direito para redimensionar.", "autoReleaseDelay": "Devido ao Windows não capturar eventos de liberação de certas teclas de nível do sistema, elas serão automaticamente tratadas como liberadas após um tempo limite.", "hideOnHover": "Quando ativado, a janela será ocultada quando o mouse passar sobre ela.", - "position": "Entra em vigor após inicializar o aplicativo ou quando este parâmetro, o tamanho da janela, o modelo ou a resolução de tela é alterado." + "position": "Entra em vigor após inicializar o aplicativo ou quando este parâmetro, o tamanho da janela, o modelo ou a resolução de tela é alterado.", + "microphoneEnabled": "Quando ativado, o modelo responderá ao volume de entrada do microfone.", + "microphoneSensitivity": "Ajuste a sensibilidade da entrada do microfone.", + "microphoneThreshold": "Ações do modelo são acionadas quando o volume atinge esse limiar.", + "microphoneSmoothing": "Controle a suavidade dos movimentos da boca para evitar tremores." }, "options": { "topLeft": "Canto Superior Esquerdo", diff --git a/src/locales/vi-VN.json b/src/locales/vi-VN.json index 3ed91ae2..5d97a1dc 100644 --- a/src/locales/vi-VN.json +++ b/src/locales/vi-VN.json @@ -23,7 +23,12 @@ "motionSound": "Âm thanh hành động", "autoReleaseDelay": "Độ trễ tự động nhả phím", "hideOnHover": "Ẩn khi di chuột", - "position": "Vị trí cửa sổ" + "position": "Vị trí cửa sổ", + "audioSettings": "Cài đặt Âm thanh", + "microphoneEnabled": "Đầu vào Microphone", + "microphoneSensitivity": "Độ nhạy Microphone", + "microphoneThreshold": "Ngưỡng kích hoạt", + "microphoneSmoothing": "Độ mượt" }, "hints": { "mirrorMode": "Bật để lật ngang mô hình.", @@ -35,7 +40,11 @@ "windowSize": "Di chuyển chuột đến mép cửa sổ hoặc giữ Shift và kéo chuột phải để thay đổi kích thước.", "autoReleaseDelay": "Do Windows không bắt được sự kiện nhả của một số phím hệ thống, các phím đó sẽ được tự động xem như đã nhả sau khi hết thời gian chờ.", "hideOnHover": "Khi bật, cửa sổ sẽ ẩn khi chuột di chuyển vào.", - "position": "Có hiệu lực sau khi khởi động ứng dụng hoặc khi tham số này, kích thước cửa sổ, mô hình hoặc độ phân giải màn hình thay đổi." + "position": "Có hiệu lực sau khi khởi động ứng dụng hoặc khi tham số này, kích thước cửa sổ, mô hình hoặc độ phân giải màn hình thay đổi.", + "microphoneEnabled": "Khi bật, mô hình sẽ phản ứng với âm lượng đầu vào từ microphone.", + "microphoneSensitivity": "Điều chỉnh độ nhạy của đầu vào microphone.", + "microphoneThreshold": "Hành động của mô hình được kích hoạt khi âm lượng đạt ngưỡng này.", + "microphoneSmoothing": "Kiểm soát độ mượt của chuyển động miệng để tránh giật." }, "options": { "topLeft": "Góc trên cùng bên trái", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 32a56554..4d00f1e8 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -23,7 +23,12 @@ "motionSound": "动作音效", "autoReleaseDelay": "按键自动释放延迟", "hideOnHover": "鼠标移入隐藏", - "position": "窗口位置" + "position": "窗口位置", + "audioSettings": "音频设置", + "microphoneEnabled": "麦克风输入", + "microphoneSensitivity": "麦克风灵敏度", + "microphoneThreshold": "触发阈值", + "microphoneSmoothing": "平滑度" }, "hints": { "mirrorMode": "启用后,模型将水平镜像翻转。", @@ -35,7 +40,11 @@ "windowSize": "将鼠标移至窗口边缘,或按住 Shift 并右键拖动,也可以调整窗口大小。", "autoReleaseDelay": "由于 Windows 下部分系统级按键无法捕获释放事件,超时后将自动视为已释放。", "hideOnHover": "启用后,鼠标悬停在窗口上时,窗口会隐藏。", - "position": "应用启动后,或当此参数、窗口尺寸、模型、电脑分辨率发生变化时生效。" + "position": "应用启动后,或当此参数、窗口尺寸、模型、电脑分辨率发生变化时生效。", + "microphoneEnabled": "启用后,模型会根据麦克风输入音量做出反应", + "microphoneSensitivity": "调整麦克风输入的灵敏度", + "microphoneThreshold": "音量达到此阈值时触发模型动作", + "microphoneSmoothing": "控制嘴部动作的平滑程度,避免抖动" }, "options": { "topLeft": "左上角", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index f4a39623..cd222838 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -23,7 +23,12 @@ "motionSound": "動作音效", "autoReleaseDelay": "按鍵自動釋放延遲", "hideOnHover": "滑鼠游標移入隱藏", - "position": "視窗位置" + "position": "視窗位置", + "audioSettings": "音訊設定", + "microphoneEnabled": "麥克風輸入", + "microphoneSensitivity": "麥克風靈敏度", + "microphoneThreshold": "觸發閾值", + "microphoneSmoothing": "平滑度" }, "hints": { "mirrorMode": "啟用後,模型將水平鏡像翻轉。", @@ -35,7 +40,11 @@ "windowSize": "將滑鼠游標移至視窗邊緣,或按住 Shift 並右鍵拖曳,也可以調整視窗大小。", "autoReleaseDelay": "由於 Windows 下部份系統級按鍵無法擷取釋放事件,超時後將自動視為已釋放。", "hideOnHover": "啟用後,滑鼠游標懸停在視窗上時,視窗會隱藏。", - "position": "應用程式啟動後,或當此參數、視窗尺寸、模型、電腦解析度發生變化時生效。" + "position": "應用程式啟動後,或當此參數、視窗尺寸、模型、電腦解析度發生變化時生效。", + "microphoneEnabled": "啟用後,模型會根據麥克風輸入音量做出反應", + "microphoneSensitivity": "調整麥克風輸入的靈敏度", + "microphoneThreshold": "音量達到此閾值時觸發模型動作", + "microphoneSmoothing": "控制嘴部動作的平滑程度,避免抖動" }, "options": { "topLeft": "左上角", diff --git a/src/pages/preference/components/cat/index.vue b/src/pages/preference/components/cat/index.vue index 6489848d..fd982e68 100644 --- a/src/pages/preference/components/cat/index.vue +++ b/src/pages/preference/components/cat/index.vue @@ -113,4 +113,57 @@ const catStore = useCatStore() /> + + + + + + + + diff --git a/src/stores/cat.ts b/src/stores/cat.ts index 1b1b1791..34b17041 100644 --- a/src/stores/cat.ts +++ b/src/stores/cat.ts @@ -8,6 +8,10 @@ export interface CatStore { mouseMirror: boolean motionSound: boolean autoReleaseDelay: number + microphoneEnabled: boolean + microphoneSensitivity: number + microphoneThreshold: number + microphoneSmoothing: number } window: { visible: boolean @@ -54,6 +58,10 @@ export const useCatStore = defineStore('cat', () => { mouseMirror: false, motionSound: true, autoReleaseDelay: 3, + microphoneEnabled: false, + microphoneSensitivity: 50, + microphoneThreshold: 30, + microphoneSmoothing: 70, }) const window = reactive({ From ab7fe427ba6fa368f581dbc7ee2499c9c682f17a Mon Sep 17 00:00:00 2001 From: zrz <1261482317@qq.com> Date: Tue, 14 Apr 2026 13:27:35 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=BA=A6=E5=85=8B?= =?UTF-8?q?=E9=A3=8E=E6=97=A0=E6=B3=95=E5=9C=A8=E5=90=AF=E5=8A=A8=E6=97=B6?= =?UTF-8?q?=E5=BC=80=E5=A7=8B=E7=9B=91=E5=90=AC=E9=9C=80=E8=A6=81=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E8=AE=BE=E7=BD=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useMicrophone.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/composables/useMicrophone.ts b/src/composables/useMicrophone.ts index 35aaa021..439a99de 100644 --- a/src/composables/useMicrophone.ts +++ b/src/composables/useMicrophone.ts @@ -60,6 +60,11 @@ export function useMicrophone() { frequencyData.value = new Uint8Array(analyser.value.frequencyBinCount) timeDomainData.value = new Uint8Array(analyser.value.fftSize) + // 确保音频上下文处于运行状态 + if (audioContext.value.state !== 'running') { + await audioContext.value.resume() + } + // 开始音频分析循环 startAudioAnalysis() } catch (error: any) { @@ -201,14 +206,14 @@ export function useMicrophone() { } else { stopMicrophone() } - }) + }, { immediate: true }) // 监听平滑度变化 watch(() => catStore.model.microphoneSmoothing, (smoothing) => { if (analyser.value) { analyser.value.smoothingTimeConstant = smoothing / 100 } - }) + }, { immediate: true }) // 清理 onUnmounted(() => { From c3f6716fa113a3ed3dbcdcfa04e6c02c680c2af2 Mon Sep 17 00:00:00 2001 From: zrz <1261482317@qq.com> Date: Tue, 14 Apr 2026 20:14:42 +0800 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=E5=B0=9D=E8=AF=95=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E5=90=88=E7=89=88=E6=9C=AC=E5=A4=B1=E8=AF=AF=EF=BC=8C=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E5=88=B0merge=E4=B9=8B=E5=89=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useDevice.ts | 2 - src/composables/useMicrophone.ts | 9 +++- src/locales/en-US.json | 13 +---- src/locales/pt-BR.json | 13 +---- src/locales/vi-VN.json | 13 +---- src/locales/zh-CN.json | 13 +---- src/locales/zh-TW.json | 13 +---- src/pages/preference/components/cat/index.vue | 53 ------------------- src/stores/cat.ts | 8 --- 9 files changed, 17 insertions(+), 120 deletions(-) diff --git a/src/composables/useDevice.ts b/src/composables/useDevice.ts index d88e597a..785f61ba 100644 --- a/src/composables/useDevice.ts +++ b/src/composables/useDevice.ts @@ -4,7 +4,6 @@ import { cursorPosition } from '@tauri-apps/api/window' import { INVOKE_KEY, LISTEN_KEY } from '../constants' -import { useMicrophone } from './useMicrophone' import { useModel } from './useModel' import { useTauriListen } from './useTauriListen' @@ -40,7 +39,6 @@ export function useDevice() { const releaseTimers = new Map() const catStore = useCatStore() const { handlePress, handleRelease, handleMouseChange, handleMouseMove } = useModel() - useMicrophone() const startListening = () => { invoke(INVOKE_KEY.START_DEVICE_LISTENING) diff --git a/src/composables/useMicrophone.ts b/src/composables/useMicrophone.ts index 35aaa021..439a99de 100644 --- a/src/composables/useMicrophone.ts +++ b/src/composables/useMicrophone.ts @@ -60,6 +60,11 @@ export function useMicrophone() { frequencyData.value = new Uint8Array(analyser.value.frequencyBinCount) timeDomainData.value = new Uint8Array(analyser.value.fftSize) + // 确保音频上下文处于运行状态 + if (audioContext.value.state !== 'running') { + await audioContext.value.resume() + } + // 开始音频分析循环 startAudioAnalysis() } catch (error: any) { @@ -201,14 +206,14 @@ export function useMicrophone() { } else { stopMicrophone() } - }) + }, { immediate: true }) // 监听平滑度变化 watch(() => catStore.model.microphoneSmoothing, (smoothing) => { if (analyser.value) { analyser.value.smoothingTimeConstant = smoothing / 100 } - }) + }, { immediate: true }) // 清理 onUnmounted(() => { diff --git a/src/locales/en-US.json b/src/locales/en-US.json index bcd981c6..c5c263fc 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -23,12 +23,7 @@ "motionSound": "Motion Sound", "autoReleaseDelay": "Auto Release Delay", "hideOnHover": "Hide on Hover", - "position": "Window Position", - "audioSettings": "Audio Settings", - "microphoneEnabled": "Microphone Input", - "microphoneSensitivity": "Microphone Sensitivity", - "microphoneThreshold": "Activation Threshold", - "microphoneSmoothing": "Smoothing" + "position": "Window Position" }, "hints": { "mirrorMode": "When enabled, the model will be mirrored horizontally.", @@ -40,11 +35,7 @@ "windowSize": "Move mouse to window edge, or hold Shift and right-drag to resize.", "autoReleaseDelay": "On Windows, some system keys cannot capture release events and will auto-release after timeout.", "hideOnHover": "When enabled, the window hides when mouse hovers over it.", - "position": "Takes effect after the app starts, or when this parameter, window size, model, or screen resolution changes.", - "microphoneEnabled": "When enabled, the model will respond to microphone input volume.", - "microphoneSensitivity": "Adjust the sensitivity of microphone input.", - "microphoneThreshold": "Model actions are triggered when volume reaches this threshold.", - "microphoneSmoothing": "Control the smoothness of mouth movements to avoid jitter." + "position": "Takes effect after the app starts, or when this parameter, window size, model, or screen resolution changes." }, "options": { "topLeft": "Top Left", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index 94c62791..9dd1079d 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -23,12 +23,7 @@ "motionSound": "Som de Ação", "autoReleaseDelay": "Atraso de Liberação Automática", "hideOnHover": "Ocultar ao Passar o Mouse", - "position": "Posição da Janela", - "audioSettings": "Configurações de Áudio", - "microphoneEnabled": "Entrada de Microfone", - "microphoneSensitivity": "Sensibilidade do Microfone", - "microphoneThreshold": "Limiar de Ativação", - "microphoneSmoothing": "Suavização" + "position": "Posição da Janela" }, "hints": { "mirrorMode": "Quando ativado, o modelo será invertido horizontalmente.", @@ -40,11 +35,7 @@ "windowSize": "Mova o mouse para a borda da janela ou segure Shift e arraste com o botão direito para redimensionar.", "autoReleaseDelay": "Devido ao Windows não capturar eventos de liberação de certas teclas de nível do sistema, elas serão automaticamente tratadas como liberadas após um tempo limite.", "hideOnHover": "Quando ativado, a janela será ocultada quando o mouse passar sobre ela.", - "position": "Entra em vigor após inicializar o aplicativo ou quando este parâmetro, o tamanho da janela, o modelo ou a resolução de tela é alterado.", - "microphoneEnabled": "Quando ativado, o modelo responderá ao volume de entrada do microfone.", - "microphoneSensitivity": "Ajuste a sensibilidade da entrada do microfone.", - "microphoneThreshold": "Ações do modelo são acionadas quando o volume atinge esse limiar.", - "microphoneSmoothing": "Controle a suavidade dos movimentos da boca para evitar tremores." + "position": "Entra em vigor após inicializar o aplicativo ou quando este parâmetro, o tamanho da janela, o modelo ou a resolução de tela é alterado." }, "options": { "topLeft": "Canto Superior Esquerdo", diff --git a/src/locales/vi-VN.json b/src/locales/vi-VN.json index 5d97a1dc..3ed91ae2 100644 --- a/src/locales/vi-VN.json +++ b/src/locales/vi-VN.json @@ -23,12 +23,7 @@ "motionSound": "Âm thanh hành động", "autoReleaseDelay": "Độ trễ tự động nhả phím", "hideOnHover": "Ẩn khi di chuột", - "position": "Vị trí cửa sổ", - "audioSettings": "Cài đặt Âm thanh", - "microphoneEnabled": "Đầu vào Microphone", - "microphoneSensitivity": "Độ nhạy Microphone", - "microphoneThreshold": "Ngưỡng kích hoạt", - "microphoneSmoothing": "Độ mượt" + "position": "Vị trí cửa sổ" }, "hints": { "mirrorMode": "Bật để lật ngang mô hình.", @@ -40,11 +35,7 @@ "windowSize": "Di chuyển chuột đến mép cửa sổ hoặc giữ Shift và kéo chuột phải để thay đổi kích thước.", "autoReleaseDelay": "Do Windows không bắt được sự kiện nhả của một số phím hệ thống, các phím đó sẽ được tự động xem như đã nhả sau khi hết thời gian chờ.", "hideOnHover": "Khi bật, cửa sổ sẽ ẩn khi chuột di chuyển vào.", - "position": "Có hiệu lực sau khi khởi động ứng dụng hoặc khi tham số này, kích thước cửa sổ, mô hình hoặc độ phân giải màn hình thay đổi.", - "microphoneEnabled": "Khi bật, mô hình sẽ phản ứng với âm lượng đầu vào từ microphone.", - "microphoneSensitivity": "Điều chỉnh độ nhạy của đầu vào microphone.", - "microphoneThreshold": "Hành động của mô hình được kích hoạt khi âm lượng đạt ngưỡng này.", - "microphoneSmoothing": "Kiểm soát độ mượt của chuyển động miệng để tránh giật." + "position": "Có hiệu lực sau khi khởi động ứng dụng hoặc khi tham số này, kích thước cửa sổ, mô hình hoặc độ phân giải màn hình thay đổi." }, "options": { "topLeft": "Góc trên cùng bên trái", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 4d00f1e8..32a56554 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -23,12 +23,7 @@ "motionSound": "动作音效", "autoReleaseDelay": "按键自动释放延迟", "hideOnHover": "鼠标移入隐藏", - "position": "窗口位置", - "audioSettings": "音频设置", - "microphoneEnabled": "麦克风输入", - "microphoneSensitivity": "麦克风灵敏度", - "microphoneThreshold": "触发阈值", - "microphoneSmoothing": "平滑度" + "position": "窗口位置" }, "hints": { "mirrorMode": "启用后,模型将水平镜像翻转。", @@ -40,11 +35,7 @@ "windowSize": "将鼠标移至窗口边缘,或按住 Shift 并右键拖动,也可以调整窗口大小。", "autoReleaseDelay": "由于 Windows 下部分系统级按键无法捕获释放事件,超时后将自动视为已释放。", "hideOnHover": "启用后,鼠标悬停在窗口上时,窗口会隐藏。", - "position": "应用启动后,或当此参数、窗口尺寸、模型、电脑分辨率发生变化时生效。", - "microphoneEnabled": "启用后,模型会根据麦克风输入音量做出反应", - "microphoneSensitivity": "调整麦克风输入的灵敏度", - "microphoneThreshold": "音量达到此阈值时触发模型动作", - "microphoneSmoothing": "控制嘴部动作的平滑程度,避免抖动" + "position": "应用启动后,或当此参数、窗口尺寸、模型、电脑分辨率发生变化时生效。" }, "options": { "topLeft": "左上角", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index cd222838..f4a39623 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -23,12 +23,7 @@ "motionSound": "動作音效", "autoReleaseDelay": "按鍵自動釋放延遲", "hideOnHover": "滑鼠游標移入隱藏", - "position": "視窗位置", - "audioSettings": "音訊設定", - "microphoneEnabled": "麥克風輸入", - "microphoneSensitivity": "麥克風靈敏度", - "microphoneThreshold": "觸發閾值", - "microphoneSmoothing": "平滑度" + "position": "視窗位置" }, "hints": { "mirrorMode": "啟用後,模型將水平鏡像翻轉。", @@ -40,11 +35,7 @@ "windowSize": "將滑鼠游標移至視窗邊緣,或按住 Shift 並右鍵拖曳,也可以調整視窗大小。", "autoReleaseDelay": "由於 Windows 下部份系統級按鍵無法擷取釋放事件,超時後將自動視為已釋放。", "hideOnHover": "啟用後,滑鼠游標懸停在視窗上時,視窗會隱藏。", - "position": "應用程式啟動後,或當此參數、視窗尺寸、模型、電腦解析度發生變化時生效。", - "microphoneEnabled": "啟用後,模型會根據麥克風輸入音量做出反應", - "microphoneSensitivity": "調整麥克風輸入的靈敏度", - "microphoneThreshold": "音量達到此閾值時觸發模型動作", - "microphoneSmoothing": "控制嘴部動作的平滑程度,避免抖動" + "position": "應用程式啟動後,或當此參數、視窗尺寸、模型、電腦解析度發生變化時生效。" }, "options": { "topLeft": "左上角", diff --git a/src/pages/preference/components/cat/index.vue b/src/pages/preference/components/cat/index.vue index fd982e68..6489848d 100644 --- a/src/pages/preference/components/cat/index.vue +++ b/src/pages/preference/components/cat/index.vue @@ -113,57 +113,4 @@ const catStore = useCatStore() /> - - - - - - - - diff --git a/src/stores/cat.ts b/src/stores/cat.ts index 34b17041..1b1b1791 100644 --- a/src/stores/cat.ts +++ b/src/stores/cat.ts @@ -8,10 +8,6 @@ export interface CatStore { mouseMirror: boolean motionSound: boolean autoReleaseDelay: number - microphoneEnabled: boolean - microphoneSensitivity: number - microphoneThreshold: number - microphoneSmoothing: number } window: { visible: boolean @@ -58,10 +54,6 @@ export const useCatStore = defineStore('cat', () => { mouseMirror: false, motionSound: true, autoReleaseDelay: 3, - microphoneEnabled: false, - microphoneSensitivity: 50, - microphoneThreshold: 30, - microphoneSmoothing: 70, }) const window = reactive({ From 2f9662a09fbb9cfd4b6b2ff971d98b904445004c Mon Sep 17 00:00:00 2001 From: zrz <1261482317@qq.com> Date: Tue, 14 Apr 2026 20:24:11 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=20=E5=BD=93=E5=89=8D=E5=B7=B2?= =?UTF-8?q?=E6=9C=89=E9=BA=A6=E5=85=8B=E9=A3=8E=E4=BB=A3=E7=A0=81=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useMicrophone.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/composables/useMicrophone.ts b/src/composables/useMicrophone.ts index 35aaa021..439a99de 100644 --- a/src/composables/useMicrophone.ts +++ b/src/composables/useMicrophone.ts @@ -60,6 +60,11 @@ export function useMicrophone() { frequencyData.value = new Uint8Array(analyser.value.frequencyBinCount) timeDomainData.value = new Uint8Array(analyser.value.fftSize) + // 确保音频上下文处于运行状态 + if (audioContext.value.state !== 'running') { + await audioContext.value.resume() + } + // 开始音频分析循环 startAudioAnalysis() } catch (error: any) { @@ -201,14 +206,14 @@ export function useMicrophone() { } else { stopMicrophone() } - }) + }, { immediate: true }) // 监听平滑度变化 watch(() => catStore.model.microphoneSmoothing, (smoothing) => { if (analyser.value) { analyser.value.smoothingTimeConstant = smoothing / 100 } - }) + }, { immediate: true }) // 清理 onUnmounted(() => { From f14b14f0bccaeb742350d710fa41e4cc8b663c55 Mon Sep 17 00:00:00 2001 From: zrz <1261482317@qq.com> Date: Tue, 14 Apr 2026 20:48:15 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=88=B0=E4=B8=BB=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useSharedMenu.ts | 3 +- src/composables/useWindowState.ts | 39 ++++++++++++++++++- src/constants/index.ts | 5 +++ src/locales/en-US.json | 14 ++----- src/locales/pt-BR.json | 14 ++----- src/locales/vi-VN.json | 14 ++----- src/locales/zh-CN.json | 14 ++----- src/locales/zh-TW.json | 14 ++----- src/pages/main/index.vue | 7 ---- src/pages/preference/components/cat/index.vue | 11 ++++-- .../preference/components/shortcut/index.vue | 3 +- src/plugins/window.ts | 4 +- src/stores/cat.ts | 4 +- 13 files changed, 79 insertions(+), 67 deletions(-) diff --git a/src/composables/useSharedMenu.ts b/src/composables/useSharedMenu.ts index 3cdc4327..ed3ef36d 100644 --- a/src/composables/useSharedMenu.ts +++ b/src/composables/useSharedMenu.ts @@ -2,6 +2,7 @@ import { CheckMenuItem, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-app import { range } from 'es-toolkit' import { useI18n } from 'vue-i18n' +import { WINDOW_LABEL } from '@/constants' import { showWindow } from '@/plugins/window' import { useCatStore } from '@/stores/cat' import { isMac } from '@/utils/platform' @@ -63,7 +64,7 @@ export function useSharedMenu() { MenuItem.new({ text: t('composables.useSharedMenu.labels.preference'), accelerator: isMac ? 'Cmd+,' : '', - action: () => showWindow('preference'), + action: () => showWindow(WINDOW_LABEL.PREFERENCE), }), MenuItem.new({ text: catStore.window.visible ? t('composables.useSharedMenu.labels.hideCat') : t('composables.useSharedMenu.labels.showCat'), diff --git a/src/composables/useWindowState.ts b/src/composables/useWindowState.ts index fa4d0aaf..ef2f4d6b 100644 --- a/src/composables/useWindowState.ts +++ b/src/composables/useWindowState.ts @@ -3,10 +3,14 @@ import type { Event } from '@tauri-apps/api/event' import { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi' import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' import { availableMonitors } from '@tauri-apps/api/window' +import { useDebounceFn } from '@vueuse/core' import { isNumber } from 'es-toolkit/compat' -import { onMounted, ref } from 'vue' +import { onMounted, ref, watch } from 'vue' +import { WINDOW_LABEL } from '@/constants' import { useAppStore } from '@/stores/app' +import { useCatStore } from '@/stores/cat' +import { getCursorMonitor } from '@/utils/monitor' export type WindowState = Record | undefined> @@ -15,14 +19,43 @@ const { label } = appWindow export function useWindowState() { const appStore = useAppStore() + const catStore = useCatStore() const isRestored = ref(false) onMounted(() => { appWindow.onMoved(onChange) appWindow.onResized(onChange) + + appWindow.onScaleChanged(clampToMonitor) }) + const clampToMonitor = useDebounceFn(async () => { + if (label !== WINDOW_LABEL.MAIN || !catStore.window.keepInScreen) return + + const monitor = await getCursorMonitor() + + if (!monitor) return + + const { position: monitorPos, size: monitorSize } = monitor + const windowSize = await appWindow.outerSize() + const windowPos = await appWindow.outerPosition() + + const minX = monitorPos.x + const maxX = monitorPos.x + monitorSize.width - windowSize.width + const minY = monitorPos.y + const maxY = monitorPos.y + monitorSize.height - windowSize.height + + const clampedX = Math.max(minX, Math.min(windowPos.x, maxX)) + const clampedY = Math.max(minY, Math.min(windowPos.y, maxY)) + + if (clampedX === windowPos.x && clampedY === windowPos.y) return + + return appWindow.setPosition(new PhysicalPosition(clampedX, clampedY)) + }, 500) + + watch(() => catStore.window.keepInScreen, clampToMonitor) + const onChange = async (event: Event) => { const minimized = await appWindow.isMinimized() @@ -31,6 +64,8 @@ export function useWindowState() { appStore.windowState[label] ??= {} Object.assign(appStore.windowState[label], event.payload) + + clampToMonitor() } const restoreState = async () => { @@ -58,6 +93,8 @@ export function useWindowState() { } isRestored.value = true + + clampToMonitor() } return { diff --git a/src/constants/index.ts b/src/constants/index.ts index 316168f0..2ebadd53 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -26,3 +26,8 @@ export const LANGUAGE = { VI_VN: 'vi-VN', PT_BR: 'pt-BR', } as const + +export const WINDOW_LABEL = { + MAIN: 'main', + PREFERENCE: 'preference', +} as const diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 5eb14c0b..51b7c16a 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -16,14 +16,14 @@ "windowSettings": "Window Settings", "passThrough": "Pass Through", "alwaysOnTop": "Always on Top", + "keepInScreen": "Keep on Screen", "windowSize": "Window Size", "windowRadius": "Window Radius", "opacity": "Opacity", "motionSound": "Motion Sound", "behavior": "Motions and Expressions", "autoReleaseDelay": "Auto Release Delay", - "hideOnHover": "Hide on Hover", - "position": "Window Position" + "hideOnHover": "Hide on Hover" }, "hints": { "mirrorMode": "When enabled, the model will be mirrored horizontally.", @@ -32,16 +32,10 @@ "behavior": "When enabled, motions and expressions can be configured and triggered.", "passThrough": "When enabled, clicks pass through the window without affecting it.", "alwaysOnTop": "When enabled, the window stays above all other windows.", + "keepInScreen": "When enabled, the window automatically stays within the screen boundaries.", "windowSize": "Move mouse to window edge, or hold Shift and right-drag to resize.", "autoReleaseDelay": "On Windows, some system keys cannot capture release events and will auto-release after timeout.", - "hideOnHover": "When enabled, the window hides when mouse hovers over it.", - "position": "Takes effect after the app starts, or when this parameter, window size, model, or screen resolution changes." - }, - "options": { - "topLeft": "Top Left", - "topRight": "Top Right", - "bottomLeft": "Bottom Left", - "bottomRight": "Bottom Right" + "hideOnHover": "When enabled, the window hides when mouse hovers over it." } }, "general": { diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index f16f4bcf..abaa94f6 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -16,14 +16,14 @@ "windowSettings": "Configurações da Janela", "passThrough": "Janela Transparente", "alwaysOnTop": "Sempre no Topo", + "keepInScreen": "Manter na Tela", "windowSize": "Tamanho da Janela", "windowRadius": "Raio da Janela", "opacity": "Opacidade", "motionSound": "Som de Ação", "behavior": "Movimentos e Expressões", "autoReleaseDelay": "Atraso de Liberação Automática", - "hideOnHover": "Ocultar ao Passar o Mouse", - "position": "Posição da Janela" + "hideOnHover": "Ocultar ao Passar o Mouse" }, "hints": { "mirrorMode": "Quando ativado, o modelo será invertido horizontalmente.", @@ -32,16 +32,10 @@ "behavior": "Quando ativado, movimentos e expressões podem ser configurados e acionados.", "passThrough": "Quando ativado, a janela não afetará operações em outros aplicativos.", "alwaysOnTop": "Quando ativado, a janela sempre ficará acima de outros aplicativos.", + "keepInScreen": "Quando ativado, a janela permanece automaticamente dentro dos limites da tela.", "windowSize": "Mova o mouse para a borda da janela ou segure Shift e arraste com o botão direito para redimensionar.", "autoReleaseDelay": "Devido ao Windows não capturar eventos de liberação de certas teclas de nível do sistema, elas serão automaticamente tratadas como liberadas após um tempo limite.", - "hideOnHover": "Quando ativado, a janela será ocultada quando o mouse passar sobre ela.", - "position": "Entra em vigor após inicializar o aplicativo ou quando este parâmetro, o tamanho da janela, o modelo ou a resolução de tela é alterado." - }, - "options": { - "topLeft": "Canto Superior Esquerdo", - "topRight": "Canto Superior Direito", - "bottomLeft": "Canto Inferior Esquerdo", - "bottomRight": "Canto Inferior Direito" + "hideOnHover": "Quando ativado, a janela será ocultada quando o mouse passar sobre ela." } }, "general": { diff --git a/src/locales/vi-VN.json b/src/locales/vi-VN.json index 90e15ce8..3211827a 100644 --- a/src/locales/vi-VN.json +++ b/src/locales/vi-VN.json @@ -16,14 +16,14 @@ "windowSettings": "Cài đặt Cửa sổ", "passThrough": "Click xuyên", "alwaysOnTop": "Luôn trên cùng", + "keepInScreen": "Giữ trong màn hình", "windowSize": "Kích thước", "windowRadius": "Độ bo tròn cửa sổ", "opacity": "Độ mờ", "motionSound": "Âm thanh hành động", "behavior": "Hành động và Biểu cảm", "autoReleaseDelay": "Độ trễ tự động nhả phím", - "hideOnHover": "Ẩn khi di chuột", - "position": "Vị trí cửa sổ" + "hideOnHover": "Ẩn khi di chuột" }, "hints": { "mirrorMode": "Bật để lật ngang mô hình.", @@ -32,16 +32,10 @@ "behavior": "Khi bật, các hành động và biểu cảm có thể được cấu hình và kích hoạt.", "passThrough": "Bật để cửa sổ không ảnh hưởng đến thao tác trên ứng dụng khác.", "alwaysOnTop": "Bật để cửa sổ luôn nằm trên ứng dụng khác.", + "keepInScreen": "Khi bật, cửa sổ sẽ tự động giữ trong ranh giới màn hình.", "windowSize": "Di chuyển chuột đến mép cửa sổ hoặc giữ Shift và kéo chuột phải để thay đổi kích thước.", "autoReleaseDelay": "Do Windows không bắt được sự kiện nhả của một số phím hệ thống, các phím đó sẽ được tự động xem như đã nhả sau khi hết thời gian chờ.", - "hideOnHover": "Khi bật, cửa sổ sẽ ẩn khi chuột di chuyển vào.", - "position": "Có hiệu lực sau khi khởi động ứng dụng hoặc khi tham số này, kích thước cửa sổ, mô hình hoặc độ phân giải màn hình thay đổi." - }, - "options": { - "topLeft": "Góc trên cùng bên trái", - "topRight": "Góc trên cùng bên phải", - "bottomLeft": "Góc dưới cùng bên trái", - "bottomRight": "Góc dưới cùng bên phải" + "hideOnHover": "Khi bật, cửa sổ sẽ ẩn khi chuột di chuyển vào." } }, "general": { diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 8e93293d..bea8316b 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -16,14 +16,14 @@ "windowSettings": "窗口设置", "passThrough": "窗口穿透", "alwaysOnTop": "窗口置顶", + "keepInScreen": "保持在屏幕内", "windowSize": "窗口尺寸", "windowRadius": "窗口圆角", "opacity": "不透明度", "motionSound": "动作音效", "behavior": "动作与表情", "autoReleaseDelay": "按键自动释放延迟", - "hideOnHover": "鼠标移入隐藏", - "position": "窗口位置" + "hideOnHover": "鼠标移入隐藏" }, "hints": { "mirrorMode": "启用后,模型将水平镜像翻转。", @@ -32,16 +32,10 @@ "behavior": "启用后,可以配置和触发模型的动作与表情。", "passThrough": "启用后,窗口不影响对其他应用程序的操作。", "alwaysOnTop": "启用后,窗口始终显示在其他应用程序上方。", + "keepInScreen": "启用后,窗口会自动调整位置,防止超出屏幕边界。", "windowSize": "将鼠标移至窗口边缘,或按住 Shift 并右键拖动,也可以调整窗口大小。", "autoReleaseDelay": "由于 Windows 下部分系统级按键无法捕获释放事件,超时后将自动视为已释放。", - "hideOnHover": "启用后,鼠标悬停在窗口上时,窗口会隐藏。", - "position": "应用启动后,或当此参数、窗口尺寸、模型、电脑分辨率发生变化时生效。" - }, - "options": { - "topLeft": "左上角", - "topRight": "右上角", - "bottomLeft": "左下角", - "bottomRight": "右下角" + "hideOnHover": "启用后,鼠标悬停在窗口上时,窗口会隐藏。" } }, "general": { diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index 2f7e78fe..76129f7c 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -16,14 +16,14 @@ "windowSettings": "視窗設定", "passThrough": "視窗穿透", "alwaysOnTop": "視窗置頂", + "keepInScreen": "保持在螢幕內", "windowSize": "視窗尺寸", "windowRadius": "視窗圓角", "opacity": "不透明度", "motionSound": "動作音效", "behavior": "動作與表情", "autoReleaseDelay": "按鍵自動釋放延遲", - "hideOnHover": "滑鼠游標移入隱藏", - "position": "視窗位置" + "hideOnHover": "滑鼠游標移入隱藏" }, "hints": { "mirrorMode": "啟用後,模型將水平鏡像翻轉。", @@ -32,16 +32,10 @@ "behavior": "啟用後,可以配置和觸發模型的動作與表情。", "passThrough": "啟用後,視窗不影響對其他應用程式的操作。", "alwaysOnTop": "啟用後,視窗始終顯示在其他應用程式上方。", + "keepInScreen": "啟用後,視窗會自動調整位置,防止超出螢幕邊界。", "windowSize": "將滑鼠游標移至視窗邊緣,或按住 Shift 並右鍵拖曳,也可以調整視窗大小。", "autoReleaseDelay": "由於 Windows 下部份系統級按鍵無法擷取釋放事件,超時後將自動視為已釋放。", - "hideOnHover": "啟用後,滑鼠游標懸停在視窗上時,視窗會隱藏。", - "position": "應用程式啟動後,或當此參數、視窗尺寸、模型、電腦解析度發生變化時生效。" - }, - "options": { - "topLeft": "左上角", - "topRight": "右上角", - "bottomLeft": "左下角", - "bottomRight": "右下角" + "hideOnHover": "啟用後,滑鼠游標懸停在視窗上時,視窗會隱藏。" } }, "general": { diff --git a/src/pages/main/index.vue b/src/pages/main/index.vue index b38f01f0..590b168f 100644 --- a/src/pages/main/index.vue +++ b/src/pages/main/index.vue @@ -17,7 +17,6 @@ import { useGamepad } from '@/composables/useGamepad' import { useModel } from '@/composables/useModel' import { useSharedMenu } from '@/composables/useSharedMenu' import { useTauriListen } from '@/composables/useTauriListen' -import { useWindowPosition } from '@/composables/useWindowPosition' import { LISTEN_KEY } from '@/constants' import { hideWindow, setAlwaysOnTop, setTaskbarVisibility, showWindow } from '@/plugins/window' import { useCatStore } from '@/stores/cat' @@ -38,7 +37,6 @@ const generalStore = useGeneralStore() const resizing = ref(false) const backgroundImagePath = ref() const { stickActive } = useGamepad() -const { isMounted, setWindowPosition } = useWindowPosition() onMounted(startListening) @@ -47,8 +45,6 @@ onUnmounted(handleDestroy) const debouncedResize = useDebounceFn(async () => { await handleResize() - await setWindowPosition() - resizing.value = false }, 100) @@ -85,8 +81,6 @@ watch(() => modelStore.currentModel, async (model) => { modelStore.supportKeys[fileName] = join(groupDir, file.name) } } - - setWindowPosition() }, { deep: true, immediate: true }) watch([() => catStore.window.scale, modelSize], async ([scale, modelSize]) => { @@ -168,7 +162,6 @@ function handleMouseMove(event: MouseEvent) { diff --git a/src/stores/cat.ts b/src/stores/cat.ts index c7865f0e..13374c4a 100644 --- a/src/stores/cat.ts +++ b/src/stores/cat.ts @@ -8,6 +8,10 @@ export interface CatStore { motionSound: boolean behavior: boolean autoReleaseDelay: number + microphoneEnabled: boolean + microphoneSensitivity: number + microphoneThreshold: number + microphoneSmoothing: number } window: { visible: boolean @@ -51,6 +55,10 @@ export const useCatStore = defineStore('cat', () => { motionSound: true, behavior: true, autoReleaseDelay: 3, + microphoneEnabled: false, + microphoneSensitivity: 50, + microphoneThreshold: 30, + microphoneSmoothing: 70, }) const window = reactive({ From bfa89131a0aa246d73d6ca40b635a7f5c7c8f1d6 Mon Sep 17 00:00:00 2001 From: zrz <1261482317@qq.com> Date: Tue, 14 Apr 2026 21:03:47 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix:=E5=88=A0=E9=99=A4=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=AF=BC=E8=87=B4=E7=9A=84=E5=A4=9A=E4=BD=99?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useWindowPosition.ts | 47 ------------------- .../cat/components/position/index.vue | 30 ------------ 2 files changed, 77 deletions(-) delete mode 100644 src/composables/useWindowPosition.ts delete mode 100644 src/pages/preference/components/cat/components/position/index.vue diff --git a/src/composables/useWindowPosition.ts b/src/composables/useWindowPosition.ts deleted file mode 100644 index 521d990a..00000000 --- a/src/composables/useWindowPosition.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { PhysicalPosition } from '@tauri-apps/api/dpi' -import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' -import { onMounted, ref, watch } from 'vue' - -import { useCatStore } from '@/stores/cat' -import { getCursorMonitor } from '@/utils/monitor' - -const appWindow = getCurrentWebviewWindow() - -export function useWindowPosition() { - const catStore = useCatStore() - const isMounted = ref(false) - - const setWindowPosition = async () => { - const monitor = await getCursorMonitor() - - if (!monitor) return - - const windowSize = await appWindow.outerSize() - - switch (catStore.window.position) { - case 'topLeft': - return appWindow.setPosition(new PhysicalPosition(0, 0)) - case 'topRight': - return appWindow.setPosition(new PhysicalPosition(monitor.size.width - windowSize.width, 0)) - case 'bottomLeft': - return appWindow.setPosition(new PhysicalPosition(0, monitor.size.height - windowSize.height)) - default: - return appWindow.setPosition(new PhysicalPosition(monitor.size.width - windowSize.width, monitor.size.height - windowSize.height)) - } - } - - onMounted(async () => { - await setWindowPosition() - - isMounted.value = true - - appWindow.onScaleChanged(setWindowPosition) - }) - - watch(() => catStore.window.position, setWindowPosition) - - return { - isMounted, - setWindowPosition, - } -} diff --git a/src/pages/preference/components/cat/components/position/index.vue b/src/pages/preference/components/cat/components/position/index.vue deleted file mode 100644 index 050911a4..00000000 --- a/src/pages/preference/components/cat/components/position/index.vue +++ /dev/null @@ -1,30 +0,0 @@ - - -