mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-23 23:57:22 +08:00
feat: 快捷键整体重构优化
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,6 @@ import pinia from '@/store';
|
||||
|
||||
import App from './App.vue';
|
||||
import directives from './directive';
|
||||
import { initAppShortcuts } from './utils/appShortcuts';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
@@ -23,6 +22,3 @@ app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(i18n as any);
|
||||
app.mount('#app');
|
||||
|
||||
// 初始化应用内快捷键
|
||||
initAppShortcuts();
|
||||
|
||||
+108
-142
@@ -3,68 +3,61 @@ import { onMounted, onUnmounted } from 'vue';
|
||||
import i18n from '@/../i18n/renderer';
|
||||
import { usePlayerStore, useSettingsStore } from '@/store';
|
||||
|
||||
import {
|
||||
hasShortcutAction,
|
||||
normalizeShortcutAccelerator,
|
||||
normalizeShortcutsConfig,
|
||||
type ShortcutAction,
|
||||
shortcutActionOrder,
|
||||
type ShortcutsConfig
|
||||
} from '../../shared/shortcuts';
|
||||
import { isElectron } from '.';
|
||||
import { isEditableTarget, keyboardEventToAccelerator } from './shortcutKeyboard';
|
||||
import { showShortcutToast } from './shortcutToast';
|
||||
|
||||
// 添加一个简单的防抖机制
|
||||
let actionTimeout: NodeJS.Timeout | null = null;
|
||||
const ACTION_DELAY = 300; // 毫秒
|
||||
const ACTION_DELAY = 260;
|
||||
|
||||
// 添加一个操作锁,记录最后一次操作的时间和动作
|
||||
let lastActionInfo = {
|
||||
action: '',
|
||||
timestamp: 0
|
||||
};
|
||||
|
||||
interface ShortcutConfig {
|
||||
key: string;
|
||||
enabled: boolean;
|
||||
scope: 'global' | 'app';
|
||||
}
|
||||
|
||||
interface ShortcutsConfig {
|
||||
[key: string]: ShortcutConfig;
|
||||
}
|
||||
const actionTimestamps = new Map<ShortcutAction, number>();
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
// 全局存储快捷键配置
|
||||
let appShortcuts: ShortcutsConfig = {};
|
||||
let appShortcuts: ShortcutsConfig = normalizeShortcutsConfig(null);
|
||||
let appShortcutsSuspended = false;
|
||||
let appShortcutsInitialized = false;
|
||||
|
||||
const onGlobalShortcut = (_event: unknown, action: string) => {
|
||||
if (!hasShortcutAction(action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
void handleShortcutAction(action);
|
||||
};
|
||||
|
||||
const onUpdateAppShortcuts = (_event: unknown, shortcuts: unknown) => {
|
||||
updateAppShortcuts(shortcuts);
|
||||
};
|
||||
|
||||
function shouldSkipAction(action: ShortcutAction): boolean {
|
||||
const now = Date.now();
|
||||
const lastTimestamp = actionTimestamps.get(action) ?? 0;
|
||||
|
||||
if (now - lastTimestamp < ACTION_DELAY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
actionTimestamps.set(action, now);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理快捷键动作
|
||||
* @param action 快捷键动作
|
||||
*/
|
||||
export async function handleShortcutAction(action: string) {
|
||||
const now = Date.now();
|
||||
|
||||
// 如果存在未完成的动作,则忽略当前请求
|
||||
if (actionTimeout) {
|
||||
console.log('[AppShortcuts] 忽略快速连续的动作请求:', action);
|
||||
export async function handleShortcutAction(action: ShortcutAction) {
|
||||
if (shouldSkipAction(action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是同一个动作的重复触发(300ms内)
|
||||
if (lastActionInfo.action === action && now - lastActionInfo.timestamp < ACTION_DELAY) {
|
||||
console.log(
|
||||
`[AppShortcuts] 忽略重复的 ${action} 动作,距上次仅 ${now - lastActionInfo.timestamp}ms`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新最后一次操作信息
|
||||
lastActionInfo = {
|
||||
action,
|
||||
timestamp: now
|
||||
};
|
||||
|
||||
// 设置防抖锁
|
||||
actionTimeout = setTimeout(() => {
|
||||
actionTimeout = null;
|
||||
}, ACTION_DELAY);
|
||||
|
||||
console.log(`[AppShortcuts] 执行动作: ${action}, 时间戳: ${now}`);
|
||||
|
||||
const playerStore = usePlayerStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
@@ -81,11 +74,9 @@ export async function handleShortcutAction(action: string) {
|
||||
if (playerStore.play) {
|
||||
await playerStore.handlePause();
|
||||
showToast(t('player.playBar.pause'), 'ri-pause-circle-line');
|
||||
} else {
|
||||
if (playerStore.playMusic?.id) {
|
||||
await playerStore.setPlay({ ...playerStore.playMusic });
|
||||
showToast(t('player.playBar.play'), 'ri-play-circle-line');
|
||||
}
|
||||
} else if (playerStore.playMusic?.id) {
|
||||
await playerStore.setPlay({ ...playerStore.playMusic });
|
||||
showToast(t('player.playBar.play'), 'ri-play-circle-line');
|
||||
}
|
||||
break;
|
||||
case 'prevPlay':
|
||||
@@ -115,16 +106,19 @@ export async function handleShortcutAction(action: string) {
|
||||
}
|
||||
break;
|
||||
case 'toggleFavorite': {
|
||||
const isFavorite = playerStore.favoriteList.includes(Number(playerStore.playMusic.id));
|
||||
const numericId = Number(playerStore.playMusic.id);
|
||||
console.log(`[AppShortcuts] toggleFavorite 当前状态: ${isFavorite}, ID: ${numericId}`);
|
||||
if (isFavorite) {
|
||||
playerStore.removeFromFavorite(numericId);
|
||||
console.log(`[AppShortcuts] 已从收藏中移除: ${numericId}`);
|
||||
} else {
|
||||
playerStore.addToFavorite(numericId);
|
||||
console.log(`[AppShortcuts] 已添加到收藏: ${numericId}`);
|
||||
if (!playerStore.playMusic?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSongId = Number(playerStore.playMusic.id);
|
||||
const isFavorite = playerStore.favoriteList.includes(currentSongId);
|
||||
|
||||
if (isFavorite) {
|
||||
playerStore.removeFromFavorite(currentSongId);
|
||||
} else {
|
||||
playerStore.addToFavorite(currentSongId);
|
||||
}
|
||||
|
||||
showToast(
|
||||
isFavorite
|
||||
? t('player.playBar.unFavorite', { name: playerStore.playMusic.name })
|
||||
@@ -134,119 +128,91 @@ export async function handleShortcutAction(action: string) {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.log('未知的快捷键动作:', action);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`执行快捷键动作 ${action} 时出错:`, error);
|
||||
} finally {
|
||||
// 确保在出错时也能清除超时
|
||||
clearTimeout(actionTimeout);
|
||||
actionTimeout = null;
|
||||
console.log(
|
||||
`[AppShortcuts] 动作完成: ${action}, 时间戳: ${Date.now()}, 耗时: ${Date.now() - now}ms`
|
||||
);
|
||||
console.error(`[AppShortcuts] 执行动作失败: ${action}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查按键是否匹配快捷键
|
||||
* @param e KeyboardEvent
|
||||
* @param shortcutKey 快捷键字符串
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
function matchShortcut(e: KeyboardEvent, shortcutKey: string): boolean {
|
||||
const keys = shortcutKey.split('+');
|
||||
const pressedKey = e.key.length === 1 ? e.key.toUpperCase() : e.key;
|
||||
|
||||
// 检查修饰键
|
||||
const hasCommandOrControl = keys.includes('CommandOrControl');
|
||||
const hasAlt = keys.includes('Alt');
|
||||
const hasShift = keys.includes('Shift');
|
||||
|
||||
// 检查主键
|
||||
let mainKey = keys.find((k) => !['CommandOrControl', 'Alt', 'Shift'].includes(k));
|
||||
if (!mainKey) return false;
|
||||
|
||||
// 处理特殊键
|
||||
if (mainKey === 'Left' && pressedKey === 'ArrowLeft') mainKey = 'ArrowLeft';
|
||||
if (mainKey === 'Right' && pressedKey === 'ArrowRight') mainKey = 'ArrowRight';
|
||||
if (mainKey === 'Up' && pressedKey === 'ArrowUp') mainKey = 'ArrowUp';
|
||||
if (mainKey === 'Down' && pressedKey === 'ArrowDown') mainKey = 'ArrowDown';
|
||||
|
||||
// 检查是否所有条件都匹配
|
||||
return (
|
||||
hasCommandOrControl === (e.ctrlKey || e.metaKey) &&
|
||||
hasAlt === e.altKey &&
|
||||
hasShift === e.shiftKey &&
|
||||
mainKey === pressedKey
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局键盘事件处理函数
|
||||
* @param e KeyboardEvent
|
||||
*/
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// 如果在输入框中则不处理快捷键
|
||||
if (['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (appShortcutsSuspended) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(appShortcuts).forEach(([action, config]) => {
|
||||
if (config.enabled && config.scope === 'app' && matchShortcut(e, config.key)) {
|
||||
e.preventDefault();
|
||||
handleShortcutAction(action);
|
||||
if (isEditableTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accelerator = keyboardEventToAccelerator(event);
|
||||
if (!accelerator) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const action of shortcutActionOrder) {
|
||||
const config = appShortcuts[action];
|
||||
if (!config.enabled || config.scope !== 'app') {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
const shortcutKey = normalizeShortcutAccelerator(config.key);
|
||||
if (!shortcutKey || shortcutKey !== accelerator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
void handleShortcutAction(action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新应用内快捷键
|
||||
* @param shortcuts 快捷键配置
|
||||
*/
|
||||
export function updateAppShortcuts(shortcuts: ShortcutsConfig) {
|
||||
appShortcuts = shortcuts;
|
||||
export function updateAppShortcuts(shortcuts: unknown) {
|
||||
appShortcuts = normalizeShortcutsConfig(shortcuts);
|
||||
}
|
||||
|
||||
export function setAppShortcutsSuspended(suspended: boolean) {
|
||||
appShortcutsSuspended = suspended;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化应用内快捷键
|
||||
*/
|
||||
export function initAppShortcuts() {
|
||||
if (isElectron) {
|
||||
// 监听全局快捷键事件
|
||||
window.electron.ipcRenderer.on('global-shortcut', async (_, action: string) => {
|
||||
handleShortcutAction(action);
|
||||
});
|
||||
|
||||
// 监听应用内快捷键更新
|
||||
window.electron.ipcRenderer.on('update-app-shortcuts', (_, shortcuts: ShortcutsConfig) => {
|
||||
updateAppShortcuts(shortcuts);
|
||||
});
|
||||
|
||||
// 获取初始快捷键配置
|
||||
const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts');
|
||||
if (storedShortcuts) {
|
||||
updateAppShortcuts(storedShortcuts);
|
||||
}
|
||||
|
||||
// 添加键盘事件监听
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
if (!isElectron || appShortcutsInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
appShortcutsInitialized = true;
|
||||
|
||||
window.electron.ipcRenderer.on('global-shortcut', onGlobalShortcut);
|
||||
window.electron.ipcRenderer.on('update-app-shortcuts', onUpdateAppShortcuts);
|
||||
|
||||
const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts');
|
||||
updateAppShortcuts(storedShortcuts);
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理应用内快捷键
|
||||
*/
|
||||
export function cleanupAppShortcuts() {
|
||||
if (isElectron) {
|
||||
// 移除全局事件监听
|
||||
window.electron.ipcRenderer.removeAllListeners('global-shortcut');
|
||||
window.electron.ipcRenderer.removeAllListeners('update-app-shortcuts');
|
||||
|
||||
// 移除键盘事件监听
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
if (!isElectron || !appShortcutsInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
appShortcutsInitialized = false;
|
||||
|
||||
window.electron.ipcRenderer.removeListener('global-shortcut', onGlobalShortcut);
|
||||
window.electron.ipcRenderer.removeListener('update-app-shortcuts', onUpdateAppShortcuts);
|
||||
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { normalizeShortcutAccelerator } from '../../shared/shortcuts';
|
||||
|
||||
const modifierOnlyKeys = new Set(['Control', 'Meta', 'Alt', 'Shift']);
|
||||
|
||||
const keyAliases: Record<string, string> = {
|
||||
ArrowLeft: 'Left',
|
||||
ArrowRight: 'Right',
|
||||
ArrowUp: 'Up',
|
||||
ArrowDown: 'Down',
|
||||
Escape: 'Escape',
|
||||
Enter: 'Enter',
|
||||
Tab: 'Tab',
|
||||
Backspace: 'Backspace',
|
||||
Delete: 'Delete',
|
||||
Insert: 'Insert',
|
||||
Home: 'Home',
|
||||
End: 'End',
|
||||
PageUp: 'PageUp',
|
||||
PageDown: 'PageDown',
|
||||
' ': 'Space',
|
||||
Spacebar: 'Space',
|
||||
MediaPlayPause: 'MediaPlayPause',
|
||||
MediaNextTrack: 'MediaNextTrack',
|
||||
MediaPreviousTrack: 'MediaPreviousTrack',
|
||||
MediaStop: 'MediaStop'
|
||||
};
|
||||
|
||||
function resolveMainKey(event: KeyboardEvent): string | null {
|
||||
if (event.isComposing || modifierOnlyKeys.has(event.key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { code, key } = event;
|
||||
|
||||
if (/^Key[A-Z]$/.test(code)) {
|
||||
return code.slice(3);
|
||||
}
|
||||
|
||||
if (/^Digit[0-9]$/.test(code)) {
|
||||
return code.slice(5);
|
||||
}
|
||||
|
||||
if (/^Numpad[0-9]$/.test(code)) {
|
||||
return code.slice(6);
|
||||
}
|
||||
|
||||
if (/^F([1-9]|1\d|2[0-4])$/.test(code)) {
|
||||
return code;
|
||||
}
|
||||
|
||||
if (code === 'NumpadAdd') {
|
||||
return 'Plus';
|
||||
}
|
||||
|
||||
if (code === 'NumpadSubtract' || code === 'Minus') {
|
||||
return 'Minus';
|
||||
}
|
||||
|
||||
if (code === 'Equal' && event.shiftKey) {
|
||||
return 'Plus';
|
||||
}
|
||||
|
||||
if (key === '+' || key === '=') {
|
||||
return 'Plus';
|
||||
}
|
||||
|
||||
if (key === '-' || key === '_') {
|
||||
return 'Minus';
|
||||
}
|
||||
|
||||
if (keyAliases[key]) {
|
||||
return keyAliases[key];
|
||||
}
|
||||
|
||||
if (/^[A-Za-z0-9]$/.test(key)) {
|
||||
return key.toUpperCase();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function keyboardEventToAccelerator(event: KeyboardEvent): string | null {
|
||||
const mainKey = resolveMainKey(event);
|
||||
if (!mainKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
parts.push('CommandOrControl');
|
||||
}
|
||||
if (event.altKey) {
|
||||
parts.push('Alt');
|
||||
}
|
||||
if (event.shiftKey) {
|
||||
parts.push('Shift');
|
||||
}
|
||||
|
||||
return normalizeShortcutAccelerator([...parts, mainKey].join('+'));
|
||||
}
|
||||
|
||||
export function isEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.isContentEditable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tagName = target.tagName;
|
||||
if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(target.closest('[contenteditable="true"]'));
|
||||
}
|
||||
Reference in New Issue
Block a user