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
|
.cursor
|
||||||
.windsurf
|
.windsurf
|
||||||
.agent
|
.agent
|
||||||
|
.agents
|
||||||
.claude
|
.claude
|
||||||
.kiro
|
.kiro
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
|||||||
@@ -383,28 +383,61 @@ export default {
|
|||||||
title: 'Shortcut Settings',
|
title: 'Shortcut Settings',
|
||||||
shortcut: 'Shortcut',
|
shortcut: 'Shortcut',
|
||||||
shortcutDesc: 'Customize global shortcuts',
|
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',
|
shortcutConflict: 'Shortcut Conflict',
|
||||||
inputPlaceholder: 'Click to input shortcut',
|
inputPlaceholder: 'Click to input shortcut',
|
||||||
|
clickToRecord: 'Click then press a shortcut',
|
||||||
|
recording: 'Recording...',
|
||||||
resetShortcuts: 'Reset',
|
resetShortcuts: 'Reset',
|
||||||
|
restoreSingle: 'Restore',
|
||||||
disableAll: 'Disable All',
|
disableAll: 'Disable All',
|
||||||
enableAll: 'Enable All',
|
enableAll: 'Enable All',
|
||||||
|
groups: {
|
||||||
|
playback: 'Playback',
|
||||||
|
sound: 'Volume & Favorite',
|
||||||
|
window: 'Window'
|
||||||
|
},
|
||||||
togglePlay: 'Play/Pause',
|
togglePlay: 'Play/Pause',
|
||||||
|
togglePlayDesc: 'Toggle current playback state',
|
||||||
prevPlay: 'Previous',
|
prevPlay: 'Previous',
|
||||||
|
prevPlayDesc: 'Play the previous track',
|
||||||
nextPlay: 'Next',
|
nextPlay: 'Next',
|
||||||
|
nextPlayDesc: 'Play the next track',
|
||||||
volumeUp: 'Volume Up',
|
volumeUp: 'Volume Up',
|
||||||
|
volumeUpDesc: 'Increase player volume',
|
||||||
volumeDown: 'Volume Down',
|
volumeDown: 'Volume Down',
|
||||||
|
volumeDownDesc: 'Decrease player volume',
|
||||||
toggleFavorite: 'Favorite/Unfavorite',
|
toggleFavorite: 'Favorite/Unfavorite',
|
||||||
|
toggleFavoriteDesc: 'Favorite or unfavorite current track',
|
||||||
toggleWindow: 'Show/Hide Window',
|
toggleWindow: 'Show/Hide Window',
|
||||||
|
toggleWindowDesc: 'Quickly show or hide the main window',
|
||||||
scopeGlobal: 'Global',
|
scopeGlobal: 'Global',
|
||||||
scopeApp: 'App Only',
|
scopeApp: 'App Only',
|
||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
disabled: 'Disabled',
|
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: {
|
messages: {
|
||||||
resetSuccess: 'Shortcuts reset successfully, please save',
|
resetSuccess: 'Shortcuts reset successfully, please save',
|
||||||
conflict: 'Shortcut conflict, please reset',
|
conflict: 'Shortcut conflict, please reset',
|
||||||
saveSuccess: 'Shortcuts saved successfully',
|
saveSuccess: 'Shortcuts saved successfully',
|
||||||
saveError: 'Failed to save shortcuts',
|
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',
|
cancelEdit: 'Edit cancelled',
|
||||||
|
clearToDisable: 'Shortcut disabled',
|
||||||
|
invalidShortcut: 'Invalid shortcut, please use a valid combination',
|
||||||
disableAll: 'All shortcuts disabled, please save to apply',
|
disableAll: 'All shortcuts disabled, please save to apply',
|
||||||
enableAll: 'All shortcuts enabled, please save to apply'
|
enableAll: 'All shortcuts enabled, please save to apply'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,28 +382,61 @@ export default {
|
|||||||
title: 'ショートカット設定',
|
title: 'ショートカット設定',
|
||||||
shortcut: 'ショートカット',
|
shortcut: 'ショートカット',
|
||||||
shortcutDesc: 'ショートカットをカスタマイズ',
|
shortcutDesc: 'ショートカットをカスタマイズ',
|
||||||
|
summaryReady: 'ショートカット設定は保存可能です',
|
||||||
|
summaryRecording: '新しいショートカットを記録中です',
|
||||||
|
summaryBlocked: '競合または無効な項目を修正してください',
|
||||||
|
platformHintMac: 'macOS では CommandOrControl は Cmd と表示されます',
|
||||||
|
platformHintWindows: 'Windows では CommandOrControl は Ctrl と表示されます',
|
||||||
|
platformHintLinux: 'Linux では CommandOrControl は Ctrl と表示されます',
|
||||||
|
platformHintGeneric: 'CommandOrControl はOSに応じて自動変換されます',
|
||||||
|
enabledCount: '有効',
|
||||||
|
recordingTip: '欄をクリックしてキー入力。Escでキャンセル、Deleteで無効化',
|
||||||
shortcutConflict: 'ショートカットの競合',
|
shortcutConflict: 'ショートカットの競合',
|
||||||
inputPlaceholder: 'クリックしてショートカットを入力',
|
inputPlaceholder: 'クリックしてショートカットを入力',
|
||||||
|
clickToRecord: 'クリックしてキーを入力',
|
||||||
|
recording: '記録中...',
|
||||||
resetShortcuts: 'デフォルトに戻す',
|
resetShortcuts: 'デフォルトに戻す',
|
||||||
|
restoreSingle: '復元',
|
||||||
disableAll: 'すべて無効',
|
disableAll: 'すべて無効',
|
||||||
enableAll: 'すべて有効',
|
enableAll: 'すべて有効',
|
||||||
|
groups: {
|
||||||
|
playback: '再生操作',
|
||||||
|
sound: '音量とお気に入り',
|
||||||
|
window: 'ウィンドウ'
|
||||||
|
},
|
||||||
togglePlay: '再生/一時停止',
|
togglePlay: '再生/一時停止',
|
||||||
|
togglePlayDesc: '現在の再生状態を切り替えます',
|
||||||
prevPlay: '前の曲',
|
prevPlay: '前の曲',
|
||||||
|
prevPlayDesc: '前の曲に切り替えます',
|
||||||
nextPlay: '次の曲',
|
nextPlay: '次の曲',
|
||||||
|
nextPlayDesc: '次の曲に切り替えます',
|
||||||
volumeUp: '音量を上げる',
|
volumeUp: '音量を上げる',
|
||||||
|
volumeUpDesc: 'プレイヤー音量を上げます',
|
||||||
volumeDown: '音量を下げる',
|
volumeDown: '音量を下げる',
|
||||||
|
volumeDownDesc: 'プレイヤー音量を下げます',
|
||||||
toggleFavorite: 'お気に入り/お気に入り解除',
|
toggleFavorite: 'お気に入り/お気に入り解除',
|
||||||
|
toggleFavoriteDesc: '現在の曲をお気に入り切り替えします',
|
||||||
toggleWindow: 'ウィンドウ表示/非表示',
|
toggleWindow: 'ウィンドウ表示/非表示',
|
||||||
|
toggleWindowDesc: 'メインウィンドウを表示/非表示にします',
|
||||||
scopeGlobal: 'グローバル',
|
scopeGlobal: 'グローバル',
|
||||||
scopeApp: 'アプリ内',
|
scopeApp: 'アプリ内',
|
||||||
enabled: '有効',
|
enabled: '有効',
|
||||||
disabled: '無効',
|
disabled: '無効',
|
||||||
|
issueInvalid: '無効な組み合わせ',
|
||||||
|
issueReserved: 'システム予約',
|
||||||
|
registrationWarningTitle: '以下のショートカットは登録できませんでした',
|
||||||
|
registrationOccupied: 'システムまたは他アプリで使用中',
|
||||||
|
registrationInvalid: 'ショートカット形式が無効',
|
||||||
messages: {
|
messages: {
|
||||||
resetSuccess: 'デフォルトのショートカットに戻しました。保存を忘れずに',
|
resetSuccess: 'デフォルトのショートカットに戻しました。保存を忘れずに',
|
||||||
conflict: '競合するショートカットがあります。再設定してください',
|
conflict: '競合するショートカットがあります。再設定してください',
|
||||||
saveSuccess: 'ショートカット設定を保存しました',
|
saveSuccess: 'ショートカット設定を保存しました',
|
||||||
saveError: 'ショートカットの保存に失敗しました。再試行してください',
|
saveError: 'ショートカットの保存に失敗しました。再試行してください',
|
||||||
|
saveValidationError: 'ショートカット検証に失敗しました。内容を確認してください',
|
||||||
|
partialRegistered: '保存しましたが、一部のグローバルショートカットは登録されませんでした',
|
||||||
cancelEdit: '変更をキャンセルしました',
|
cancelEdit: '変更をキャンセルしました',
|
||||||
|
clearToDisable: 'このショートカットを無効にしました',
|
||||||
|
invalidShortcut: '無効なショートカットです。有効な組み合わせを入力してください',
|
||||||
disableAll: 'すべてのショートカットを無効にしました。保存を忘れずに',
|
disableAll: 'すべてのショートカットを無効にしました。保存を忘れずに',
|
||||||
enableAll: 'すべてのショートカットを有効にしました。保存を忘れずに'
|
enableAll: 'すべてのショートカットを有効にしました。保存を忘れずに'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,28 +383,61 @@ export default {
|
|||||||
title: '단축키 설정',
|
title: '단축키 설정',
|
||||||
shortcut: '단축키',
|
shortcut: '단축키',
|
||||||
shortcutDesc: '단축키 사용자 정의',
|
shortcutDesc: '단축키 사용자 정의',
|
||||||
|
summaryReady: '단축키 구성이 저장 가능한 상태입니다',
|
||||||
|
summaryRecording: '새 단축키 조합을 입력 중입니다',
|
||||||
|
summaryBlocked: '충돌 또는 잘못된 항목을 먼저 수정하세요',
|
||||||
|
platformHintMac: 'macOS에서는 CommandOrControl이 Cmd로 표시됩니다',
|
||||||
|
platformHintWindows: 'Windows에서는 CommandOrControl이 Ctrl로 표시됩니다',
|
||||||
|
platformHintLinux: 'Linux에서는 CommandOrControl이 Ctrl로 표시됩니다',
|
||||||
|
platformHintGeneric: 'CommandOrControl은 운영체제에 맞게 자동 변환됩니다',
|
||||||
|
enabledCount: '활성화됨',
|
||||||
|
recordingTip: '필드를 클릭 후 조합키 입력, Esc 취소, Delete 비활성화',
|
||||||
shortcutConflict: '단축키 충돌',
|
shortcutConflict: '단축키 충돌',
|
||||||
inputPlaceholder: '클릭하여 단축키 입력',
|
inputPlaceholder: '클릭하여 단축키 입력',
|
||||||
|
clickToRecord: '클릭 후 단축키 입력',
|
||||||
|
recording: '입력 중...',
|
||||||
resetShortcuts: '기본값 복원',
|
resetShortcuts: '기본값 복원',
|
||||||
|
restoreSingle: '복원',
|
||||||
disableAll: '모두 비활성화',
|
disableAll: '모두 비활성화',
|
||||||
enableAll: '모두 활성화',
|
enableAll: '모두 활성화',
|
||||||
|
groups: {
|
||||||
|
playback: '재생 제어',
|
||||||
|
sound: '볼륨 및 즐겨찾기',
|
||||||
|
window: '창 제어'
|
||||||
|
},
|
||||||
togglePlay: '재생/일시정지',
|
togglePlay: '재생/일시정지',
|
||||||
|
togglePlayDesc: '현재 재생 상태를 전환합니다',
|
||||||
prevPlay: '이전 곡',
|
prevPlay: '이전 곡',
|
||||||
|
prevPlayDesc: '이전 곡으로 이동합니다',
|
||||||
nextPlay: '다음 곡',
|
nextPlay: '다음 곡',
|
||||||
|
nextPlayDesc: '다음 곡으로 이동합니다',
|
||||||
volumeUp: '볼륨 증가',
|
volumeUp: '볼륨 증가',
|
||||||
|
volumeUpDesc: '플레이어 볼륨을 높입니다',
|
||||||
volumeDown: '볼륨 감소',
|
volumeDown: '볼륨 감소',
|
||||||
|
volumeDownDesc: '플레이어 볼륨을 낮춥니다',
|
||||||
toggleFavorite: '즐겨찾기/즐겨찾기 취소',
|
toggleFavorite: '즐겨찾기/즐겨찾기 취소',
|
||||||
|
toggleFavoriteDesc: '현재 곡 즐겨찾기를 전환합니다',
|
||||||
toggleWindow: '창 표시/숨기기',
|
toggleWindow: '창 표시/숨기기',
|
||||||
|
toggleWindowDesc: '메인 창을 빠르게 표시/숨김합니다',
|
||||||
scopeGlobal: '전역',
|
scopeGlobal: '전역',
|
||||||
scopeApp: '앱 내',
|
scopeApp: '앱 내',
|
||||||
enabled: '활성화',
|
enabled: '활성화',
|
||||||
disabled: '비활성화',
|
disabled: '비활성화',
|
||||||
|
issueInvalid: '잘못된 조합',
|
||||||
|
issueReserved: '시스템 예약',
|
||||||
|
registrationWarningTitle: '다음 단축키는 등록되지 않았습니다',
|
||||||
|
registrationOccupied: '시스템 또는 다른 앱에서 사용 중',
|
||||||
|
registrationInvalid: '단축키 형식이 잘못됨',
|
||||||
messages: {
|
messages: {
|
||||||
resetSuccess: '기본 단축키로 복원되었습니다. 저장을 잊지 마세요',
|
resetSuccess: '기본 단축키로 복원되었습니다. 저장을 잊지 마세요',
|
||||||
conflict: '충돌하는 단축키가 있습니다. 다시 설정하세요',
|
conflict: '충돌하는 단축키가 있습니다. 다시 설정하세요',
|
||||||
saveSuccess: '단축키 설정이 저장되었습니다',
|
saveSuccess: '단축키 설정이 저장되었습니다',
|
||||||
saveError: '단축키 저장 실패, 다시 시도하세요',
|
saveError: '단축키 저장 실패, 다시 시도하세요',
|
||||||
|
saveValidationError: '단축키 검증에 실패했습니다. 설정을 확인하세요',
|
||||||
|
partialRegistered: '저장되었지만 일부 전역 단축키는 등록되지 않았습니다',
|
||||||
cancelEdit: '수정이 취소되었습니다',
|
cancelEdit: '수정이 취소되었습니다',
|
||||||
|
clearToDisable: '해당 단축키가 비활성화되었습니다',
|
||||||
|
invalidShortcut: '잘못된 단축키입니다. 유효한 조합을 입력하세요',
|
||||||
disableAll: '모든 단축키가 비활성화되었습니다. 저장을 잊지 마세요',
|
disableAll: '모든 단축키가 비활성화되었습니다. 저장을 잊지 마세요',
|
||||||
enableAll: '모든 단축키가 활성화되었습니다. 저장을 잊지 마세요'
|
enableAll: '모든 단축키가 활성화되었습니다. 저장을 잊지 마세요'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -380,28 +380,61 @@ export default {
|
|||||||
title: '快捷键设置',
|
title: '快捷键设置',
|
||||||
shortcut: '快捷键',
|
shortcut: '快捷键',
|
||||||
shortcutDesc: '自定义快捷键',
|
shortcutDesc: '自定义快捷键',
|
||||||
|
summaryReady: '当前快捷键配置可保存',
|
||||||
|
summaryRecording: '正在录制新的快捷键组合',
|
||||||
|
summaryBlocked: '存在冲突或无效项,请先修正',
|
||||||
|
platformHintMac: 'macOS 下 CommandOrControl 会显示为 Cmd',
|
||||||
|
platformHintWindows: 'Windows 下 CommandOrControl 会显示为 Ctrl',
|
||||||
|
platformHintLinux: 'Linux 下 CommandOrControl 会显示为 Ctrl',
|
||||||
|
platformHintGeneric: '不同系统下 CommandOrControl 会自动适配',
|
||||||
|
enabledCount: '已启用',
|
||||||
|
recordingTip: '点击快捷键框后按下组合键,Esc 取消,Delete 可禁用该项',
|
||||||
shortcutConflict: '快捷键冲突',
|
shortcutConflict: '快捷键冲突',
|
||||||
inputPlaceholder: '点击输入快捷键',
|
inputPlaceholder: '点击输入快捷键',
|
||||||
|
clickToRecord: '点击后按下组合键',
|
||||||
|
recording: '录制中...',
|
||||||
resetShortcuts: '恢复默认',
|
resetShortcuts: '恢复默认',
|
||||||
|
restoreSingle: '恢复',
|
||||||
disableAll: '全部禁用',
|
disableAll: '全部禁用',
|
||||||
enableAll: '全部启用',
|
enableAll: '全部启用',
|
||||||
|
groups: {
|
||||||
|
playback: '播放控制',
|
||||||
|
sound: '音量与收藏',
|
||||||
|
window: '窗口控制'
|
||||||
|
},
|
||||||
togglePlay: '播放/暂停',
|
togglePlay: '播放/暂停',
|
||||||
|
togglePlayDesc: '切换当前歌曲播放状态',
|
||||||
prevPlay: '上一首',
|
prevPlay: '上一首',
|
||||||
|
prevPlayDesc: '切换到上一首歌曲',
|
||||||
nextPlay: '下一首',
|
nextPlay: '下一首',
|
||||||
|
nextPlayDesc: '切换到下一首歌曲',
|
||||||
volumeUp: '音量增加',
|
volumeUp: '音量增加',
|
||||||
|
volumeUpDesc: '提高播放器音量',
|
||||||
volumeDown: '音量减少',
|
volumeDown: '音量减少',
|
||||||
|
volumeDownDesc: '降低播放器音量',
|
||||||
toggleFavorite: '收藏/取消收藏',
|
toggleFavorite: '收藏/取消收藏',
|
||||||
|
toggleFavoriteDesc: '收藏或取消当前歌曲',
|
||||||
toggleWindow: '显示/隐藏窗口',
|
toggleWindow: '显示/隐藏窗口',
|
||||||
|
toggleWindowDesc: '快速显示或隐藏主窗口',
|
||||||
scopeGlobal: '全局',
|
scopeGlobal: '全局',
|
||||||
scopeApp: '应用内',
|
scopeApp: '应用内',
|
||||||
enabled: '启用',
|
enabled: '启用',
|
||||||
disabled: '禁用',
|
disabled: '禁用',
|
||||||
|
issueInvalid: '组合无效',
|
||||||
|
issueReserved: '系统保留',
|
||||||
|
registrationWarningTitle: '以下快捷键未能注册,请更换组合后重试',
|
||||||
|
registrationOccupied: '被系统或其他应用占用',
|
||||||
|
registrationInvalid: '键位格式无效',
|
||||||
messages: {
|
messages: {
|
||||||
resetSuccess: '已恢复默认快捷键,请记得保存',
|
resetSuccess: '已恢复默认快捷键,请记得保存',
|
||||||
conflict: '存在冲突的快捷键,请重新设置',
|
conflict: '存在冲突的快捷键,请重新设置',
|
||||||
saveSuccess: '快捷键设置已保存',
|
saveSuccess: '快捷键设置已保存',
|
||||||
saveError: '保存快捷键失败,请重试',
|
saveError: '保存快捷键失败,请重试',
|
||||||
|
saveValidationError: '快捷键校验未通过,请检查后重试',
|
||||||
|
partialRegistered: '已保存,但部分全局快捷键未注册成功',
|
||||||
cancelEdit: '已取消修改',
|
cancelEdit: '已取消修改',
|
||||||
|
clearToDisable: '已禁用该快捷键',
|
||||||
|
invalidShortcut: '快捷键无效,请输入有效组合',
|
||||||
disableAll: '已禁用所有快捷键,请记得保存',
|
disableAll: '已禁用所有快捷键,请记得保存',
|
||||||
enableAll: '已启用所有快捷键,请记得保存'
|
enableAll: '已启用所有快捷键,请记得保存'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,28 +377,61 @@ export default {
|
|||||||
title: '快捷鍵設定',
|
title: '快捷鍵設定',
|
||||||
shortcut: '快捷鍵',
|
shortcut: '快捷鍵',
|
||||||
shortcutDesc: '自訂快捷鍵',
|
shortcutDesc: '自訂快捷鍵',
|
||||||
|
summaryReady: '目前快捷鍵設定可直接儲存',
|
||||||
|
summaryRecording: '正在錄製新的快捷鍵組合',
|
||||||
|
summaryBlocked: '存在衝突或無效項目,請先修正',
|
||||||
|
platformHintMac: 'macOS 下 CommandOrControl 會顯示為 Cmd',
|
||||||
|
platformHintWindows: 'Windows 下 CommandOrControl 會顯示為 Ctrl',
|
||||||
|
platformHintLinux: 'Linux 下 CommandOrControl 會顯示為 Ctrl',
|
||||||
|
platformHintGeneric: 'CommandOrControl 會依系統自動適配',
|
||||||
|
enabledCount: '已啟用',
|
||||||
|
recordingTip: '點擊快捷鍵欄位後輸入組合鍵,Esc 取消,Delete 可停用',
|
||||||
shortcutConflict: '快捷鍵衝突',
|
shortcutConflict: '快捷鍵衝突',
|
||||||
inputPlaceholder: '點擊輸入快捷鍵',
|
inputPlaceholder: '點擊輸入快捷鍵',
|
||||||
|
clickToRecord: '點擊後輸入快捷鍵',
|
||||||
|
recording: '錄製中...',
|
||||||
resetShortcuts: '恢復預設',
|
resetShortcuts: '恢復預設',
|
||||||
|
restoreSingle: '恢復',
|
||||||
disableAll: '全部停用',
|
disableAll: '全部停用',
|
||||||
enableAll: '全部啟用',
|
enableAll: '全部啟用',
|
||||||
|
groups: {
|
||||||
|
playback: '播放控制',
|
||||||
|
sound: '音量與收藏',
|
||||||
|
window: '視窗控制'
|
||||||
|
},
|
||||||
togglePlay: '播放/暫停',
|
togglePlay: '播放/暫停',
|
||||||
|
togglePlayDesc: '切換目前歌曲播放狀態',
|
||||||
prevPlay: '上一首',
|
prevPlay: '上一首',
|
||||||
|
prevPlayDesc: '切換到上一首歌曲',
|
||||||
nextPlay: '下一首',
|
nextPlay: '下一首',
|
||||||
|
nextPlayDesc: '切換到下一首歌曲',
|
||||||
volumeUp: '增加音量',
|
volumeUp: '增加音量',
|
||||||
|
volumeUpDesc: '提高播放器音量',
|
||||||
volumeDown: '減少音量',
|
volumeDown: '減少音量',
|
||||||
|
volumeDownDesc: '降低播放器音量',
|
||||||
toggleFavorite: '收藏/取消收藏',
|
toggleFavorite: '收藏/取消收藏',
|
||||||
|
toggleFavoriteDesc: '收藏或取消目前歌曲',
|
||||||
toggleWindow: '顯示/隱藏視窗',
|
toggleWindow: '顯示/隱藏視窗',
|
||||||
|
toggleWindowDesc: '快速顯示或隱藏主視窗',
|
||||||
scopeGlobal: '全域',
|
scopeGlobal: '全域',
|
||||||
scopeApp: '應用程式內',
|
scopeApp: '應用程式內',
|
||||||
enabled: '已啟用',
|
enabled: '已啟用',
|
||||||
disabled: '已停用',
|
disabled: '已停用',
|
||||||
|
issueInvalid: '組合無效',
|
||||||
|
issueReserved: '系統保留',
|
||||||
|
registrationWarningTitle: '以下快捷鍵未能註冊,請改用其他組合',
|
||||||
|
registrationOccupied: '被系統或其他應用程式占用',
|
||||||
|
registrationInvalid: '鍵位格式無效',
|
||||||
messages: {
|
messages: {
|
||||||
resetSuccess: '已恢復預設快捷鍵,請記得儲存',
|
resetSuccess: '已恢復預設快捷鍵,請記得儲存',
|
||||||
conflict: '存在快捷鍵衝突,請重新設定',
|
conflict: '存在快捷鍵衝突,請重新設定',
|
||||||
saveSuccess: '快捷鍵設定已儲存',
|
saveSuccess: '快捷鍵設定已儲存',
|
||||||
saveError: '快捷鍵儲存失敗,請重試',
|
saveError: '快捷鍵儲存失敗,請重試',
|
||||||
|
saveValidationError: '快捷鍵校驗未通過,請檢查後重試',
|
||||||
|
partialRegistered: '已儲存,但部分全域快捷鍵未註冊成功',
|
||||||
cancelEdit: '已取消修改',
|
cancelEdit: '已取消修改',
|
||||||
|
clearToDisable: '已停用該快捷鍵',
|
||||||
|
invalidShortcut: '快捷鍵無效,請輸入有效組合',
|
||||||
disableAll: '已停用所有快捷鍵,請記得儲存',
|
disableAll: '已停用所有快捷鍵,請記得儲存',
|
||||||
enableAll: '已啟用所有快捷鍵,請記得儲存'
|
enableAll: '已啟用所有快捷鍵,請記得儲存'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { initializeLoginWindow } from './modules/loginWindow';
|
|||||||
import { initLxMusicHttp } from './modules/lxMusicHttp';
|
import { initLxMusicHttp } from './modules/lxMusicHttp';
|
||||||
import { initializeOtherApi } from './modules/otherApi';
|
import { initializeOtherApi } from './modules/otherApi';
|
||||||
import { initializeRemoteControl } from './modules/remoteControl';
|
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 { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray';
|
||||||
import { setupUpdateHandlers } from './modules/update';
|
import { setupUpdateHandlers } from './modules/update';
|
||||||
import { createMainWindow, initializeWindowManager, setAppQuitting } from './modules/window';
|
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) => {
|
ipcMain.on('change-language', (_, locale: Language) => {
|
||||||
// 更新主进程的语言设置
|
// 更新主进程的语言设置
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
|
|
||||||
|
import { createDefaultShortcuts, type ShortcutsConfig } from '../../shared/shortcuts';
|
||||||
import set from '../set.json';
|
import set from '../set.json';
|
||||||
import { defaultShortcuts } from './shortcuts';
|
|
||||||
|
|
||||||
type SetConfig = {
|
type SetConfig = {
|
||||||
isProxy: boolean;
|
isProxy: boolean;
|
||||||
@@ -29,7 +29,7 @@ type SetConfig = {
|
|||||||
};
|
};
|
||||||
interface StoreType {
|
interface StoreType {
|
||||||
set: SetConfig;
|
set: SetConfig;
|
||||||
shortcuts: typeof defaultShortcuts;
|
shortcuts: ShortcutsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
let store: Store<StoreType>;
|
let store: Store<StoreType>;
|
||||||
@@ -42,7 +42,7 @@ export function initializeConfig() {
|
|||||||
name: 'config',
|
name: 'config',
|
||||||
defaults: {
|
defaults: {
|
||||||
set: set as SetConfig,
|
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';
|
import { getStore } from './config';
|
||||||
|
|
||||||
// 添加获取平台信息的 IPC 处理程序
|
type ShortcutRegistrationFailureReason = 'invalid' | 'occupied';
|
||||||
ipcMain.on('get-platform', (event) => {
|
|
||||||
event.returnValue = process.platform;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 定义快捷键配置接口
|
type ShortcutRegistrationFailure = {
|
||||||
export interface ShortcutConfig {
|
action: ShortcutAction;
|
||||||
key: string;
|
key: string;
|
||||||
enabled: boolean;
|
reason: ShortcutRegistrationFailureReason;
|
||||||
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' }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mainWindowRef: Electron.BrowserWindow | null = null;
|
type ShortcutRegistrationResult = {
|
||||||
|
success: boolean;
|
||||||
|
failed: ShortcutRegistrationFailure[];
|
||||||
|
};
|
||||||
|
|
||||||
// 注册快捷键
|
type ShortcutValidationReason = 'invalid' | 'conflict' | 'reserved';
|
||||||
export function registerShortcuts(
|
|
||||||
mainWindow: Electron.BrowserWindow,
|
type ShortcutValidationIssue = {
|
||||||
shortcutsConfig?: ShortcutsConfig
|
action: ShortcutAction;
|
||||||
) {
|
key: string;
|
||||||
mainWindowRef = mainWindow;
|
scope: ShortcutScope;
|
||||||
|
reason: ShortcutValidationReason;
|
||||||
|
conflictWith?: ShortcutAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ShortcutValidationResult = {
|
||||||
|
shortcuts: ShortcutsConfig;
|
||||||
|
hasBlockingIssue: boolean;
|
||||||
|
issues: ShortcutValidationIssue[];
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoredShortcuts(): ShortcutsConfig {
|
||||||
const store = getStore();
|
const store = getStore();
|
||||||
const shortcuts =
|
const rawShortcuts = store.get('shortcuts');
|
||||||
shortcutsConfig || (store.get('shortcuts') as ShortcutsConfig) || defaultShortcuts;
|
const normalizedShortcuts = normalizeShortcutsConfig(rawShortcuts);
|
||||||
|
|
||||||
// 注销所有已注册的快捷键
|
const serializedRaw = JSON.stringify(rawShortcuts ?? null);
|
||||||
globalShortcut.unregisterAll();
|
const serializedNormalized = JSON.stringify(normalizedShortcuts);
|
||||||
|
|
||||||
// 对旧格式数据进行兼容处理
|
if (serializedRaw !== serializedNormalized) {
|
||||||
if (shortcuts && typeof shortcuts.togglePlay === 'string') {
|
store.set('shortcuts', normalizedShortcuts);
|
||||||
// 将 shortcuts 强制转换为 unknown,再转为 Record<string, string>
|
}
|
||||||
const oldShortcuts = { ...shortcuts } as unknown as Record<string, string>;
|
|
||||||
const newShortcuts: ShortcutsConfig = {};
|
|
||||||
|
|
||||||
Object.entries(oldShortcuts).forEach(([key, value]) => {
|
return normalizedShortcuts;
|
||||||
newShortcuts[key] = {
|
}
|
||||||
key: value,
|
|
||||||
enabled: true,
|
|
||||||
scope: ['volumeUp', 'volumeDown', 'toggleFavorite'].includes(key) ? 'app' : 'global'
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
store.set('shortcuts', newShortcuts);
|
function persistShortcuts(shortcuts: ShortcutsConfig) {
|
||||||
registerShortcuts(mainWindow, newShortcuts);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册全局快捷键
|
mainWindowRef!.webContents.send('update-app-shortcuts', shortcuts);
|
||||||
Object.entries(shortcuts).forEach(([action, config]) => {
|
mainWindowRef!.webContents.send('shortcuts-updated', shortcuts, registration);
|
||||||
const { key, enabled, scope } = config as ShortcutConfig;
|
}
|
||||||
|
|
||||||
// 只注册启用且作用域为全局的快捷键
|
function unregisterManagedGlobalShortcuts() {
|
||||||
if (!enabled || scope !== 'global') return;
|
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 {
|
try {
|
||||||
switch (action) {
|
const registered = globalShortcut.register(accelerator, () => {
|
||||||
case 'toggleWindow':
|
handleShortcutAction(action);
|
||||||
globalShortcut.register(key, () => {
|
});
|
||||||
if (mainWindow.isVisible()) {
|
|
||||||
mainWindow.hide();
|
if (!registered) {
|
||||||
} else {
|
failed.push({
|
||||||
mainWindow.show();
|
action,
|
||||||
}
|
key: accelerator,
|
||||||
});
|
reason: 'occupied'
|
||||||
break;
|
});
|
||||||
default:
|
return;
|
||||||
globalShortcut.register(key, () => {
|
|
||||||
mainWindow.webContents.send('global-shortcut', action);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
managedGlobalShortcuts.set(action, accelerator);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`注册快捷键 ${key} 失败:`, error);
|
console.error(`[Shortcuts] 注册快捷键失败: ${accelerator}`, error);
|
||||||
|
failed.push({
|
||||||
|
action,
|
||||||
|
key: accelerator,
|
||||||
|
reason: 'invalid'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 通知渲染进程更新应用内快捷键
|
return {
|
||||||
mainWindow.webContents.send('update-app-shortcuts', shortcuts);
|
success: failed.length === 0,
|
||||||
|
failed
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化快捷键
|
function validateShortcuts(rawShortcuts: unknown): ShortcutValidationResult {
|
||||||
export function initializeShortcuts(mainWindow: Electron.BrowserWindow) {
|
const shortcuts = normalizeShortcutsConfig(rawShortcuts);
|
||||||
mainWindowRef = mainWindow;
|
const issues: ShortcutValidationIssue[] = [];
|
||||||
registerShortcuts(mainWindow);
|
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', () => {
|
ipcMain.on('disable-shortcuts', () => {
|
||||||
globalShortcut.unregisterAll();
|
shortcutsEnabled = false;
|
||||||
|
unregisterManagedGlobalShortcuts();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听启用快捷键事件
|
|
||||||
ipcMain.on('enable-shortcuts', () => {
|
ipcMain.on('enable-shortcuts', () => {
|
||||||
if (mainWindowRef) {
|
shortcutsEnabled = true;
|
||||||
registerShortcuts(mainWindowRef);
|
const shortcuts = getStoredShortcuts();
|
||||||
}
|
applyShortcuts(shortcuts);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听快捷键更新事件
|
ipcMain.on('update-shortcuts', (_, shortcutsConfig: unknown) => {
|
||||||
ipcMain.on('update-shortcuts', (_, shortcutsConfig: ShortcutsConfig) => {
|
saveShortcuts(shortcutsConfig);
|
||||||
if (mainWindowRef) {
|
});
|
||||||
registerShortcuts(mainWindowRef, 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 App from './App.vue';
|
||||||
import directives from './directive';
|
import directives from './directive';
|
||||||
import { initAppShortcuts } from './utils/appShortcuts';
|
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
@@ -23,6 +22,3 @@ app.use(pinia);
|
|||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(i18n as any);
|
app.use(i18n as any);
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|
||||||
// 初始化应用内快捷键
|
|
||||||
initAppShortcuts();
|
|
||||||
|
|||||||
@@ -3,68 +3,61 @@ import { onMounted, onUnmounted } from 'vue';
|
|||||||
import i18n from '@/../i18n/renderer';
|
import i18n from '@/../i18n/renderer';
|
||||||
import { usePlayerStore, useSettingsStore } from '@/store';
|
import { usePlayerStore, useSettingsStore } from '@/store';
|
||||||
|
|
||||||
|
import {
|
||||||
|
hasShortcutAction,
|
||||||
|
normalizeShortcutAccelerator,
|
||||||
|
normalizeShortcutsConfig,
|
||||||
|
type ShortcutAction,
|
||||||
|
shortcutActionOrder,
|
||||||
|
type ShortcutsConfig
|
||||||
|
} from '../../shared/shortcuts';
|
||||||
import { isElectron } from '.';
|
import { isElectron } from '.';
|
||||||
|
import { isEditableTarget, keyboardEventToAccelerator } from './shortcutKeyboard';
|
||||||
import { showShortcutToast } from './shortcutToast';
|
import { showShortcutToast } from './shortcutToast';
|
||||||
|
|
||||||
// 添加一个简单的防抖机制
|
const ACTION_DELAY = 260;
|
||||||
let actionTimeout: NodeJS.Timeout | null = null;
|
|
||||||
const ACTION_DELAY = 300; // 毫秒
|
|
||||||
|
|
||||||
// 添加一个操作锁,记录最后一次操作的时间和动作
|
const actionTimestamps = new Map<ShortcutAction, number>();
|
||||||
let lastActionInfo = {
|
|
||||||
action: '',
|
|
||||||
timestamp: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ShortcutConfig {
|
|
||||||
key: string;
|
|
||||||
enabled: boolean;
|
|
||||||
scope: 'global' | 'app';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ShortcutsConfig {
|
|
||||||
[key: string]: ShortcutConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { t } = i18n.global;
|
const { t } = i18n.global;
|
||||||
|
|
||||||
// 全局存储快捷键配置
|
let appShortcuts: ShortcutsConfig = normalizeShortcutsConfig(null);
|
||||||
let appShortcuts: ShortcutsConfig = {};
|
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 快捷键动作
|
* @param action 快捷键动作
|
||||||
*/
|
*/
|
||||||
export async function handleShortcutAction(action: string) {
|
export async function handleShortcutAction(action: ShortcutAction) {
|
||||||
const now = Date.now();
|
if (shouldSkipAction(action)) {
|
||||||
|
|
||||||
// 如果存在未完成的动作,则忽略当前请求
|
|
||||||
if (actionTimeout) {
|
|
||||||
console.log('[AppShortcuts] 忽略快速连续的动作请求:', action);
|
|
||||||
return;
|
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 playerStore = usePlayerStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
@@ -81,11 +74,9 @@ export async function handleShortcutAction(action: string) {
|
|||||||
if (playerStore.play) {
|
if (playerStore.play) {
|
||||||
await playerStore.handlePause();
|
await playerStore.handlePause();
|
||||||
showToast(t('player.playBar.pause'), 'ri-pause-circle-line');
|
showToast(t('player.playBar.pause'), 'ri-pause-circle-line');
|
||||||
} else {
|
} else if (playerStore.playMusic?.id) {
|
||||||
if (playerStore.playMusic?.id) {
|
await playerStore.setPlay({ ...playerStore.playMusic });
|
||||||
await playerStore.setPlay({ ...playerStore.playMusic });
|
showToast(t('player.playBar.play'), 'ri-play-circle-line');
|
||||||
showToast(t('player.playBar.play'), 'ri-play-circle-line');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'prevPlay':
|
case 'prevPlay':
|
||||||
@@ -115,16 +106,19 @@ export async function handleShortcutAction(action: string) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'toggleFavorite': {
|
case 'toggleFavorite': {
|
||||||
const isFavorite = playerStore.favoriteList.includes(Number(playerStore.playMusic.id));
|
if (!playerStore.playMusic?.id) {
|
||||||
const numericId = Number(playerStore.playMusic.id);
|
return;
|
||||||
console.log(`[AppShortcuts] toggleFavorite 当前状态: ${isFavorite}, ID: ${numericId}`);
|
|
||||||
if (isFavorite) {
|
|
||||||
playerStore.removeFromFavorite(numericId);
|
|
||||||
console.log(`[AppShortcuts] 已从收藏中移除: ${numericId}`);
|
|
||||||
} else {
|
|
||||||
playerStore.addToFavorite(numericId);
|
|
||||||
console.log(`[AppShortcuts] 已添加到收藏: ${numericId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentSongId = Number(playerStore.playMusic.id);
|
||||||
|
const isFavorite = playerStore.favoriteList.includes(currentSongId);
|
||||||
|
|
||||||
|
if (isFavorite) {
|
||||||
|
playerStore.removeFromFavorite(currentSongId);
|
||||||
|
} else {
|
||||||
|
playerStore.addToFavorite(currentSongId);
|
||||||
|
}
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
isFavorite
|
isFavorite
|
||||||
? t('player.playBar.unFavorite', { name: playerStore.playMusic.name })
|
? t('player.playBar.unFavorite', { name: playerStore.playMusic.name })
|
||||||
@@ -134,119 +128,91 @@ export async function handleShortcutAction(action: string) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
console.log('未知的快捷键动作:', action);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`执行快捷键动作 ${action} 时出错:`, error);
|
console.error(`[AppShortcuts] 执行动作失败: ${action}`, error);
|
||||||
} finally {
|
|
||||||
// 确保在出错时也能清除超时
|
|
||||||
clearTimeout(actionTimeout);
|
|
||||||
actionTimeout = null;
|
|
||||||
console.log(
|
|
||||||
`[AppShortcuts] 动作完成: ${action}, 时间戳: ${Date.now()}, 耗时: ${Date.now() - now}ms`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查按键是否匹配快捷键
|
|
||||||
* @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) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
// 如果在输入框中则不处理快捷键
|
if (appShortcutsSuspended) {
|
||||||
if (['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(appShortcuts).forEach(([action, config]) => {
|
if (isEditableTarget(event.target)) {
|
||||||
if (config.enabled && config.scope === 'app' && matchShortcut(e, config.key)) {
|
return;
|
||||||
e.preventDefault();
|
}
|
||||||
handleShortcutAction(action);
|
|
||||||
|
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) {
|
export function updateAppShortcuts(shortcuts: unknown) {
|
||||||
appShortcuts = shortcuts;
|
appShortcuts = normalizeShortcutsConfig(shortcuts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAppShortcutsSuspended(suspended: boolean) {
|
||||||
|
appShortcutsSuspended = suspended;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化应用内快捷键
|
* 初始化应用内快捷键
|
||||||
*/
|
*/
|
||||||
export function initAppShortcuts() {
|
export function initAppShortcuts() {
|
||||||
if (isElectron) {
|
if (!isElectron || appShortcutsInitialized) {
|
||||||
// 监听全局快捷键事件
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
export function cleanupAppShortcuts() {
|
||||||
if (isElectron) {
|
if (!isElectron || !appShortcutsInitialized) {
|
||||||
// 移除全局事件监听
|
return;
|
||||||
window.electron.ipcRenderer.removeAllListeners('global-shortcut');
|
|
||||||
window.electron.ipcRenderer.removeAllListeners('update-app-shortcuts');
|
|
||||||
|
|
||||||
// 移除键盘事件监听
|
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.*",
|
"electron.vite.config.*",
|
||||||
"src/main/**/*",
|
"src/main/**/*",
|
||||||
"src/preload/**/*",
|
"src/preload/**/*",
|
||||||
|
"src/shared/**/*",
|
||||||
"src/i18n/**/*"
|
"src/i18n/**/*"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"src/preload/*.d.ts",
|
"src/preload/*.d.ts",
|
||||||
"src/renderer/**/*",
|
"src/renderer/**/*",
|
||||||
"src/renderer/**/*.vue",
|
"src/renderer/**/*.vue",
|
||||||
|
"src/shared/**/*",
|
||||||
"src/i18n/**/*",
|
"src/i18n/**/*",
|
||||||
"src/main/modules/config.ts",
|
"src/main/modules/config.ts",
|
||||||
"src/main/modules/shortcuts.ts"
|
"src/main/modules/shortcuts.ts"
|
||||||
|
|||||||
Reference in New Issue
Block a user