diff --git a/src/core/hotkey/schema.ts b/src/core/hotkey/schema.ts index 53012a2..ba98ecc 100644 --- a/src/core/hotkey/schema.ts +++ b/src/core/hotkey/schema.ts @@ -53,6 +53,7 @@ export const hotkeyCommandList = [ 'playPauseAudio', 'seekForward', 'volumeDown', + 'delayTestTap', 'copy', 'cut', @@ -96,6 +97,7 @@ export const getDefaultHotkeyMap = () => seekForward: k('ArrowRight'), volumeUp: k('ArrowUp'), volumeDown: k('ArrowDown'), + delayTestTap: k('g'), undo: k(Ctrl, 'z'), redo: [k(Ctrl, 'y'), k(Ctrl, Shift, 'z')], find: k(Ctrl, 'f'), diff --git a/src/core/pref/persistence.ts b/src/core/pref/persistence.ts index bf2f905..8d5e1ec 100644 --- a/src/core/pref/persistence.ts +++ b/src/core/pref/persistence.ts @@ -1,13 +1,21 @@ import stableStringify from 'json-stable-stringify' import { omit } from 'lodash-es' -import { getDefaultHotkeyMap } from '@core/hotkey' +import { getDefaultHotkeyMap, isHotkeyMatch } from '@core/hotkey' import { reservedHotkeyCommands } from '@core/hotkey/schema' +import type { HotKey } from '@core/hotkey/types' import { type PreferenceSchema, getDefaultPref } from './schema' const STORAGE_KEY = 'amll_editor:preference' -const PREF_VERSION = 1 +const PREF_VERSION = 2 + +const LEGACY_DELAY_TEST_TAP: HotKey.Key = { + code: 'Space', + ctrl: false, + alt: false, + shift: false, +} interface PersistedPref { appVersion: string @@ -20,15 +28,26 @@ export function loadPreference(): PreferenceSchema { const raw = localStorage.getItem(STORAGE_KEY) if (!raw) return getDefaultPref() const parsed = JSON.parse(raw) as PersistedPref - if (parsed.prefVersion > PREF_VERSION) + const prefVersion = parsed.prefVersion ?? 0 + if (prefVersion > PREF_VERSION) console.warn( - `Found preference version ${parsed.prefVersion}, newer than current version ${PREF_VERSION}.`, + `Found preference version ${prefVersion}, newer than current version ${PREF_VERSION}.`, ) - if (parsed.data.hotkeyMap) - parsed.data.hotkeyMap = { - ...getDefaultHotkeyMap(), + if (parsed.data.hotkeyMap) { + const defaultHotkeyMap = getDefaultHotkeyMap() + const hotkeyMap = { + ...defaultHotkeyMap, ...omit(parsed.data.hotkeyMap, reservedHotkeyCommands), } + if ( + prefVersion < PREF_VERSION && + parsed.data.hotkeyMap.delayTestTap?.length === 1 && + isHotkeyMatch(parsed.data.hotkeyMap.delayTestTap[0], LEGACY_DELAY_TEST_TAP) + ) { + hotkeyMap.delayTestTap = defaultHotkeyMap.delayTestTap + } + parsed.data.hotkeyMap = hotkeyMap + } return { ...getDefaultPref(), ...parsed.data, diff --git a/src/core/pref/schema.ts b/src/core/pref/schema.ts index b67d7ac..30d60fd 100644 --- a/src/core/pref/schema.ts +++ b/src/core/pref/schema.ts @@ -15,6 +15,7 @@ export interface PreferenceSchema { macStyleShortcuts: boolean hotkeyMap: HotKey.Map audioSeekingStepMs: number + latencyTestBpm: number // Timing globalLatencyMs: number alwaysIgnoreBackground: boolean @@ -43,6 +44,7 @@ export const getDefaultPref = (): PreferenceSchema => ({ macStyleShortcuts: isAppleDevice(), hotkeyMap: getDefaultHotkeyMap(), audioSeekingStepMs: 5000, + latencyTestBpm: 120, globalLatencyMs: 0, alwaysIgnoreBackground: false, hideLineTiming: true, diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index fb51ef9..97c6d99 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -160,6 +160,7 @@ const en = { timeShift: { groupLabel: 'Time Shift', delayTest: 'Delay Test', + delayTestDesc: 'Open the latency test dialog to measure tap timing against the beep track.', delay: 'Delay', batchTimeShift: 'Batch Shift', batchTimeShiftDesc: @@ -287,6 +288,8 @@ const en = { macStyleShortcutsDesc: 'Display shortcuts using ⌘, ⌥ symbols etc.', audioSeekingStepMs: 'Seek step size', audioSeekingStepMsDesc: 'Time to jump when using hotkeys (ms)', + latencyTestBpm: 'Delay test BPM', + latencyTestBpmDesc: 'Beep rate used by the latency test dialog', swapTranslateRoman: 'Swap translation & romanization panels', swapTranslateRomanDesc: 'Place romanization panel on the left', hideTranslateRoman: 'Hide translation & romanization panels', @@ -613,6 +616,7 @@ const en = { playPauseAudio: 'Play / Pause', seekForward: 'Seek Forward', volumeDown: 'Volume Down', + delayTestTap: 'Delay Test Tap', }, keyNames: { space: 'Space', @@ -741,6 +745,21 @@ const en = { applyToLine: 'Apply to Selected Lines', applyToAll: 'Apply to All', }, + delayTestDialog: { + header: 'Delay Test', + description: 'Press the configured key on every beep to measure input latency.', + tapHint: 'Press', + bpmLabel: 'BPM', + signHint: 'Positive = early, negative = late', + current: 'Current', + fastest: 'Fastest', + slowest: 'Slowest', + noSamples: 'No taps yet', + start: 'Start', + stop: 'Stop', + applyCurrent: 'Apply Current', + applyAverage: 'Apply Average', + }, consoleArt, } as const satisfies Translations diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 0da6a49..5088f16 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -423,6 +423,10 @@ type RootTranslation = { * 延迟测试 */ delayTest: string + /** + * 打开延迟测试对话框,测量按键与蜂鸣之间的延迟。 + */ + delayTestDesc: string /** * 延迟 */ @@ -832,6 +836,14 @@ type RootTranslation = { * 按键快进或快退时跳转的时长 (毫秒) */ audioSeekingStepMsDesc: string + /** + * 延迟测试 BPM + */ + latencyTestBpm: string + /** + * 延迟测试对话框使用的蜂鸣节拍 + */ + latencyTestBpmDesc: string /** * 交换翻译与音译框位置 */ @@ -1768,6 +1780,10 @@ type RootTranslation = { * 减小音量 */ volumeDown: string + /** + * 延迟测试按键 + */ + delayTestTap: string } keyNames: { /** @@ -2098,6 +2114,60 @@ type RootTranslation = { */ applyToAll: string } + delayTestDialog: { + /** + * 延迟测试 + */ + header: string + /** + * 在每次蜂鸣时按下配置的按键,以测量输入延迟。 + */ + description: string + /** + * 按下 + */ + tapHint: string + /** + * BPM + */ + bpmLabel: string + /** + * 正值表示更快,负值表示更慢 + */ + signHint: string + /** + * 当前 + */ + current: string + /** + * 最快 + */ + fastest: string + /** + * 最慢 + */ + slowest: string + /** + * 尚无按键记录 + */ + noSamples: string + /** + * 开始 + */ + start: string + /** + * 结束 + */ + stop: string + /** + * 应用当前值 + */ + applyCurrent: string + /** + * 应用平均值 + */ + applyAverage: string + } /** * ╭──────────╮ ╶╴ ┌─╴ ╶─┐┌─┐ ┌─┐ ┌─────┐ ┌─┐┌─┐ │ ━━━━━━ │ ╱ ╲ │ ╲╱ ││ │ │ │ │ ┌───┘ │ │└─┘┌─┐ @@ -2529,6 +2599,10 @@ export type TranslationFunctions = { * 延迟测试 */ delayTest: () => LocalizedString + /** + * 打开延迟测试对话框,测量按键与蜂鸣之间的延迟。 + */ + delayTestDesc: () => LocalizedString /** * 延迟 */ @@ -2938,6 +3012,14 @@ export type TranslationFunctions = { * 按键快进或快退时跳转的时长 (毫秒) */ audioSeekingStepMsDesc: () => LocalizedString + /** + * 延迟测试 BPM + */ + latencyTestBpm: () => LocalizedString + /** + * 延迟测试对话框使用的蜂鸣节拍 + */ + latencyTestBpmDesc: () => LocalizedString /** * 交换翻译与音译框位置 */ @@ -3870,6 +3952,10 @@ export type TranslationFunctions = { * 减小音量 */ volumeDown: () => LocalizedString + /** + * 延迟测试按键 + */ + delayTestTap: () => LocalizedString } keyNames: { /** @@ -4199,6 +4285,60 @@ export type TranslationFunctions = { */ applyToAll: () => LocalizedString } + delayTestDialog: { + /** + * 延迟测试 + */ + header: () => LocalizedString + /** + * 在每次蜂鸣时按下配置的按键,以测量输入延迟。 + */ + description: () => LocalizedString + /** + * 按下 + */ + tapHint: () => LocalizedString + /** + * BPM + */ + bpmLabel: () => LocalizedString + /** + * 正值表示更快,负值表示更慢 + */ + signHint: () => LocalizedString + /** + * 当前 + */ + current: () => LocalizedString + /** + * 最快 + */ + fastest: () => LocalizedString + /** + * 最慢 + */ + slowest: () => LocalizedString + /** + * 尚无按键记录 + */ + noSamples: () => LocalizedString + /** + * 开始 + */ + start: () => LocalizedString + /** + * 结束 + */ + stop: () => LocalizedString + /** + * 应用当前值 + */ + applyCurrent: () => LocalizedString + /** + * 应用平均值 + */ + applyAverage: () => LocalizedString + } /** * ╭──────────╮ ╶╴ ┌─╴ ╶─┐┌─┐ ┌─┐ ┌─────┐ ┌─┐┌─┐ │ ━━━━━━ │ ╱ ╲ │ ╲╱ ││ │ │ │ │ ┌───┘ │ │└─┘┌─┐ diff --git a/src/i18n/zh-hans/index.ts b/src/i18n/zh-hans/index.ts index 012eb5f..605f211 100644 --- a/src/i18n/zh-hans/index.ts +++ b/src/i18n/zh-hans/index.ts @@ -156,6 +156,7 @@ const zhHans = { timeShift: { groupLabel: '时移', delayTest: '延迟测试', + delayTestDesc: '打开延迟测试对话框,测量按键与蜂鸣之间的延迟。', delay: '延迟', batchTimeShift: '批量时移', batchTimeShiftDesc: '打开批量时移对话框,调整多个音节或行的时间戳。', @@ -278,6 +279,8 @@ const zhHans = { macStyleShortcutsDesc: '使用 ⌘、⌥ 等符号展示组合键', audioSeekingStepMs: '音频按键跳转步长', audioSeekingStepMsDesc: '按键快进或快退时跳转的时长 (毫秒)', + latencyTestBpm: '延迟测试 BPM', + latencyTestBpmDesc: '延迟测试对话框使用的蜂鸣节拍', swapTranslateRoman: '交换翻译与音译框位置', swapTranslateRomanDesc: '在内容视图将音译框置于左侧,并影响查找顺序', hideTranslateRoman: '隐藏翻译音译框', @@ -591,6 +594,7 @@ const zhHans = { playPauseAudio: '播放/暂停音频', seekForward: '快进', volumeDown: '减小音量', + delayTestTap: '延迟测试按键', }, keyNames: { space: '空格', @@ -717,6 +721,21 @@ const zhHans = { applyToLine: '应用到选定行', applyToAll: '应用到全文', }, + delayTestDialog: { + header: '延迟测试', + description: '在每次蜂鸣时按下配置的按键,以测量输入延迟。', + tapHint: '按下', + bpmLabel: 'BPM', + signHint: '正值表示更快,负值表示更慢', + current: '当前', + fastest: '最快', + slowest: '最慢', + noSamples: '尚无按键记录', + start: '开始', + stop: '结束', + applyCurrent: '应用当前值', + applyAverage: '应用平均值', + }, consoleArt, } satisfies BaseTranslation diff --git a/src/ui/dialogs/dialogComponents/DelayTestDialog.vue b/src/ui/dialogs/dialogComponents/DelayTestDialog.vue new file mode 100644 index 0000000..9d0ec9b --- /dev/null +++ b/src/ui/dialogs/dialogComponents/DelayTestDialog.vue @@ -0,0 +1,529 @@ + + + + + + + diff --git a/src/ui/dialogs/dialogComponents/KeyBindingDialog.vue b/src/ui/dialogs/dialogComponents/KeyBindingDialog.vue index 3a55e1d..4c6e60b 100644 --- a/src/ui/dialogs/dialogComponents/KeyBindingDialog.vue +++ b/src/ui/dialogs/dialogComponents/KeyBindingDialog.vue @@ -109,6 +109,7 @@ const groupedCmdList = [ 'playPauseAudio', 'seekForward', 'volumeDown', + 'delayTestTap', ], }, ] as const satisfies { diff --git a/src/ui/dialogs/index.ts b/src/ui/dialogs/index.ts index 61f517e..a96e5fa 100644 --- a/src/ui/dialogs/index.ts +++ b/src/ui/dialogs/index.ts @@ -3,6 +3,7 @@ import type { Component } from 'vue' import type { ValueOf } from '@utils/types' import AboutDialog from './dialogComponents/AboutDialog.vue' +import DelayTestDialog from './dialogComponents/DelayTestDialog.vue' import BatchTimeShiftDialog from './dialogComponents/BatchTimeShiftDialog.vue' import CompatibilityDialog from './dialogComponents/CompatibilityDialog.vue' import FindReplaceDialog from './dialogComponents/FindReplaceDialog.vue' @@ -11,6 +12,7 @@ import FromTextModal from './dialogComponents/FromTextModal.vue' import KeyBindingDialog from './dialogComponents/KeyBindingDialog.vue' export const DialogKey = { + DelayTest: 'delayTest', BatchTimeShift: 'batchTimeShift', FindReplace: 'findReplace', KeyBinding: 'keyBinding', @@ -27,6 +29,7 @@ interface DialogReg { } export const dialogRegs: DialogReg[] = [ + { key: DialogKey.DelayTest, component: DelayTestDialog }, { key: DialogKey.BatchTimeShift, component: BatchTimeShiftDialog }, { key: DialogKey.FindReplace, component: FindReplaceDialog }, { key: DialogKey.KeyBinding, component: KeyBindingDialog }, diff --git a/src/ui/ribbon/groups/TimeShiftGroup.vue b/src/ui/ribbon/groups/TimeShiftGroup.vue index 5505795..4508ef6 100644 --- a/src/ui/ribbon/groups/TimeShiftGroup.vue +++ b/src/ui/ribbon/groups/TimeShiftGroup.vue @@ -4,8 +4,9 @@ icon="mdi mdi-timer-music-outline" :label="tt.delayTest()" size="small" - severity="secondary" - disabled + :severity="runtimeStore.dialogShown.delayTest ? undefined : 'secondary'" + @click="runtimeStore.dialogShown.delayTest = !runtimeStore.dialogShown.delayTest" + v-tooltip="tipDesc(tt.delayTest(), tt.delayTestDesc(), 'delayTestTap')" />