mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 14:50:50 +08:00
@@ -1,11 +1,12 @@
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils';
|
||||
import { app, globalShortcut, ipcMain, nativeImage } from 'electron';
|
||||
import { app, ipcMain, nativeImage } from 'electron';
|
||||
import { join } from 'path';
|
||||
|
||||
import { loadLyricWindow } from './lyric';
|
||||
import { initializeCacheManager } from './modules/cache';
|
||||
import { initializeConfig } from './modules/config';
|
||||
import { initializeFileManager } from './modules/fileManager';
|
||||
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
||||
import { initializeTray } from './modules/tray';
|
||||
import { createMainWindow, initializeWindowManager } from './modules/window';
|
||||
import { startMusicApi } from './server';
|
||||
@@ -44,6 +45,9 @@ function initialize() {
|
||||
|
||||
// 加载歌词窗口
|
||||
loadLyricWindow(ipcMain, mainWindow);
|
||||
|
||||
// 初始化快捷键
|
||||
initializeShortcuts(mainWindow);
|
||||
}
|
||||
|
||||
// 应用程序准备就绪时的处理
|
||||
@@ -65,15 +69,9 @@ app.whenReady().then(() => {
|
||||
});
|
||||
});
|
||||
|
||||
// 应用程序准备就绪后的快捷键设置
|
||||
app.on('ready', () => {
|
||||
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
// 监听快捷键更新
|
||||
ipcMain.on('update-shortcuts', () => {
|
||||
registerShortcuts(mainWindow);
|
||||
});
|
||||
|
||||
// 所有窗口关闭时的处理
|
||||
|
||||
@@ -2,6 +2,7 @@ import { app, ipcMain } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
|
||||
import set from '../set.json';
|
||||
import { defaultShortcuts } from './shortcuts';
|
||||
|
||||
interface StoreType {
|
||||
set: {
|
||||
@@ -12,6 +13,7 @@ interface StoreType {
|
||||
authorUrl: string;
|
||||
musicApiPort: number;
|
||||
};
|
||||
shortcuts: typeof defaultShortcuts;
|
||||
}
|
||||
|
||||
let store: Store<StoreType>;
|
||||
@@ -23,7 +25,8 @@ export function initializeConfig() {
|
||||
store = new Store<StoreType>({
|
||||
name: 'config',
|
||||
defaults: {
|
||||
set
|
||||
set,
|
||||
shortcuts: defaultShortcuts
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,3 +44,7 @@ export function initializeConfig() {
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
export function getStore() {
|
||||
return store;
|
||||
}
|
||||
|
||||
88
src/main/modules/shortcuts.ts
Normal file
88
src/main/modules/shortcuts.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { globalShortcut, ipcMain } from 'electron';
|
||||
|
||||
import { getStore } from './config';
|
||||
|
||||
// 添加获取平台信息的 IPC 处理程序
|
||||
ipcMain.on('get-platform', (event) => {
|
||||
event.returnValue = process.platform;
|
||||
});
|
||||
|
||||
// 定义默认快捷键
|
||||
export const defaultShortcuts = {
|
||||
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'
|
||||
};
|
||||
|
||||
let mainWindowRef: Electron.BrowserWindow | null = null;
|
||||
|
||||
// 注册快捷键
|
||||
export function registerShortcuts(mainWindow: Electron.BrowserWindow) {
|
||||
mainWindowRef = mainWindow;
|
||||
const store = getStore();
|
||||
const shortcuts = store.get('shortcuts');
|
||||
|
||||
// 注销所有已注册的快捷键
|
||||
globalShortcut.unregisterAll();
|
||||
|
||||
// 显示/隐藏主窗口
|
||||
globalShortcut.register(shortcuts.toggleWindow, () => {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
// 播放/暂停
|
||||
globalShortcut.register(shortcuts.togglePlay, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'togglePlay');
|
||||
});
|
||||
|
||||
// 上一首
|
||||
globalShortcut.register(shortcuts.prevPlay, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'prevPlay');
|
||||
});
|
||||
|
||||
// 下一首
|
||||
globalShortcut.register(shortcuts.nextPlay, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'nextPlay');
|
||||
});
|
||||
|
||||
// 音量增加
|
||||
globalShortcut.register(shortcuts.volumeUp, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'volumeUp');
|
||||
});
|
||||
|
||||
// 音量减少
|
||||
globalShortcut.register(shortcuts.volumeDown, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'volumeDown');
|
||||
});
|
||||
|
||||
// 收藏当前歌曲
|
||||
globalShortcut.register(shortcuts.toggleFavorite, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'toggleFavorite');
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化快捷键
|
||||
export function initializeShortcuts(mainWindow: Electron.BrowserWindow) {
|
||||
mainWindowRef = mainWindow;
|
||||
registerShortcuts(mainWindow);
|
||||
|
||||
// 监听禁用快捷键事件
|
||||
ipcMain.on('disable-shortcuts', () => {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// 监听启用快捷键事件
|
||||
ipcMain.on('enable-shortcuts', () => {
|
||||
if (mainWindowRef) {
|
||||
registerShortcuts(mainWindowRef);
|
||||
}
|
||||
});
|
||||
}
|
||||
1
src/renderer/components.d.ts
vendored
1
src/renderer/components.d.ts
vendored
@@ -11,6 +11,7 @@ declare module 'vue' {
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
|
||||
91
src/renderer/components/ShortcutToast.vue
Normal file
91
src/renderer/components/ShortcutToast.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<transition name="shortcut-toast">
|
||||
<div v-if="visible" class="shortcut-toast">
|
||||
<div class="shortcut-toast-content">
|
||||
<div class="shortcut-toast-icon">
|
||||
<i :class="icon"></i>
|
||||
</div>
|
||||
<div class="shortcut-toast-text">{{ text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, ref } from 'vue';
|
||||
|
||||
const visible = ref(false);
|
||||
const text = ref('');
|
||||
const icon = ref('');
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const show = (message: string, iconName: string) => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
text.value = message;
|
||||
icon.value = iconName;
|
||||
visible.value = true;
|
||||
|
||||
timer = setTimeout(() => {
|
||||
visible.value = false;
|
||||
// 在动画结束后触发销毁事件
|
||||
setTimeout(() => {
|
||||
emit('destroy');
|
||||
}, 300);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// 清理定时器
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['destroy']);
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
show
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shortcut-toast {
|
||||
@apply fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-[9999];
|
||||
@apply flex items-center justify-center;
|
||||
|
||||
&-content {
|
||||
@apply flex flex-col items-center gap-2 p-4 rounded-lg;
|
||||
@apply bg-light-200 bg-opacity-70 dark:bg-dark-200 dark:bg-opacity-90;
|
||||
@apply text-dark-100 dark:text-light-100;
|
||||
@apply shadow-lg backdrop-blur-sm;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
@apply text-3xl;
|
||||
}
|
||||
|
||||
&-text {
|
||||
@apply text-sm font-medium text-center;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-toast-enter-active,
|
||||
.shortcut-toast-leave-active {
|
||||
@apply transition-all duration-300;
|
||||
}
|
||||
|
||||
.shortcut-toast-enter-from,
|
||||
.shortcut-toast-leave-to {
|
||||
@apply opacity-0 scale-90;
|
||||
}
|
||||
|
||||
.shortcut-toast-enter-to,
|
||||
.shortcut-toast-leave-from {
|
||||
@apply opacity-100 scale-100;
|
||||
}
|
||||
</style>
|
||||
380
src/renderer/components/settings/ShortcutSettings.vue
Normal file
380
src/renderer/components/settings/ShortcutSettings.vue
Normal file
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="visible"
|
||||
preset="dialog"
|
||||
title="快捷键设置"
|
||||
:show-icon="false"
|
||||
style="width: 600px"
|
||||
@after-leave="handleAfterLeave"
|
||||
>
|
||||
<div class="shortcut-settings">
|
||||
<div class="shortcut-card">
|
||||
<div class="shortcut-content">
|
||||
<n-scrollbar>
|
||||
<n-space vertical>
|
||||
<div v-for="(shortcut, key) in tempShortcuts" :key="key" class="shortcut-item">
|
||||
<div class="shortcut-info">
|
||||
<span class="shortcut-label">{{ getShortcutLabel(key) }}</span>
|
||||
</div>
|
||||
<div class="shortcut-input">
|
||||
<n-input
|
||||
:value="formatShortcut(shortcut)"
|
||||
:status="duplicateKeys[key] ? 'error' : undefined"
|
||||
placeholder="点击输入快捷键"
|
||||
readonly
|
||||
@keydown="(e) => handleKeyDown(e, key)"
|
||||
@focus="() => startRecording(key)"
|
||||
@blur="stopRecording"
|
||||
/>
|
||||
<n-tooltip v-if="duplicateKeys[key]" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-icon class="error-icon" size="18">
|
||||
<i class="ri-error-warning-line"></i>
|
||||
</n-icon>
|
||||
</template>
|
||||
快捷键冲突
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
|
||||
<div class="shortcut-footer">
|
||||
<n-space justify="end">
|
||||
<n-button size="small" @click="handleCancel">取消</n-button>
|
||||
<n-button size="small" @click="resetShortcuts">恢复默认</n-button>
|
||||
<n-button type="primary" size="small" :disabled="hasConflict" @click="handleSave">
|
||||
保存
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { isElectron } from '@/utils';
|
||||
|
||||
interface Shortcuts {
|
||||
togglePlay: string;
|
||||
prevPlay: string;
|
||||
nextPlay: string;
|
||||
volumeUp: string;
|
||||
volumeDown: string;
|
||||
toggleFavorite: string;
|
||||
toggleWindow: string;
|
||||
}
|
||||
|
||||
const defaultShortcuts: Shortcuts = {
|
||||
togglePlay: 'CommandOrControl+Alt+P',
|
||||
prevPlay: 'Alt+Left',
|
||||
nextPlay: 'Alt+Right',
|
||||
volumeUp: 'Alt+Up',
|
||||
volumeDown: 'Alt+Down',
|
||||
toggleFavorite: 'CommandOrControl+Alt+L',
|
||||
toggleWindow: 'CommandOrControl+Alt+Shift+M'
|
||||
};
|
||||
|
||||
const shortcuts = ref<Shortcuts>(
|
||||
isElectron
|
||||
? window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts') || defaultShortcuts
|
||||
: { ...defaultShortcuts }
|
||||
);
|
||||
|
||||
// 临时存储编辑中的快捷键
|
||||
const tempShortcuts = ref<Shortcuts>({ ...shortcuts.value });
|
||||
|
||||
// 监听快捷键更新
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.on('shortcuts-updated', () => {
|
||||
const newShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts');
|
||||
if (newShortcuts) {
|
||||
shortcuts.value = newShortcuts;
|
||||
tempShortcuts.value = { ...newShortcuts };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 组件挂载时禁用快捷键
|
||||
onMounted(() => {
|
||||
if (isElectron) {
|
||||
// 禁用全局快捷键
|
||||
window.electron.ipcRenderer.send('disable-shortcuts');
|
||||
|
||||
const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts');
|
||||
console.log('storedShortcuts', storedShortcuts);
|
||||
if (storedShortcuts) {
|
||||
shortcuts.value = storedShortcuts;
|
||||
tempShortcuts.value = { ...storedShortcuts };
|
||||
} else {
|
||||
shortcuts.value = { ...defaultShortcuts };
|
||||
tempShortcuts.value = { ...defaultShortcuts };
|
||||
window.electron.ipcRenderer.send('set-store-value', 'shortcuts', defaultShortcuts);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const shortcutLabels: Record<keyof Shortcuts, string> = {
|
||||
togglePlay: '播放/暂停',
|
||||
prevPlay: '上一首',
|
||||
nextPlay: '下一首',
|
||||
volumeUp: '音量增加',
|
||||
volumeDown: '音量减少',
|
||||
toggleFavorite: '收藏/取消收藏',
|
||||
toggleWindow: '显示/隐藏窗口'
|
||||
};
|
||||
|
||||
const getShortcutLabel = (key: keyof Shortcuts) => shortcutLabels[key];
|
||||
|
||||
const isRecording = ref(false);
|
||||
const currentKey = ref<keyof Shortcuts | ''>('');
|
||||
const message = useMessage();
|
||||
|
||||
// 检查快捷键冲突
|
||||
const duplicateKeys = computed(() => {
|
||||
const result: Record<string, boolean> = {};
|
||||
const usedShortcuts = new Set<string>();
|
||||
|
||||
Object.entries(tempShortcuts.value).forEach(([key, shortcut]) => {
|
||||
if (usedShortcuts.has(shortcut)) {
|
||||
result[key] = true;
|
||||
} else {
|
||||
usedShortcuts.add(shortcut);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// 是否存在冲突
|
||||
const hasConflict = computed(() => Object.keys(duplicateKeys.value).length > 0);
|
||||
|
||||
const startRecording = (key: keyof Shortcuts) => {
|
||||
isRecording.value = true;
|
||||
currentKey.value = key;
|
||||
// 禁用全局快捷键
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.send('disable-shortcuts');
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
isRecording.value = false;
|
||||
currentKey.value = '';
|
||||
// 重新启用全局快捷键
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.send('enable-shortcuts');
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent, key: keyof Shortcuts) => {
|
||||
if (!isRecording.value || currentKey.value !== key) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const modifiers: string[] = [];
|
||||
|
||||
// 统一使用 CommandOrControl
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
modifiers.push('CommandOrControl');
|
||||
}
|
||||
if (e.altKey) modifiers.push('Alt');
|
||||
if (e.shiftKey) modifiers.push('Shift');
|
||||
|
||||
let keyName = e.key;
|
||||
|
||||
// 特殊按键处理
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
keyName = 'Left';
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
keyName = 'Right';
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
keyName = 'Up';
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
keyName = 'Down';
|
||||
break;
|
||||
case 'Control':
|
||||
case 'Alt':
|
||||
case 'Shift':
|
||||
case 'Meta':
|
||||
case 'Command':
|
||||
return; // 忽略单独的修饰键
|
||||
default:
|
||||
keyName = e.key.length === 1 ? e.key.toUpperCase() : e.key;
|
||||
}
|
||||
|
||||
if (!['Control', 'Alt', 'Shift', 'Meta', 'Command'].includes(keyName)) {
|
||||
tempShortcuts.value[key] = [...modifiers, keyName].join('+');
|
||||
}
|
||||
};
|
||||
|
||||
const resetShortcuts = () => {
|
||||
tempShortcuts.value = { ...defaultShortcuts };
|
||||
message.success('已恢复默认快捷键,请记得保存');
|
||||
};
|
||||
|
||||
const saveShortcuts = () => {
|
||||
if (hasConflict.value) {
|
||||
message.error('存在冲突的快捷键,请重新设置');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建一个新的 Shortcuts 对象
|
||||
const shortcutsToSave = cloneDeep(tempShortcuts.value);
|
||||
|
||||
shortcuts.value = shortcutsToSave;
|
||||
|
||||
if (isElectron) {
|
||||
try {
|
||||
// 先保存到 store
|
||||
window.electron.ipcRenderer.send('set-store-value', 'shortcuts', shortcutsToSave);
|
||||
// 然后更新快捷键
|
||||
window.electron.ipcRenderer.send('update-shortcuts');
|
||||
message.success('快捷键设置已保存');
|
||||
} catch (error) {
|
||||
console.error('保存快捷键失败:', error);
|
||||
message.error('保存快捷键失败,请重试');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
tempShortcuts.value = { ...shortcuts.value };
|
||||
message.info('已取消修改');
|
||||
emit('update:show', false);
|
||||
};
|
||||
|
||||
// 组件卸载时确保快捷键被重新启用
|
||||
onUnmounted(() => {
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.send('enable-shortcuts');
|
||||
}
|
||||
});
|
||||
|
||||
// 格式化快捷键显示
|
||||
const formatShortcut = (shortcut: string) => {
|
||||
const isMac = isElectron
|
||||
? window.electron.ipcRenderer.sendSync('get-platform') === 'darwin'
|
||||
: false;
|
||||
return shortcut
|
||||
.replace(/CommandOrControl/g, isMac ? '⌘' : 'Ctrl')
|
||||
.replace(/\+/g, ' + ')
|
||||
.replace(/Meta/g, isMac ? '⌘' : 'Win')
|
||||
.replace(/Control/g, isMac ? '⌃' : 'Ctrl')
|
||||
.replace(/Alt/g, isMac ? '⌥' : 'Alt')
|
||||
.replace(/Shift/g, isMac ? '⇧' : 'Shift')
|
||||
.replace(/ArrowUp/g, '↑')
|
||||
.replace(/ArrowDown/g, '↓')
|
||||
.replace(/ArrowLeft/g, '←')
|
||||
.replace(/ArrowRight/g, '→');
|
||||
};
|
||||
|
||||
const visible = ref(false);
|
||||
const emit = defineEmits(['update:show', 'change']);
|
||||
|
||||
// 接收外部的 show 属性
|
||||
const props = defineProps<{
|
||||
show?: boolean;
|
||||
}>();
|
||||
|
||||
// 监听 show 属性变化
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听内部 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:show', newVal);
|
||||
});
|
||||
|
||||
// 处理弹窗关闭后的事件
|
||||
const handleAfterLeave = () => {
|
||||
// 重置临时数据
|
||||
tempShortcuts.value = { ...shortcuts.value };
|
||||
};
|
||||
|
||||
// 处理取消按钮点击
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
cancelEdit();
|
||||
};
|
||||
|
||||
// 处理保存按钮点击
|
||||
const handleSave = () => {
|
||||
saveShortcuts();
|
||||
visible.value = false;
|
||||
emit('change', shortcuts.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shortcut-settings {
|
||||
height: 500px;
|
||||
|
||||
.shortcut-card {
|
||||
@apply flex flex-col h-full;
|
||||
|
||||
.shortcut-footer {
|
||||
@apply p-4 border-t border-gray-100 dark:border-gray-800;
|
||||
}
|
||||
|
||||
.shortcut-content {
|
||||
@apply flex-1 overflow-hidden;
|
||||
|
||||
:deep(.n-scrollbar) {
|
||||
@apply h-full;
|
||||
|
||||
.n-scrollbar-content {
|
||||
@apply p-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
@apply flex items-center justify-between p-3 rounded-lg transition-all mb-3;
|
||||
@apply bg-gray-50 dark:bg-gray-800;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.shortcut-info {
|
||||
@apply flex flex-col;
|
||||
|
||||
.shortcut-label {
|
||||
@apply text-base font-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-input {
|
||||
@apply flex items-center gap-2;
|
||||
min-width: 200px;
|
||||
|
||||
:deep(.n-input) {
|
||||
.n-input__input-el {
|
||||
@apply text-center font-mono;
|
||||
}
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
@apply text-red-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -154,7 +154,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
@@ -168,6 +168,7 @@ import {
|
||||
} from '@/hooks/MusicHook';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';
|
||||
import { showShortcutToast } from '@/utils/shortcutToast';
|
||||
|
||||
import MusicFull from './MusicFull.vue';
|
||||
|
||||
@@ -338,6 +339,52 @@ const handleArtistClick = (id: number) => {
|
||||
musicFullVisible.value = false;
|
||||
store.commit('setCurrentArtistId', id);
|
||||
};
|
||||
|
||||
// 添加全局快捷键处理
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.on('global-shortcut', (_, action: string) => {
|
||||
console.log('action', action);
|
||||
switch (action) {
|
||||
case 'togglePlay':
|
||||
playMusicEvent();
|
||||
showShortcutToast(
|
||||
store.state.play ? '开始播放' : '暂停播放',
|
||||
store.state.play ? 'ri-pause-circle-line' : 'ri-play-circle-line'
|
||||
);
|
||||
break;
|
||||
case 'prevPlay':
|
||||
handlePrev();
|
||||
showShortcutToast('上一首', 'ri-skip-back-line');
|
||||
break;
|
||||
case 'nextPlay':
|
||||
handleNext();
|
||||
showShortcutToast('下一首', 'ri-skip-forward-line');
|
||||
break;
|
||||
case 'volumeUp':
|
||||
if (volumeSlider.value < 100) {
|
||||
volumeSlider.value = Math.min(volumeSlider.value + 10, 100);
|
||||
showShortcutToast(`音量${volumeSlider.value}%`, 'ri-volume-up-line');
|
||||
}
|
||||
break;
|
||||
case 'volumeDown':
|
||||
if (volumeSlider.value > 0) {
|
||||
volumeSlider.value = Math.max(volumeSlider.value - 10, 0);
|
||||
showShortcutToast(`音量${volumeSlider.value}%`, 'ri-volume-down-line');
|
||||
}
|
||||
break;
|
||||
case 'toggleFavorite':
|
||||
toggleFavorite(new Event('click'));
|
||||
showShortcutToast(
|
||||
isFavorite.value ? `已收藏${playMusic.value.name}` : `已取消收藏${playMusic.value.name}`,
|
||||
isFavorite.value ? 'ri-heart-fill' : 'ri-heart-line'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.log('未知的快捷键动作:', action);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
40
src/renderer/utils/shortcutToast.ts
Normal file
40
src/renderer/utils/shortcutToast.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { createVNode, render } from 'vue';
|
||||
|
||||
import ShortcutToast from '@/components/ShortcutToast.vue';
|
||||
|
||||
let container: HTMLDivElement | null = null;
|
||||
let toastInstance: any = null;
|
||||
|
||||
export function showShortcutToast(message: string, iconName: string) {
|
||||
// 如果容器不存在,创建一个新的容器
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// 如果已经有实例,先销毁它
|
||||
if (toastInstance) {
|
||||
render(null, container);
|
||||
toastInstance = null;
|
||||
}
|
||||
|
||||
// 创建新的 toast 实例
|
||||
const vnode = createVNode(ShortcutToast, {
|
||||
onDestroy: () => {
|
||||
if (container) {
|
||||
render(null, container);
|
||||
document.body.removeChild(container);
|
||||
container = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染 toast
|
||||
render(vnode, container);
|
||||
toastInstance = vnode.component?.exposed;
|
||||
|
||||
// 显示 toast
|
||||
if (toastInstance) {
|
||||
toastInstance.show(message, iconName);
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,16 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isElectron" class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">快捷键设置</div>
|
||||
<div class="set-item-content">自定义全局快捷键</div>
|
||||
</div>
|
||||
<n-button type="primary" size="small" @click="showShortcutModal = true">配置</n-button>
|
||||
</div>
|
||||
|
||||
<shortcut-settings v-model:show="showShortcutModal" @change="handleShortcutsChange" />
|
||||
|
||||
<div v-if="isElectron" class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">重启</div>
|
||||
@@ -325,6 +335,7 @@ import localData from '@/../main/set.json';
|
||||
import Coffee from '@/components/Coffee.vue';
|
||||
import DonationList from '@/components/common/DonationList.vue';
|
||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||
import ShortcutSettings from '@/components/settings/ShortcutSettings.vue';
|
||||
import { isElectron } from '@/utils';
|
||||
import { openDirectory, selectDirectory } from '@/utils/fileOperation';
|
||||
import { checkUpdate, UpdateResult } from '@/utils/update';
|
||||
@@ -631,6 +642,12 @@ const clearCache = async () => {
|
||||
showClearCacheModal.value = false;
|
||||
selectedCacheTypes.value = [];
|
||||
};
|
||||
|
||||
const showShortcutModal = ref(false);
|
||||
|
||||
const handleShortcutsChange = (shortcuts: any) => {
|
||||
console.log('快捷键已更新:', shortcuts);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user