mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 14:20:50 +08:00
feat: 快捷键整体重构优化
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,6 +32,7 @@ android/app/release
|
||||
.cursor
|
||||
.windsurf
|
||||
.agent
|
||||
.agents
|
||||
.claude
|
||||
.kiro
|
||||
CLAUDE.md
|
||||
|
||||
@@ -383,28 +383,61 @@ export default {
|
||||
title: 'Shortcut Settings',
|
||||
shortcut: 'Shortcut',
|
||||
shortcutDesc: 'Customize global shortcuts',
|
||||
summaryReady: 'Shortcut configuration is ready to save',
|
||||
summaryRecording: 'Recording a new shortcut combination',
|
||||
summaryBlocked: 'Fix conflicts or invalid entries before saving',
|
||||
platformHintMac: 'On macOS, CommandOrControl is displayed as Cmd',
|
||||
platformHintWindows: 'On Windows, CommandOrControl is displayed as Ctrl',
|
||||
platformHintLinux: 'On Linux, CommandOrControl is displayed as Ctrl',
|
||||
platformHintGeneric: 'CommandOrControl is adapted per operating system',
|
||||
enabledCount: 'Enabled',
|
||||
recordingTip: 'Click a shortcut field, press combination. Esc cancels, Delete disables',
|
||||
shortcutConflict: 'Shortcut Conflict',
|
||||
inputPlaceholder: 'Click to input shortcut',
|
||||
clickToRecord: 'Click then press a shortcut',
|
||||
recording: 'Recording...',
|
||||
resetShortcuts: 'Reset',
|
||||
restoreSingle: 'Restore',
|
||||
disableAll: 'Disable All',
|
||||
enableAll: 'Enable All',
|
||||
groups: {
|
||||
playback: 'Playback',
|
||||
sound: 'Volume & Favorite',
|
||||
window: 'Window'
|
||||
},
|
||||
togglePlay: 'Play/Pause',
|
||||
togglePlayDesc: 'Toggle current playback state',
|
||||
prevPlay: 'Previous',
|
||||
prevPlayDesc: 'Play the previous track',
|
||||
nextPlay: 'Next',
|
||||
nextPlayDesc: 'Play the next track',
|
||||
volumeUp: 'Volume Up',
|
||||
volumeUpDesc: 'Increase player volume',
|
||||
volumeDown: 'Volume Down',
|
||||
volumeDownDesc: 'Decrease player volume',
|
||||
toggleFavorite: 'Favorite/Unfavorite',
|
||||
toggleFavoriteDesc: 'Favorite or unfavorite current track',
|
||||
toggleWindow: 'Show/Hide Window',
|
||||
toggleWindowDesc: 'Quickly show or hide the main window',
|
||||
scopeGlobal: 'Global',
|
||||
scopeApp: 'App Only',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
issueInvalid: 'Invalid combo',
|
||||
issueReserved: 'System reserved',
|
||||
registrationWarningTitle: 'These shortcuts could not be registered',
|
||||
registrationOccupied: 'Occupied by system or another app',
|
||||
registrationInvalid: 'Invalid shortcut format',
|
||||
messages: {
|
||||
resetSuccess: 'Shortcuts reset successfully, please save',
|
||||
conflict: 'Shortcut conflict, please reset',
|
||||
saveSuccess: 'Shortcuts saved successfully',
|
||||
saveError: 'Failed to save shortcuts',
|
||||
saveValidationError: 'Shortcut validation failed, please review and try again',
|
||||
partialRegistered: 'Saved, but some global shortcuts were not registered',
|
||||
cancelEdit: 'Edit cancelled',
|
||||
clearToDisable: 'Shortcut disabled',
|
||||
invalidShortcut: 'Invalid shortcut, please use a valid combination',
|
||||
disableAll: 'All shortcuts disabled, please save to apply',
|
||||
enableAll: 'All shortcuts enabled, please save to apply'
|
||||
}
|
||||
|
||||
@@ -382,28 +382,61 @@ export default {
|
||||
title: 'ショートカット設定',
|
||||
shortcut: 'ショートカット',
|
||||
shortcutDesc: 'ショートカットをカスタマイズ',
|
||||
summaryReady: 'ショートカット設定は保存可能です',
|
||||
summaryRecording: '新しいショートカットを記録中です',
|
||||
summaryBlocked: '競合または無効な項目を修正してください',
|
||||
platformHintMac: 'macOS では CommandOrControl は Cmd と表示されます',
|
||||
platformHintWindows: 'Windows では CommandOrControl は Ctrl と表示されます',
|
||||
platformHintLinux: 'Linux では CommandOrControl は Ctrl と表示されます',
|
||||
platformHintGeneric: 'CommandOrControl はOSに応じて自動変換されます',
|
||||
enabledCount: '有効',
|
||||
recordingTip: '欄をクリックしてキー入力。Escでキャンセル、Deleteで無効化',
|
||||
shortcutConflict: 'ショートカットの競合',
|
||||
inputPlaceholder: 'クリックしてショートカットを入力',
|
||||
clickToRecord: 'クリックしてキーを入力',
|
||||
recording: '記録中...',
|
||||
resetShortcuts: 'デフォルトに戻す',
|
||||
restoreSingle: '復元',
|
||||
disableAll: 'すべて無効',
|
||||
enableAll: 'すべて有効',
|
||||
groups: {
|
||||
playback: '再生操作',
|
||||
sound: '音量とお気に入り',
|
||||
window: 'ウィンドウ'
|
||||
},
|
||||
togglePlay: '再生/一時停止',
|
||||
togglePlayDesc: '現在の再生状態を切り替えます',
|
||||
prevPlay: '前の曲',
|
||||
prevPlayDesc: '前の曲に切り替えます',
|
||||
nextPlay: '次の曲',
|
||||
nextPlayDesc: '次の曲に切り替えます',
|
||||
volumeUp: '音量を上げる',
|
||||
volumeUpDesc: 'プレイヤー音量を上げます',
|
||||
volumeDown: '音量を下げる',
|
||||
volumeDownDesc: 'プレイヤー音量を下げます',
|
||||
toggleFavorite: 'お気に入り/お気に入り解除',
|
||||
toggleFavoriteDesc: '現在の曲をお気に入り切り替えします',
|
||||
toggleWindow: 'ウィンドウ表示/非表示',
|
||||
toggleWindowDesc: 'メインウィンドウを表示/非表示にします',
|
||||
scopeGlobal: 'グローバル',
|
||||
scopeApp: 'アプリ内',
|
||||
enabled: '有効',
|
||||
disabled: '無効',
|
||||
issueInvalid: '無効な組み合わせ',
|
||||
issueReserved: 'システム予約',
|
||||
registrationWarningTitle: '以下のショートカットは登録できませんでした',
|
||||
registrationOccupied: 'システムまたは他アプリで使用中',
|
||||
registrationInvalid: 'ショートカット形式が無効',
|
||||
messages: {
|
||||
resetSuccess: 'デフォルトのショートカットに戻しました。保存を忘れずに',
|
||||
conflict: '競合するショートカットがあります。再設定してください',
|
||||
saveSuccess: 'ショートカット設定を保存しました',
|
||||
saveError: 'ショートカットの保存に失敗しました。再試行してください',
|
||||
saveValidationError: 'ショートカット検証に失敗しました。内容を確認してください',
|
||||
partialRegistered: '保存しましたが、一部のグローバルショートカットは登録されませんでした',
|
||||
cancelEdit: '変更をキャンセルしました',
|
||||
clearToDisable: 'このショートカットを無効にしました',
|
||||
invalidShortcut: '無効なショートカットです。有効な組み合わせを入力してください',
|
||||
disableAll: 'すべてのショートカットを無効にしました。保存を忘れずに',
|
||||
enableAll: 'すべてのショートカットを有効にしました。保存を忘れずに'
|
||||
}
|
||||
|
||||
@@ -383,28 +383,61 @@ export default {
|
||||
title: '단축키 설정',
|
||||
shortcut: '단축키',
|
||||
shortcutDesc: '단축키 사용자 정의',
|
||||
summaryReady: '단축키 구성이 저장 가능한 상태입니다',
|
||||
summaryRecording: '새 단축키 조합을 입력 중입니다',
|
||||
summaryBlocked: '충돌 또는 잘못된 항목을 먼저 수정하세요',
|
||||
platformHintMac: 'macOS에서는 CommandOrControl이 Cmd로 표시됩니다',
|
||||
platformHintWindows: 'Windows에서는 CommandOrControl이 Ctrl로 표시됩니다',
|
||||
platformHintLinux: 'Linux에서는 CommandOrControl이 Ctrl로 표시됩니다',
|
||||
platformHintGeneric: 'CommandOrControl은 운영체제에 맞게 자동 변환됩니다',
|
||||
enabledCount: '활성화됨',
|
||||
recordingTip: '필드를 클릭 후 조합키 입력, Esc 취소, Delete 비활성화',
|
||||
shortcutConflict: '단축키 충돌',
|
||||
inputPlaceholder: '클릭하여 단축키 입력',
|
||||
clickToRecord: '클릭 후 단축키 입력',
|
||||
recording: '입력 중...',
|
||||
resetShortcuts: '기본값 복원',
|
||||
restoreSingle: '복원',
|
||||
disableAll: '모두 비활성화',
|
||||
enableAll: '모두 활성화',
|
||||
groups: {
|
||||
playback: '재생 제어',
|
||||
sound: '볼륨 및 즐겨찾기',
|
||||
window: '창 제어'
|
||||
},
|
||||
togglePlay: '재생/일시정지',
|
||||
togglePlayDesc: '현재 재생 상태를 전환합니다',
|
||||
prevPlay: '이전 곡',
|
||||
prevPlayDesc: '이전 곡으로 이동합니다',
|
||||
nextPlay: '다음 곡',
|
||||
nextPlayDesc: '다음 곡으로 이동합니다',
|
||||
volumeUp: '볼륨 증가',
|
||||
volumeUpDesc: '플레이어 볼륨을 높입니다',
|
||||
volumeDown: '볼륨 감소',
|
||||
volumeDownDesc: '플레이어 볼륨을 낮춥니다',
|
||||
toggleFavorite: '즐겨찾기/즐겨찾기 취소',
|
||||
toggleFavoriteDesc: '현재 곡 즐겨찾기를 전환합니다',
|
||||
toggleWindow: '창 표시/숨기기',
|
||||
toggleWindowDesc: '메인 창을 빠르게 표시/숨김합니다',
|
||||
scopeGlobal: '전역',
|
||||
scopeApp: '앱 내',
|
||||
enabled: '활성화',
|
||||
disabled: '비활성화',
|
||||
issueInvalid: '잘못된 조합',
|
||||
issueReserved: '시스템 예약',
|
||||
registrationWarningTitle: '다음 단축키는 등록되지 않았습니다',
|
||||
registrationOccupied: '시스템 또는 다른 앱에서 사용 중',
|
||||
registrationInvalid: '단축키 형식이 잘못됨',
|
||||
messages: {
|
||||
resetSuccess: '기본 단축키로 복원되었습니다. 저장을 잊지 마세요',
|
||||
conflict: '충돌하는 단축키가 있습니다. 다시 설정하세요',
|
||||
saveSuccess: '단축키 설정이 저장되었습니다',
|
||||
saveError: '단축키 저장 실패, 다시 시도하세요',
|
||||
saveValidationError: '단축키 검증에 실패했습니다. 설정을 확인하세요',
|
||||
partialRegistered: '저장되었지만 일부 전역 단축키는 등록되지 않았습니다',
|
||||
cancelEdit: '수정이 취소되었습니다',
|
||||
clearToDisable: '해당 단축키가 비활성화되었습니다',
|
||||
invalidShortcut: '잘못된 단축키입니다. 유효한 조합을 입력하세요',
|
||||
disableAll: '모든 단축키가 비활성화되었습니다. 저장을 잊지 마세요',
|
||||
enableAll: '모든 단축키가 활성화되었습니다. 저장을 잊지 마세요'
|
||||
}
|
||||
|
||||
@@ -380,28 +380,61 @@ export default {
|
||||
title: '快捷键设置',
|
||||
shortcut: '快捷键',
|
||||
shortcutDesc: '自定义快捷键',
|
||||
summaryReady: '当前快捷键配置可保存',
|
||||
summaryRecording: '正在录制新的快捷键组合',
|
||||
summaryBlocked: '存在冲突或无效项,请先修正',
|
||||
platformHintMac: 'macOS 下 CommandOrControl 会显示为 Cmd',
|
||||
platformHintWindows: 'Windows 下 CommandOrControl 会显示为 Ctrl',
|
||||
platformHintLinux: 'Linux 下 CommandOrControl 会显示为 Ctrl',
|
||||
platformHintGeneric: '不同系统下 CommandOrControl 会自动适配',
|
||||
enabledCount: '已启用',
|
||||
recordingTip: '点击快捷键框后按下组合键,Esc 取消,Delete 可禁用该项',
|
||||
shortcutConflict: '快捷键冲突',
|
||||
inputPlaceholder: '点击输入快捷键',
|
||||
clickToRecord: '点击后按下组合键',
|
||||
recording: '录制中...',
|
||||
resetShortcuts: '恢复默认',
|
||||
restoreSingle: '恢复',
|
||||
disableAll: '全部禁用',
|
||||
enableAll: '全部启用',
|
||||
groups: {
|
||||
playback: '播放控制',
|
||||
sound: '音量与收藏',
|
||||
window: '窗口控制'
|
||||
},
|
||||
togglePlay: '播放/暂停',
|
||||
togglePlayDesc: '切换当前歌曲播放状态',
|
||||
prevPlay: '上一首',
|
||||
prevPlayDesc: '切换到上一首歌曲',
|
||||
nextPlay: '下一首',
|
||||
nextPlayDesc: '切换到下一首歌曲',
|
||||
volumeUp: '音量增加',
|
||||
volumeUpDesc: '提高播放器音量',
|
||||
volumeDown: '音量减少',
|
||||
volumeDownDesc: '降低播放器音量',
|
||||
toggleFavorite: '收藏/取消收藏',
|
||||
toggleFavoriteDesc: '收藏或取消当前歌曲',
|
||||
toggleWindow: '显示/隐藏窗口',
|
||||
toggleWindowDesc: '快速显示或隐藏主窗口',
|
||||
scopeGlobal: '全局',
|
||||
scopeApp: '应用内',
|
||||
enabled: '启用',
|
||||
disabled: '禁用',
|
||||
issueInvalid: '组合无效',
|
||||
issueReserved: '系统保留',
|
||||
registrationWarningTitle: '以下快捷键未能注册,请更换组合后重试',
|
||||
registrationOccupied: '被系统或其他应用占用',
|
||||
registrationInvalid: '键位格式无效',
|
||||
messages: {
|
||||
resetSuccess: '已恢复默认快捷键,请记得保存',
|
||||
conflict: '存在冲突的快捷键,请重新设置',
|
||||
saveSuccess: '快捷键设置已保存',
|
||||
saveError: '保存快捷键失败,请重试',
|
||||
saveValidationError: '快捷键校验未通过,请检查后重试',
|
||||
partialRegistered: '已保存,但部分全局快捷键未注册成功',
|
||||
cancelEdit: '已取消修改',
|
||||
clearToDisable: '已禁用该快捷键',
|
||||
invalidShortcut: '快捷键无效,请输入有效组合',
|
||||
disableAll: '已禁用所有快捷键,请记得保存',
|
||||
enableAll: '已启用所有快捷键,请记得保存'
|
||||
}
|
||||
|
||||
@@ -377,28 +377,61 @@ export default {
|
||||
title: '快捷鍵設定',
|
||||
shortcut: '快捷鍵',
|
||||
shortcutDesc: '自訂快捷鍵',
|
||||
summaryReady: '目前快捷鍵設定可直接儲存',
|
||||
summaryRecording: '正在錄製新的快捷鍵組合',
|
||||
summaryBlocked: '存在衝突或無效項目,請先修正',
|
||||
platformHintMac: 'macOS 下 CommandOrControl 會顯示為 Cmd',
|
||||
platformHintWindows: 'Windows 下 CommandOrControl 會顯示為 Ctrl',
|
||||
platformHintLinux: 'Linux 下 CommandOrControl 會顯示為 Ctrl',
|
||||
platformHintGeneric: 'CommandOrControl 會依系統自動適配',
|
||||
enabledCount: '已啟用',
|
||||
recordingTip: '點擊快捷鍵欄位後輸入組合鍵,Esc 取消,Delete 可停用',
|
||||
shortcutConflict: '快捷鍵衝突',
|
||||
inputPlaceholder: '點擊輸入快捷鍵',
|
||||
clickToRecord: '點擊後輸入快捷鍵',
|
||||
recording: '錄製中...',
|
||||
resetShortcuts: '恢復預設',
|
||||
restoreSingle: '恢復',
|
||||
disableAll: '全部停用',
|
||||
enableAll: '全部啟用',
|
||||
groups: {
|
||||
playback: '播放控制',
|
||||
sound: '音量與收藏',
|
||||
window: '視窗控制'
|
||||
},
|
||||
togglePlay: '播放/暫停',
|
||||
togglePlayDesc: '切換目前歌曲播放狀態',
|
||||
prevPlay: '上一首',
|
||||
prevPlayDesc: '切換到上一首歌曲',
|
||||
nextPlay: '下一首',
|
||||
nextPlayDesc: '切換到下一首歌曲',
|
||||
volumeUp: '增加音量',
|
||||
volumeUpDesc: '提高播放器音量',
|
||||
volumeDown: '減少音量',
|
||||
volumeDownDesc: '降低播放器音量',
|
||||
toggleFavorite: '收藏/取消收藏',
|
||||
toggleFavoriteDesc: '收藏或取消目前歌曲',
|
||||
toggleWindow: '顯示/隱藏視窗',
|
||||
toggleWindowDesc: '快速顯示或隱藏主視窗',
|
||||
scopeGlobal: '全域',
|
||||
scopeApp: '應用程式內',
|
||||
enabled: '已啟用',
|
||||
disabled: '已停用',
|
||||
issueInvalid: '組合無效',
|
||||
issueReserved: '系統保留',
|
||||
registrationWarningTitle: '以下快捷鍵未能註冊,請改用其他組合',
|
||||
registrationOccupied: '被系統或其他應用程式占用',
|
||||
registrationInvalid: '鍵位格式無效',
|
||||
messages: {
|
||||
resetSuccess: '已恢復預設快捷鍵,請記得儲存',
|
||||
conflict: '存在快捷鍵衝突,請重新設定',
|
||||
saveSuccess: '快捷鍵設定已儲存',
|
||||
saveError: '快捷鍵儲存失敗,請重試',
|
||||
saveValidationError: '快捷鍵校驗未通過,請檢查後重試',
|
||||
partialRegistered: '已儲存,但部分全域快捷鍵未註冊成功',
|
||||
cancelEdit: '已取消修改',
|
||||
clearToDisable: '已停用該快捷鍵',
|
||||
invalidShortcut: '快捷鍵無效,請輸入有效組合',
|
||||
disableAll: '已停用所有快捷鍵,請記得儲存',
|
||||
enableAll: '已啟用所有快捷鍵,請記得儲存'
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { initializeLoginWindow } from './modules/loginWindow';
|
||||
import { initLxMusicHttp } from './modules/lxMusicHttp';
|
||||
import { initializeOtherApi } from './modules/otherApi';
|
||||
import { initializeRemoteControl } from './modules/remoteControl';
|
||||
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
||||
import { initializeShortcuts } from './modules/shortcuts';
|
||||
import { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray';
|
||||
import { setupUpdateHandlers } from './modules/update';
|
||||
import { createMainWindow, initializeWindowManager, setAppQuitting } from './modules/window';
|
||||
@@ -149,11 +149,6 @@ if (!isSingleInstance) {
|
||||
});
|
||||
});
|
||||
|
||||
// 监听快捷键更新
|
||||
ipcMain.on('update-shortcuts', () => {
|
||||
registerShortcuts(mainWindow);
|
||||
});
|
||||
|
||||
// 监听语言切换
|
||||
ipcMain.on('change-language', (_, locale: Language) => {
|
||||
// 更新主进程的语言设置
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { app, ipcMain } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
|
||||
import { createDefaultShortcuts, type ShortcutsConfig } from '../../shared/shortcuts';
|
||||
import set from '../set.json';
|
||||
import { defaultShortcuts } from './shortcuts';
|
||||
|
||||
type SetConfig = {
|
||||
isProxy: boolean;
|
||||
@@ -29,7 +29,7 @@ type SetConfig = {
|
||||
};
|
||||
interface StoreType {
|
||||
set: SetConfig;
|
||||
shortcuts: typeof defaultShortcuts;
|
||||
shortcuts: ShortcutsConfig;
|
||||
}
|
||||
|
||||
let store: Store<StoreType>;
|
||||
@@ -42,7 +42,7 @@ export function initializeConfig() {
|
||||
name: 'config',
|
||||
defaults: {
|
||||
set: set as SetConfig,
|
||||
shortcuts: defaultShortcuts
|
||||
shortcuts: createDefaultShortcuts()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,122 +1,398 @@
|
||||
import { globalShortcut, ipcMain } from 'electron';
|
||||
import { type BrowserWindow, globalShortcut, ipcMain } from 'electron';
|
||||
|
||||
import {
|
||||
defaultShortcuts,
|
||||
getReservedAccelerators,
|
||||
getShortcutConflicts,
|
||||
hasShortcutAction,
|
||||
isModifierOnlyShortcut,
|
||||
normalizeShortcutAccelerator,
|
||||
normalizeShortcutsConfig,
|
||||
type ShortcutAction,
|
||||
shortcutActionOrder,
|
||||
type ShortcutPlatform,
|
||||
type ShortcutsConfig,
|
||||
type ShortcutScope
|
||||
} from '../../shared/shortcuts';
|
||||
import { getStore } from './config';
|
||||
|
||||
// 添加获取平台信息的 IPC 处理程序
|
||||
ipcMain.on('get-platform', (event) => {
|
||||
event.returnValue = process.platform;
|
||||
});
|
||||
type ShortcutRegistrationFailureReason = 'invalid' | 'occupied';
|
||||
|
||||
// 定义快捷键配置接口
|
||||
export interface ShortcutConfig {
|
||||
type ShortcutRegistrationFailure = {
|
||||
action: ShortcutAction;
|
||||
key: string;
|
||||
enabled: boolean;
|
||||
scope: 'global' | 'app';
|
||||
}
|
||||
|
||||
export interface ShortcutsConfig {
|
||||
[key: string]: ShortcutConfig;
|
||||
}
|
||||
|
||||
// 定义默认快捷键
|
||||
export const defaultShortcuts: ShortcutsConfig = {
|
||||
togglePlay: { key: 'CommandOrControl+Alt+P', enabled: true, scope: 'global' },
|
||||
prevPlay: { key: 'Alt+Left', enabled: true, scope: 'global' },
|
||||
nextPlay: { key: 'Alt+Right', enabled: true, scope: 'global' },
|
||||
volumeUp: { key: 'Alt+Up', enabled: true, scope: 'app' },
|
||||
volumeDown: { key: 'Alt+Down', enabled: true, scope: 'app' },
|
||||
toggleFavorite: { key: 'CommandOrControl+Alt+L', enabled: true, scope: 'app' },
|
||||
toggleWindow: { key: 'CommandOrControl+Alt+Shift+M', enabled: true, scope: 'global' }
|
||||
reason: ShortcutRegistrationFailureReason;
|
||||
};
|
||||
|
||||
let mainWindowRef: Electron.BrowserWindow | null = null;
|
||||
type ShortcutRegistrationResult = {
|
||||
success: boolean;
|
||||
failed: ShortcutRegistrationFailure[];
|
||||
};
|
||||
|
||||
// 注册快捷键
|
||||
export function registerShortcuts(
|
||||
mainWindow: Electron.BrowserWindow,
|
||||
shortcutsConfig?: ShortcutsConfig
|
||||
) {
|
||||
mainWindowRef = mainWindow;
|
||||
const store = getStore();
|
||||
const shortcuts =
|
||||
shortcutsConfig || (store.get('shortcuts') as ShortcutsConfig) || defaultShortcuts;
|
||||
type ShortcutValidationReason = 'invalid' | 'conflict' | 'reserved';
|
||||
|
||||
// 注销所有已注册的快捷键
|
||||
globalShortcut.unregisterAll();
|
||||
type ShortcutValidationIssue = {
|
||||
action: ShortcutAction;
|
||||
key: string;
|
||||
scope: ShortcutScope;
|
||||
reason: ShortcutValidationReason;
|
||||
conflictWith?: ShortcutAction;
|
||||
};
|
||||
|
||||
// 对旧格式数据进行兼容处理
|
||||
if (shortcuts && typeof shortcuts.togglePlay === 'string') {
|
||||
// 将 shortcuts 强制转换为 unknown,再转为 Record<string, string>
|
||||
const oldShortcuts = { ...shortcuts } as unknown as Record<string, string>;
|
||||
const newShortcuts: ShortcutsConfig = {};
|
||||
type ShortcutValidationResult = {
|
||||
shortcuts: ShortcutsConfig;
|
||||
hasBlockingIssue: boolean;
|
||||
issues: ShortcutValidationIssue[];
|
||||
};
|
||||
|
||||
Object.entries(oldShortcuts).forEach(([key, value]) => {
|
||||
newShortcuts[key] = {
|
||||
key: value,
|
||||
enabled: true,
|
||||
scope: ['volumeUp', 'volumeDown', 'toggleFavorite'].includes(key) ? 'app' : 'global'
|
||||
};
|
||||
type ShortcutSaveResult = {
|
||||
ok: boolean;
|
||||
validation: ShortcutValidationResult;
|
||||
registration: ShortcutRegistrationResult;
|
||||
};
|
||||
|
||||
let mainWindowRef: BrowserWindow | null = null;
|
||||
let shortcutsEnabled = true;
|
||||
let shortcutIpcReady = false;
|
||||
|
||||
const managedGlobalShortcuts = new Map<ShortcutAction, string>();
|
||||
|
||||
function currentPlatform(): ShortcutPlatform {
|
||||
if (
|
||||
process.platform === 'darwin' ||
|
||||
process.platform === 'win32' ||
|
||||
process.platform === 'linux'
|
||||
) {
|
||||
return process.platform;
|
||||
}
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
function hasAvailableMainWindow(): boolean {
|
||||
return Boolean(mainWindowRef && !mainWindowRef.isDestroyed());
|
||||
}
|
||||
|
||||
function isShortcutsConfigEqual(left: ShortcutsConfig, right: ShortcutsConfig): boolean {
|
||||
return shortcutActionOrder.every((action) => {
|
||||
const leftConfig = left[action];
|
||||
const rightConfig = right[action];
|
||||
return (
|
||||
leftConfig.key === rightConfig.key &&
|
||||
leftConfig.enabled === rightConfig.enabled &&
|
||||
leftConfig.scope === rightConfig.scope
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
store.set('shortcuts', newShortcuts);
|
||||
registerShortcuts(mainWindow, newShortcuts);
|
||||
function getStoredShortcuts(): ShortcutsConfig {
|
||||
const store = getStore();
|
||||
const rawShortcuts = store.get('shortcuts');
|
||||
const normalizedShortcuts = normalizeShortcutsConfig(rawShortcuts);
|
||||
|
||||
const serializedRaw = JSON.stringify(rawShortcuts ?? null);
|
||||
const serializedNormalized = JSON.stringify(normalizedShortcuts);
|
||||
|
||||
if (serializedRaw !== serializedNormalized) {
|
||||
store.set('shortcuts', normalizedShortcuts);
|
||||
}
|
||||
|
||||
return normalizedShortcuts;
|
||||
}
|
||||
|
||||
function persistShortcuts(shortcuts: ShortcutsConfig) {
|
||||
const store = getStore();
|
||||
const currentShortcuts = normalizeShortcutsConfig(store.get('shortcuts'));
|
||||
|
||||
if (!isShortcutsConfigEqual(currentShortcuts, shortcuts)) {
|
||||
store.set('shortcuts', shortcuts);
|
||||
}
|
||||
}
|
||||
|
||||
function emitShortcutsChanged(
|
||||
shortcuts: ShortcutsConfig,
|
||||
registration: ShortcutRegistrationResult
|
||||
): void {
|
||||
if (!hasAvailableMainWindow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册全局快捷键
|
||||
Object.entries(shortcuts).forEach(([action, config]) => {
|
||||
const { key, enabled, scope } = config as ShortcutConfig;
|
||||
mainWindowRef!.webContents.send('update-app-shortcuts', shortcuts);
|
||||
mainWindowRef!.webContents.send('shortcuts-updated', shortcuts, registration);
|
||||
}
|
||||
|
||||
// 只注册启用且作用域为全局的快捷键
|
||||
if (!enabled || scope !== 'global') return;
|
||||
function unregisterManagedGlobalShortcuts() {
|
||||
managedGlobalShortcuts.forEach((accelerator) => {
|
||||
try {
|
||||
globalShortcut.unregister(accelerator);
|
||||
} catch (error) {
|
||||
console.error(`[Shortcuts] 注销快捷键失败: ${accelerator}`, error);
|
||||
}
|
||||
});
|
||||
managedGlobalShortcuts.clear();
|
||||
}
|
||||
|
||||
function handleShortcutAction(action: ShortcutAction) {
|
||||
if (!hasAvailableMainWindow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mainWindow = mainWindowRef!;
|
||||
|
||||
if (action === 'toggleWindow') {
|
||||
if (mainWindow.isVisible() && mainWindow.isFocused()) {
|
||||
mainWindow.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('global-shortcut', action);
|
||||
}
|
||||
|
||||
function registerManagedGlobalShortcuts(shortcuts: ShortcutsConfig): ShortcutRegistrationResult {
|
||||
unregisterManagedGlobalShortcuts();
|
||||
|
||||
const failed: ShortcutRegistrationFailure[] = [];
|
||||
|
||||
if (!shortcutsEnabled) {
|
||||
return {
|
||||
success: true,
|
||||
failed
|
||||
};
|
||||
}
|
||||
|
||||
shortcutActionOrder.forEach((action) => {
|
||||
const config = shortcuts[action];
|
||||
if (!config.enabled || config.scope !== 'global') {
|
||||
return;
|
||||
}
|
||||
|
||||
const accelerator = normalizeShortcutAccelerator(config.key);
|
||||
if (!accelerator || isModifierOnlyShortcut(accelerator)) {
|
||||
failed.push({
|
||||
action,
|
||||
key: config.key,
|
||||
reason: 'invalid'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'toggleWindow':
|
||||
globalShortcut.register(key, () => {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
const registered = globalShortcut.register(accelerator, () => {
|
||||
handleShortcutAction(action);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
globalShortcut.register(key, () => {
|
||||
mainWindow.webContents.send('global-shortcut', action);
|
||||
|
||||
if (!registered) {
|
||||
failed.push({
|
||||
action,
|
||||
key: accelerator,
|
||||
reason: 'occupied'
|
||||
});
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
managedGlobalShortcuts.set(action, accelerator);
|
||||
} catch (error) {
|
||||
console.error(`注册快捷键 ${key} 失败:`, error);
|
||||
console.error(`[Shortcuts] 注册快捷键失败: ${accelerator}`, error);
|
||||
failed.push({
|
||||
action,
|
||||
key: accelerator,
|
||||
reason: 'invalid'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 通知渲染进程更新应用内快捷键
|
||||
mainWindow.webContents.send('update-app-shortcuts', shortcuts);
|
||||
return {
|
||||
success: failed.length === 0,
|
||||
failed
|
||||
};
|
||||
}
|
||||
|
||||
// 初始化快捷键
|
||||
export function initializeShortcuts(mainWindow: Electron.BrowserWindow) {
|
||||
mainWindowRef = mainWindow;
|
||||
registerShortcuts(mainWindow);
|
||||
function validateShortcuts(rawShortcuts: unknown): ShortcutValidationResult {
|
||||
const shortcuts = normalizeShortcutsConfig(rawShortcuts);
|
||||
const issues: ShortcutValidationIssue[] = [];
|
||||
const issueKeys = new Set<string>();
|
||||
|
||||
const rawShortcutMap =
|
||||
rawShortcuts && typeof rawShortcuts === 'object'
|
||||
? (rawShortcuts as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const pushIssue = (issue: ShortcutValidationIssue) => {
|
||||
const issueKey = `${issue.reason}:${issue.action}:${issue.scope}:${issue.key}:${issue.conflictWith ?? ''}`;
|
||||
if (issueKeys.has(issueKey)) {
|
||||
return;
|
||||
}
|
||||
issueKeys.add(issueKey);
|
||||
issues.push(issue);
|
||||
};
|
||||
|
||||
shortcutActionOrder.forEach((action) => {
|
||||
const rawActionConfig = rawShortcutMap[action];
|
||||
if (!rawActionConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawKey =
|
||||
typeof rawActionConfig === 'string'
|
||||
? rawActionConfig
|
||||
: typeof rawActionConfig === 'object' && rawActionConfig !== null
|
||||
? (rawActionConfig as { key?: unknown }).key
|
||||
: null;
|
||||
|
||||
if (typeof rawKey !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedKey = normalizeShortcutAccelerator(rawKey);
|
||||
if (!normalizedKey || isModifierOnlyShortcut(rawKey)) {
|
||||
pushIssue({
|
||||
action,
|
||||
key: rawKey,
|
||||
scope: shortcuts[action].scope,
|
||||
reason: 'invalid'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const conflicts = getShortcutConflicts(shortcuts);
|
||||
conflicts.forEach((conflict) => {
|
||||
conflict.actions.forEach((action, index) => {
|
||||
const conflictWith = conflict.actions[(index + 1) % conflict.actions.length];
|
||||
pushIssue({
|
||||
action,
|
||||
key: conflict.key,
|
||||
scope: conflict.scope,
|
||||
reason: 'conflict',
|
||||
conflictWith
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const reservedAccelerators = new Set(getReservedAccelerators(currentPlatform()));
|
||||
shortcutActionOrder.forEach((action) => {
|
||||
const config = shortcuts[action];
|
||||
if (!config.enabled || config.scope !== 'global') {
|
||||
return;
|
||||
}
|
||||
|
||||
const accelerator = normalizeShortcutAccelerator(config.key);
|
||||
if (accelerator && reservedAccelerators.has(accelerator)) {
|
||||
pushIssue({
|
||||
action,
|
||||
key: accelerator,
|
||||
scope: config.scope,
|
||||
reason: 'reserved'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
shortcuts,
|
||||
hasBlockingIssue: issues.length > 0,
|
||||
issues
|
||||
};
|
||||
}
|
||||
|
||||
function applyShortcuts(shortcuts: ShortcutsConfig): ShortcutRegistrationResult {
|
||||
const registration = registerManagedGlobalShortcuts(shortcuts);
|
||||
emitShortcutsChanged(shortcuts, registration);
|
||||
return registration;
|
||||
}
|
||||
|
||||
function saveShortcuts(rawShortcuts: unknown): ShortcutSaveResult {
|
||||
const validation = validateShortcuts(rawShortcuts);
|
||||
|
||||
if (validation.hasBlockingIssue) {
|
||||
return {
|
||||
ok: false,
|
||||
validation,
|
||||
registration: {
|
||||
success: false,
|
||||
failed: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
persistShortcuts(validation.shortcuts);
|
||||
const registration = applyShortcuts(validation.shortcuts);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
validation,
|
||||
registration
|
||||
};
|
||||
}
|
||||
|
||||
function setupShortcutIpcHandlers() {
|
||||
if (shortcutIpcReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
shortcutIpcReady = true;
|
||||
|
||||
ipcMain.on('get-platform', (event) => {
|
||||
event.returnValue = process.platform;
|
||||
});
|
||||
|
||||
// 监听禁用快捷键事件
|
||||
ipcMain.on('disable-shortcuts', () => {
|
||||
globalShortcut.unregisterAll();
|
||||
shortcutsEnabled = false;
|
||||
unregisterManagedGlobalShortcuts();
|
||||
});
|
||||
|
||||
// 监听启用快捷键事件
|
||||
ipcMain.on('enable-shortcuts', () => {
|
||||
if (mainWindowRef) {
|
||||
registerShortcuts(mainWindowRef);
|
||||
}
|
||||
shortcutsEnabled = true;
|
||||
const shortcuts = getStoredShortcuts();
|
||||
applyShortcuts(shortcuts);
|
||||
});
|
||||
|
||||
// 监听快捷键更新事件
|
||||
ipcMain.on('update-shortcuts', (_, shortcutsConfig: ShortcutsConfig) => {
|
||||
if (mainWindowRef) {
|
||||
registerShortcuts(mainWindowRef, shortcutsConfig);
|
||||
}
|
||||
ipcMain.on('update-shortcuts', (_, shortcutsConfig: unknown) => {
|
||||
saveShortcuts(shortcutsConfig);
|
||||
});
|
||||
|
||||
ipcMain.handle('shortcuts:get-config', () => {
|
||||
return getStoredShortcuts();
|
||||
});
|
||||
|
||||
ipcMain.handle('shortcuts:validate', (_, shortcutsConfig: unknown) => {
|
||||
return validateShortcuts(shortcutsConfig);
|
||||
});
|
||||
|
||||
ipcMain.handle('shortcuts:save', (_, shortcutsConfig: unknown) => {
|
||||
return saveShortcuts(shortcutsConfig);
|
||||
});
|
||||
}
|
||||
|
||||
export function registerShortcuts(mainWindow: BrowserWindow, shortcutsConfig?: ShortcutsConfig) {
|
||||
mainWindowRef = mainWindow;
|
||||
|
||||
const shortcuts = shortcutsConfig
|
||||
? normalizeShortcutsConfig(shortcutsConfig)
|
||||
: getStoredShortcuts();
|
||||
|
||||
if (shortcutsConfig) {
|
||||
persistShortcuts(shortcuts);
|
||||
}
|
||||
|
||||
return applyShortcuts(shortcuts);
|
||||
}
|
||||
|
||||
export function initializeShortcuts(mainWindow: BrowserWindow) {
|
||||
mainWindowRef = mainWindow;
|
||||
setupShortcutIpcHandlers();
|
||||
|
||||
const shortcuts = getStoredShortcuts();
|
||||
applyShortcuts(shortcuts);
|
||||
}
|
||||
|
||||
export function isShortcutActionSupported(action: string): action is ShortcutAction {
|
||||
return hasShortcutAction(action);
|
||||
}
|
||||
|
||||
export { defaultShortcuts };
|
||||
|
||||
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();
|
||||
|
||||
@@ -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,12 +74,10 @@ 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) {
|
||||
} else if (playerStore.playMusic?.id) {
|
||||
await playerStore.setPlay({ ...playerStore.playMusic });
|
||||
showToast(t('player.playBar.play'), 'ri-play-circle-line');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'prevPlay':
|
||||
await playerStore.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);
|
||||
});
|
||||
if (!isElectron || appShortcutsInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 监听应用内快捷键更新
|
||||
window.electron.ipcRenderer.on('update-app-shortcuts', (_, shortcuts: ShortcutsConfig) => {
|
||||
updateAppShortcuts(shortcuts);
|
||||
});
|
||||
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');
|
||||
if (storedShortcuts) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
118
src/renderer/utils/shortcutKeyboard.ts
Normal file
118
src/renderer/utils/shortcutKeyboard.ts
Normal file
@@ -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"]'));
|
||||
}
|
||||
368
src/shared/shortcuts.ts
Normal file
368
src/shared/shortcuts.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
export const shortcutActionOrder = [
|
||||
'togglePlay',
|
||||
'prevPlay',
|
||||
'nextPlay',
|
||||
'volumeUp',
|
||||
'volumeDown',
|
||||
'toggleFavorite',
|
||||
'toggleWindow'
|
||||
] as const;
|
||||
|
||||
export type ShortcutAction = (typeof shortcutActionOrder)[number];
|
||||
export type ShortcutScope = 'global' | 'app';
|
||||
|
||||
export type ShortcutConfig = {
|
||||
key: string;
|
||||
enabled: boolean;
|
||||
scope: ShortcutScope;
|
||||
};
|
||||
|
||||
export type ShortcutsConfig = Record<ShortcutAction, ShortcutConfig>;
|
||||
|
||||
export type ShortcutPlatform = 'darwin' | 'win32' | 'linux' | 'web';
|
||||
|
||||
export type ShortcutConflict = {
|
||||
key: string;
|
||||
scope: ShortcutScope;
|
||||
actions: ShortcutAction[];
|
||||
};
|
||||
|
||||
type ShortcutModifier = 'CommandOrControl' | 'Alt' | 'Shift';
|
||||
|
||||
const shortcutModifierOrder: ShortcutModifier[] = ['CommandOrControl', 'Alt', 'Shift'];
|
||||
|
||||
const shortcutScopeDefaults: Record<ShortcutAction, ShortcutScope> = {
|
||||
togglePlay: 'global',
|
||||
prevPlay: 'global',
|
||||
nextPlay: 'global',
|
||||
volumeUp: 'app',
|
||||
volumeDown: 'app',
|
||||
toggleFavorite: 'app',
|
||||
toggleWindow: 'global'
|
||||
};
|
||||
|
||||
const shortcutKeyDefaults: Record<ShortcutAction, string> = {
|
||||
togglePlay: 'CommandOrControl+Alt+P',
|
||||
prevPlay: 'CommandOrControl+Alt+Left',
|
||||
nextPlay: 'CommandOrControl+Alt+Right',
|
||||
volumeUp: 'CommandOrControl+Alt+Up',
|
||||
volumeDown: 'CommandOrControl+Alt+Down',
|
||||
toggleFavorite: 'CommandOrControl+Alt+L',
|
||||
toggleWindow: 'CommandOrControl+Alt+Shift+M'
|
||||
};
|
||||
|
||||
const modifierAliases: Record<string, ShortcutModifier> = {
|
||||
commandorcontrol: 'CommandOrControl',
|
||||
cmdorctrl: 'CommandOrControl',
|
||||
cmd: 'CommandOrControl',
|
||||
command: 'CommandOrControl',
|
||||
control: 'CommandOrControl',
|
||||
ctrl: 'CommandOrControl',
|
||||
meta: 'CommandOrControl',
|
||||
super: 'CommandOrControl',
|
||||
win: 'CommandOrControl',
|
||||
windows: 'CommandOrControl',
|
||||
alt: 'Alt',
|
||||
option: 'Alt',
|
||||
shift: 'Shift'
|
||||
};
|
||||
|
||||
const namedKeyAliases: Record<string, string> = {
|
||||
left: 'Left',
|
||||
arrowleft: 'Left',
|
||||
right: 'Right',
|
||||
arrowright: 'Right',
|
||||
up: 'Up',
|
||||
arrowup: 'Up',
|
||||
down: 'Down',
|
||||
arrowdown: 'Down',
|
||||
esc: 'Escape',
|
||||
escape: 'Escape',
|
||||
enter: 'Enter',
|
||||
return: 'Enter',
|
||||
tab: 'Tab',
|
||||
space: 'Space',
|
||||
spacebar: 'Space',
|
||||
' ': 'Space',
|
||||
backspace: 'Backspace',
|
||||
delete: 'Delete',
|
||||
del: 'Delete',
|
||||
insert: 'Insert',
|
||||
ins: 'Insert',
|
||||
home: 'Home',
|
||||
end: 'End',
|
||||
pageup: 'PageUp',
|
||||
pagedown: 'PageDown',
|
||||
plus: 'Plus',
|
||||
'+': 'Plus',
|
||||
equal: 'Plus',
|
||||
'=': 'Plus',
|
||||
minus: 'Minus',
|
||||
'-': 'Minus',
|
||||
mediaplaypause: 'MediaPlayPause',
|
||||
mediaplay: 'MediaPlayPause',
|
||||
medianexttrack: 'MediaNextTrack',
|
||||
mediaprevioustrack: 'MediaPreviousTrack',
|
||||
mediastop: 'MediaStop'
|
||||
};
|
||||
|
||||
const allowedNamedKeys = new Set([
|
||||
'Left',
|
||||
'Right',
|
||||
'Up',
|
||||
'Down',
|
||||
'Escape',
|
||||
'Enter',
|
||||
'Tab',
|
||||
'Space',
|
||||
'Backspace',
|
||||
'Delete',
|
||||
'Insert',
|
||||
'Home',
|
||||
'End',
|
||||
'PageUp',
|
||||
'PageDown',
|
||||
'Plus',
|
||||
'Minus',
|
||||
'MediaPlayPause',
|
||||
'MediaNextTrack',
|
||||
'MediaPreviousTrack',
|
||||
'MediaStop'
|
||||
]);
|
||||
|
||||
const functionKeyRegExp = /^F([1-9]|1\d|2[0-4])$/i;
|
||||
|
||||
const shortcutActionGroups: Record<'playback' | 'sound' | 'window', ShortcutAction[]> = {
|
||||
playback: ['togglePlay', 'prevPlay', 'nextPlay'],
|
||||
sound: ['volumeUp', 'volumeDown', 'toggleFavorite'],
|
||||
window: ['toggleWindow']
|
||||
};
|
||||
|
||||
export const shortcutGroups = shortcutActionGroups;
|
||||
|
||||
const sharedReservedAccelerators = ['CommandOrControl+Shift+I', 'F5'];
|
||||
|
||||
const platformReservedAccelerators: Record<Exclude<ShortcutPlatform, 'web'>, string[]> = {
|
||||
darwin: ['CommandOrControl+Q', 'CommandOrControl+W', 'CommandOrControl+H'],
|
||||
win32: ['Alt+F4'],
|
||||
linux: ['Alt+F4']
|
||||
};
|
||||
|
||||
function normalizeModifierToken(token: string): ShortcutModifier | null {
|
||||
return modifierAliases[token.trim().toLowerCase()] ?? null;
|
||||
}
|
||||
|
||||
function normalizeKeyToken(token: string): string | null {
|
||||
const normalizedToken = token.trim();
|
||||
if (!normalizedToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (normalizedToken.length === 1 && /[A-Za-z0-9]/.test(normalizedToken)) {
|
||||
return normalizedToken.toUpperCase();
|
||||
}
|
||||
|
||||
const functionKeyMatch = normalizedToken.match(functionKeyRegExp);
|
||||
if (functionKeyMatch) {
|
||||
return `F${functionKeyMatch[1]}`;
|
||||
}
|
||||
|
||||
const aliasKey = namedKeyAliases[normalizedToken.toLowerCase()];
|
||||
if (aliasKey) {
|
||||
return aliasKey;
|
||||
}
|
||||
|
||||
if (allowedNamedKeys.has(normalizedToken)) {
|
||||
return normalizedToken;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function createDefaultShortcutConfig(action: ShortcutAction): ShortcutConfig {
|
||||
return {
|
||||
key: shortcutKeyDefaults[action],
|
||||
enabled: true,
|
||||
scope: shortcutScopeDefaults[action]
|
||||
};
|
||||
}
|
||||
|
||||
export function createDefaultShortcuts(): ShortcutsConfig {
|
||||
return shortcutActionOrder.reduce((result, action) => {
|
||||
result[action] = createDefaultShortcutConfig(action);
|
||||
return result;
|
||||
}, {} as ShortcutsConfig);
|
||||
}
|
||||
|
||||
export const defaultShortcuts = createDefaultShortcuts();
|
||||
|
||||
export function normalizeShortcutAccelerator(raw: string): string | null {
|
||||
if (!raw || typeof raw !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const segments = raw
|
||||
.split('+')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const modifiers = new Set<ShortcutModifier>();
|
||||
let mainKey: string | null = null;
|
||||
|
||||
for (const segment of segments) {
|
||||
const modifier = normalizeModifierToken(segment);
|
||||
if (modifier) {
|
||||
modifiers.add(modifier);
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedKey = normalizeKeyToken(segment);
|
||||
if (!normalizedKey || mainKey) {
|
||||
return null;
|
||||
}
|
||||
mainKey = normalizedKey;
|
||||
}
|
||||
|
||||
if (!mainKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orderedModifiers = shortcutModifierOrder.filter((modifier) => modifiers.has(modifier));
|
||||
return [...orderedModifiers, mainKey].join('+');
|
||||
}
|
||||
|
||||
function normalizeShortcutScope(scope: unknown, action: ShortcutAction): ShortcutScope {
|
||||
return scope === 'global' || scope === 'app' ? scope : shortcutScopeDefaults[action];
|
||||
}
|
||||
|
||||
function normalizeShortcutConfig(action: ShortcutAction, value: unknown): ShortcutConfig {
|
||||
if (typeof value === 'string') {
|
||||
const normalizedKey = normalizeShortcutAccelerator(value);
|
||||
return {
|
||||
key: normalizedKey ?? shortcutKeyDefaults[action],
|
||||
enabled: true,
|
||||
scope: shortcutScopeDefaults[action]
|
||||
};
|
||||
}
|
||||
|
||||
if (!value || typeof value !== 'object') {
|
||||
return createDefaultShortcutConfig(action);
|
||||
}
|
||||
|
||||
const config = value as Partial<ShortcutConfig>;
|
||||
const normalizedKey = normalizeShortcutAccelerator(config.key ?? '');
|
||||
|
||||
return {
|
||||
key: normalizedKey ?? shortcutKeyDefaults[action],
|
||||
enabled: config.enabled !== false,
|
||||
scope: normalizeShortcutScope(config.scope, action)
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeShortcutsConfig(input: unknown): ShortcutsConfig {
|
||||
const result = createDefaultShortcuts();
|
||||
|
||||
if (!input || typeof input !== 'object') {
|
||||
return result;
|
||||
}
|
||||
|
||||
const rawConfig = input as Record<string, unknown>;
|
||||
|
||||
shortcutActionOrder.forEach((action) => {
|
||||
result[action] = normalizeShortcutConfig(action, rawConfig[action]);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getShortcutConflicts(shortcuts: ShortcutsConfig): ShortcutConflict[] {
|
||||
const shortcutBuckets = new Map<string, ShortcutAction[]>();
|
||||
|
||||
shortcutActionOrder.forEach((action) => {
|
||||
const config = shortcuts[action];
|
||||
if (!config.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedKey = normalizeShortcutAccelerator(config.key);
|
||||
if (!normalizedKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bucketKey = `${config.scope}::${normalizedKey}`;
|
||||
const actions = shortcutBuckets.get(bucketKey) ?? [];
|
||||
actions.push(action);
|
||||
shortcutBuckets.set(bucketKey, actions);
|
||||
});
|
||||
|
||||
const conflicts: ShortcutConflict[] = [];
|
||||
|
||||
shortcutBuckets.forEach((actions, key) => {
|
||||
if (actions.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [scope, accelerator] = key.split('::') as [ShortcutScope, string];
|
||||
conflicts.push({
|
||||
key: accelerator,
|
||||
scope,
|
||||
actions
|
||||
});
|
||||
});
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
export function getReservedAccelerators(platform: ShortcutPlatform): string[] {
|
||||
if (platform === 'web') {
|
||||
return [...sharedReservedAccelerators];
|
||||
}
|
||||
|
||||
return [...sharedReservedAccelerators, ...platformReservedAccelerators[platform]];
|
||||
}
|
||||
|
||||
export function formatShortcutForDisplay(shortcut: string, platform: ShortcutPlatform): string {
|
||||
const accelerator = normalizeShortcutAccelerator(shortcut) ?? shortcut;
|
||||
const isMac = platform === 'darwin';
|
||||
|
||||
return accelerator
|
||||
.split('+')
|
||||
.map((segment) => {
|
||||
if (segment === 'CommandOrControl') {
|
||||
return isMac ? 'Cmd' : 'Ctrl';
|
||||
}
|
||||
if (segment === 'Alt') {
|
||||
return isMac ? 'Option' : 'Alt';
|
||||
}
|
||||
if (segment === 'Shift') {
|
||||
return 'Shift';
|
||||
}
|
||||
if (segment === 'Plus') {
|
||||
return '+';
|
||||
}
|
||||
if (segment === 'Minus') {
|
||||
return '-';
|
||||
}
|
||||
return segment;
|
||||
})
|
||||
.join(' + ');
|
||||
}
|
||||
|
||||
export function hasShortcutAction(action: string): action is ShortcutAction {
|
||||
return shortcutActionOrder.includes(action as ShortcutAction);
|
||||
}
|
||||
|
||||
export function isModifierOnlyShortcut(shortcut: string): boolean {
|
||||
const normalized = normalizeShortcutAccelerator(shortcut);
|
||||
if (!normalized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const segments = normalized.split('+');
|
||||
const mainKey = segments[segments.length - 1];
|
||||
return shortcutModifierOrder.includes(mainKey as ShortcutModifier);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"electron.vite.config.*",
|
||||
"src/main/**/*",
|
||||
"src/preload/**/*",
|
||||
"src/shared/**/*",
|
||||
"src/i18n/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"src/preload/*.d.ts",
|
||||
"src/renderer/**/*",
|
||||
"src/renderer/**/*.vue",
|
||||
"src/shared/**/*",
|
||||
"src/i18n/**/*",
|
||||
"src/main/modules/config.ts",
|
||||
"src/main/modules/shortcuts.ts"
|
||||
|
||||
Reference in New Issue
Block a user