From 24f551a39eae849d891da26cc91f90d354e3a2db Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 12 Jul 2026 02:44:05 +0800
Subject: [PATCH] chore(mas): refresh codebase memory after split
---
C1.source/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte | 300 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 291 insertions(+), 9 deletions(-)
diff --git a/C1.source/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte b/C1.source/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte
index 7b48d66..5cbd49c 100755
--- a/C1.source/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte
+++ b/C1.source/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte
@@ -1,9 +1,9 @@
<script lang="ts">
- import { onDestroy } from 'svelte'
- import { Call } from '@wailsio/runtime'
+ import { onDestroy, onMount } from 'svelte'
+ import { Call, Events } from '@wailsio/runtime'
import ToggleSwitch from '../shared/ToggleSwitch.svelte'
import { t } from '../../lib/i18n'
- import { autoHide, soundFeedback, copyToClipboard, startAtLogin, hotkeyVK, hotkeyMode } from '../../lib/stores/config'
+ import { autoHide, soundFeedback, copyToClipboard, autoPasteExperiment, startAtLogin, hotkeyVK, hotkeyMode } from '../../lib/stores/config'
import { deviceName, engineStatus, engineHardwareInfo } from '../../lib/stores/app'
import { hotkeyName } from '../../lib/stores/indicator'
@@ -12,22 +12,45 @@
isDefault: boolean
}
+ interface TextOutputResult {
+ ok?: boolean
+ needsPermission?: boolean
+ directInserted?: boolean
+ fallbackAvailable?: boolean
+ createdAt?: string
+ }
+
+ interface ClipboardFallbackResult {
+ ok?: boolean
+ prepared?: boolean
+ restored?: boolean
+ token?: string
+ }
+
let autoHideVal = $state(true)
let soundFeedbackVal = $state(true)
let copyToClipboardVal = $state(true)
+ let autoPasteExperimentVal = $state(false)
let startAtLoginVal = $state(false)
let hideDockIconVal = $state(false)
let settingsLoaded = $state(false)
let device = $state('Default')
let status = $state('loading')
let hwInfo = $state('')
- let currentKeyName = $state('Ctrl')
+ let currentKeyName = $state('')
let hotkeyModeVal: 'tap' | 'hold' = $state('hold')
let isRecording = $state(false)
let hintText = $state('')
let devices = $state<InputDevice[]>([])
let showDeviceDropdown = $state(false)
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null
+ let textOutput = $state<TextOutputResult | null>(null)
+ let outputStatusText = $state('')
+ let outputActionText = $state('')
+ let fallbackToken = $state('')
+ let fallbackPrepared = $state(false)
+ let fallbackBusy = $state(false)
+ let lastOutputCreatedAt = $state('')
const unsub1 = autoHide.subscribe(v => { autoHideVal = v })
const unsub2 = startAtLogin.subscribe(v => { startAtLoginVal = v })
@@ -38,9 +61,10 @@
const unsub7 = soundFeedback.subscribe(v => { soundFeedbackVal = v })
const unsub8 = copyToClipboard.subscribe(v => { copyToClipboardVal = v })
const unsub9 = hotkeyMode.subscribe(v => { hotkeyModeVal = v })
+ const unsub10 = autoPasteExperiment.subscribe(v => { autoPasteExperimentVal = v })
onDestroy(() => {
- unsub1(); unsub2(); unsub3(); unsub4(); unsub5(); unsub6(); unsub7(); unsub8(); unsub9()
+ unsub1(); unsub2(); unsub3(); unsub4(); unsub5(); unsub6(); unsub7(); unsub8(); unsub9(); unsub10()
clearHotkeyCapture()
})
@@ -78,10 +102,26 @@
copyToClipboardVal = copy
} catch {}
try {
+ const autoPaste: any = await Call.ByName('voicesnap/services.ConfigService.GetAutoPasteExperiment')
+ autoPasteExperiment.set(!!autoPaste)
+ autoPasteExperimentVal = !!autoPaste
+ } catch {}
+ try {
const mode: any = await Call.ByName('voicesnap/services.ConfigService.GetHotkeyMode')
const normalized = mode === 'tap' ? 'tap' : 'hold'
hotkeyMode.set(normalized)
hotkeyModeVal = normalized
+ } catch {}
+ try {
+ const vk: any = await Call.ByName('voicesnap/services.HotkeyService.GetCurrentHotkey')
+ if (typeof vk === 'number') {
+ hotkeyVK.set(vk)
+ }
+ const name: any = await Call.ByName('voicesnap/services.HotkeyService.GetHotkeyName')
+ if (typeof name === 'string') {
+ hotkeyName.set(name)
+ currentKeyName = name
+ }
} catch {}
try {
const hideDock: any = await Call.ByName('voicesnap/services.ConfigService.GetHideDockIcon')
@@ -90,6 +130,20 @@
settingsLoaded = true
}
loadSettings()
+
+ onMount(() => {
+ const offTextOutputUpdated = Events.On('text-output:updated', () => {
+ void refreshTextOutput()
+ })
+ const pollTextOutput = window.setInterval(() => {
+ void refreshTextOutput(true)
+ }, 1000)
+ void refreshTextOutput()
+ return () => {
+ window.clearInterval(pollTextOutput)
+ offTextOutputUpdated()
+ }
+ })
async function onAutoHideChange(checked: boolean) {
autoHide.set(checked)
@@ -109,6 +163,13 @@
copyToClipboard.set(checked)
try {
await Call.ByName('voicesnap/services.ConfigService.SetCopyToClipboard', checked)
+ } catch {}
+ }
+
+ async function onAutoPasteExperimentChange(checked: boolean) {
+ autoPasteExperiment.set(checked)
+ try {
+ await Call.ByName('voicesnap/services.ConfigService.SetAutoPasteExperiment', checked)
} catch {}
}
@@ -132,6 +193,101 @@
try {
await Call.ByName('voicesnap/services.ConfigService.SetHideDockIcon', checked)
} catch {}
+ }
+
+ async function refreshTextOutput(preserveActionText = false) {
+ try {
+ const result: TextOutputResult = await Call.ByName('voicesnap/services.TextOutputService.GetLastResult')
+ const nextOutputCreatedAt = outputResultKey(result)
+ const isNewOutput = !!nextOutputCreatedAt && !!lastOutputCreatedAt && nextOutputCreatedAt !== lastOutputCreatedAt
+ if (isNewOutput) {
+ fallbackToken = ''
+ fallbackPrepared = false
+ outputActionText = ''
+ }
+ if (nextOutputCreatedAt) {
+ lastOutputCreatedAt = nextOutputCreatedAt
+ }
+ textOutput = result || null
+ outputStatusText = textOutputStatus(result)
+ if (!isNewOutput && !preserveActionText && !fallbackPrepared) {
+ outputActionText = ''
+ }
+ } catch {
+ textOutput = null
+ outputStatusText = t('settings.textOutputUnavailable')
+ }
+ }
+
+ async function prepareClipboardFallback() {
+ if (fallbackPrepared) return
+ fallbackBusy = true
+ try {
+ const result: ClipboardFallbackResult = await Call.ByName('voicesnap/services.TextOutputService.PrepareLastClipboardFallback')
+ if (result?.ok && result?.prepared && result?.token) {
+ fallbackToken = result.token
+ fallbackPrepared = true
+ outputActionText = t('settings.textOutputFallbackPrepared')
+ } else {
+ outputActionText = t('settings.textOutputFallbackUnavailable')
+ }
+ } catch {
+ outputActionText = t('settings.textOutputFallbackUnavailable')
+ } finally {
+ fallbackBusy = false
+ void refreshTextOutput()
+ }
+ }
+
+ async function restoreClipboardFallback() {
+ if (!fallbackToken) return
+ fallbackBusy = true
+ try {
+ const result: ClipboardFallbackResult = await Call.ByName('voicesnap/services.TextOutputService.RestoreClipboardFallback', fallbackToken)
+ fallbackToken = ''
+ fallbackPrepared = false
+ if (result?.ok && result?.restored) {
+ outputActionText = t('settings.textOutputClipboardRestored')
+ } else {
+ outputActionText = t('settings.textOutputClipboardKept')
+ }
+ } catch {
+ outputActionText = t('settings.textOutputClipboardRestoreFailed')
+ } finally {
+ fallbackBusy = false
+ void refreshTextOutput(true)
+ }
+ }
+
+ function textOutputStatus(result: TextOutputResult | null | undefined): string {
+ if (!hasOutputResult(result)) {
+ return t('settings.textOutputNoResult')
+ }
+ if (result.ok && result.directInserted) {
+ return t('settings.textOutputInserted')
+ }
+ if (result.ok) {
+ return t('settings.textOutputCompleted')
+ }
+ if (result.needsPermission) {
+ return t('settings.textOutputNeedsPermission')
+ }
+ if (result.fallbackAvailable) {
+ return t('settings.textOutputFallbackAvailable')
+ }
+ return t('settings.textOutputFailed')
+ }
+
+ function hasOutputResult(result: TextOutputResult | null | undefined): result is TextOutputResult {
+ if (!result) return false
+ if (result.ok || result.needsPermission || result.directInserted || result.fallbackAvailable) return true
+ return !!outputResultKey(result)
+ }
+
+ function outputResultKey(result: TextOutputResult | null | undefined): string {
+ const createdAt = result?.createdAt || ''
+ if (!createdAt || createdAt.startsWith('0001-')) return ''
+ return createdAt
}
async function toggleDeviceDropdown() {
@@ -179,6 +335,12 @@
e.preventDefault()
e.stopPropagation()
+ if (isHotkeyCaptureCancel(e)) {
+ hintText = t('status.cancelled')
+ clearHotkeyCapture()
+ return
+ }
+
const vk = mapKeyToVK(e)
if (vk > 0) {
isRecording = false
@@ -189,7 +351,7 @@
try {
await Call.ByName('voicesnap/services.HotkeyService.SetHotkey', vk)
const name: any = await Call.ByName('voicesnap/services.HotkeyService.GetKeyName', vk)
- if (name) {
+ if (typeof name === 'string') {
hotkeyName.set(name)
currentKeyName = name
}
@@ -205,6 +367,10 @@
activeKeyHandler = onKey
document.addEventListener('keydown', onKey)
+ }
+
+ function isHotkeyCaptureCancel(e: KeyboardEvent): boolean {
+ return e.key === 'Escape' || e.key === 'Esc'
}
function clearHotkeyCapture() {
@@ -256,14 +422,18 @@
<div class="hotkey-display">
<div class="hotkey-info">
<span class="hotkey-label">{t('home.hotkeyLabel')}</span>
- <div class="hotkey-key" class:recording={isRecording}>
- {isRecording ? '...' : currentKeyName}
+ <div class="hotkey-key" class:recording={isRecording} class:unset={!isRecording && !currentKeyName}>
+ {isRecording ? '...' : (currentKeyName || t('hotkeys.unset'))}
</div>
</div>
<button class="change-btn" onclick={startRecordingHotkey} disabled={isRecording}>
- {t('hotkeys.change')}
+ {currentKeyName ? t('hotkeys.change') : t('hotkeys.set')}
</button>
</div>
+
+ {#if !currentKeyName && !isRecording}
+ <p class="hint">{t('hotkeys.unsetDesc')}</p>
+ {/if}
{#if hintText}
<p class="hint" class:recording={isRecording}>{hintText}</p>
@@ -376,6 +546,48 @@
<div class="setting-row">
<div class="setting-info">
+ <span class="setting-label">{t('settings.autoPasteExperiment')}</span>
+ <span class="setting-desc">{t('settings.autoPasteExperimentDesc')}</span>
+ </div>
+ <ToggleSwitch checked={autoPasteExperimentVal} onchange={onAutoPasteExperimentChange} />
+ </div>
+
+ <div class="divider"></div>
+
+ <div class="text-output-panel">
+ <div class="setting-info">
+ <span class="setting-label">{t('settings.textOutputTitle')}</span>
+ <span class="setting-desc">{t('settings.textOutputDesc')}</span>
+ </div>
+ <div class="text-output-status" class:success={!!textOutput?.ok} class:warn={!!textOutput && !textOutput.ok}>
+ {outputStatusText || t('settings.textOutputNoResult')}
+ </div>
+ {#if outputActionText}
+ <div class="fallback-message">{outputActionText}</div>
+ {/if}
+ <div class="fallback-actions">
+ <button class="secondary-btn" onclick={() => refreshTextOutput()} disabled={fallbackBusy}>
+ {t('settings.textOutputRefresh')}
+ </button>
+ <button
+ class="primary-btn"
+ onclick={prepareClipboardFallback}
+ disabled={fallbackBusy || fallbackPrepared || !textOutput?.fallbackAvailable}
+ >
+ {t('settings.textOutputUseClipboard')}
+ </button>
+ {#if fallbackPrepared}
+ <button class="secondary-btn" onclick={restoreClipboardFallback} disabled={fallbackBusy || !fallbackToken}>
+ {t('settings.textOutputRestoreClipboard')}
+ </button>
+ {/if}
+ </div>
+ </div>
+
+ <div class="divider"></div>
+
+ <div class="setting-row">
+ <div class="setting-info">
<span class="setting-label">{t('settings.autoHide')}</span>
<span class="setting-desc">{t('settings.autoHideDesc')}</span>
</div>
@@ -465,6 +677,12 @@
animation: pulse 1s infinite;
}
+ .hotkey-key.unset {
+ color: var(--color-secondary-label);
+ font-size: var(--font-size-base);
+ font-weight: 500;
+ }
+
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
@@ -537,6 +755,70 @@
color: var(--color-tertiary-label);
}
+ .text-output-panel {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-xs) 0;
+ }
+
+ .text-output-status {
+ display: inline-flex;
+ align-self: flex-start;
+ padding: 4px 10px;
+ border-radius: var(--radius-sm);
+ background: var(--color-bg-secondary);
+ color: var(--color-secondary-label);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ }
+
+ .text-output-status.success {
+ color: var(--color-green);
+ }
+
+ .text-output-status.warn {
+ color: var(--color-orange);
+ }
+
+ .fallback-message {
+ color: var(--color-secondary-label);
+ font-size: var(--font-size-sm);
+ line-height: 1.4;
+ }
+
+ .fallback-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing-sm);
+ }
+
+ .primary-btn,
+ .secondary-btn {
+ border: none;
+ border-radius: var(--radius-sm);
+ padding: 7px 12px;
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ cursor: pointer;
+ }
+
+ .primary-btn {
+ background: var(--color-blue);
+ color: white;
+ }
+
+ .secondary-btn {
+ background: var(--color-bg-secondary);
+ color: var(--color-label);
+ }
+
+ .primary-btn:disabled,
+ .secondary-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
.segmented {
display: inline-grid;
grid-template-columns: 1fr 1fr;
--
Gitblit v1.9.3