mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 06:30:49 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7efeb9492b | ||
|
|
055536eb5c | ||
|
|
14852fc8d3 | ||
|
|
9866e772df | ||
|
|
87ca0836b1 | ||
|
|
fa07c5a40c | ||
|
|
5dbfea240b | ||
|
|
c1344393c3 | ||
|
|
426888f77c | ||
|
|
45cbc15c0f | ||
|
|
072025a543 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,20 +1,9 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
## v3.6.0
|
## v3.7.1
|
||||||
### ✨ 新功能
|
|
||||||
- 添加歌手详情功能
|
|
||||||
- 优化音乐解析(添加更多音源 减少歌曲不匹配问题)
|
|
||||||
- 添加搜索记录
|
|
||||||
- 添加关闭动画功能
|
|
||||||
- 添加歌词缓存功能
|
|
||||||
- 优化音乐播放控制和系统控制功能
|
|
||||||
|
|
||||||
### 🐞 Bug修复
|
### 🐞 Bug修复
|
||||||
- 修复下载文件类型问题
|
- 修复单曲循环后点击歌词快进会有重复的声音播放
|
||||||
|
- 修复最小化点击后恢复窗口按空格会继续最小化
|
||||||
### 🎈 性能优化
|
|
||||||
- 优化页面样式
|
|
||||||
- 优化留言显示
|
|
||||||
|
|
||||||
|
|
||||||
## 咖啡☕️
|
## 咖啡☕️
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -1,17 +1,24 @@
|
|||||||
# Alger Music Player
|
# Alger Music Player
|
||||||
主要功能如下
|
主要功能如下
|
||||||
|
- 🎵 音乐推荐
|
||||||
- 音乐推荐
|
- 🔐 网易云账号登录与同步
|
||||||
- 网易云登录
|
- 📝 功能
|
||||||
- 播放历史 歌曲收藏
|
- 播放历史记录
|
||||||
- 桌面歌词
|
- 歌曲收藏管理
|
||||||
- 歌单 mv 搜索 专辑等功能
|
- 自定义快捷键配置
|
||||||
- 识别无法播放歌曲 并解析播放
|
- 🎨 界面与交互
|
||||||
- 主题切换 更新检测
|
- 沉浸式歌词显示(点击左下角封面进入)
|
||||||
- 本地服务 不依赖线上服务
|
- 独立桌面歌词窗口
|
||||||
- 可听周杰伦(搜索专辑)
|
- 明暗主题切换
|
||||||
- 支持歌曲下载(歌曲右键)
|
- 🎼 音乐功能
|
||||||
- 支持音质选择(网易云VIP)
|
- 支持歌单、MV、专辑等完整音乐服务
|
||||||
|
- 灰色音乐资源解析(基于 @unblockneteasemusic/server)
|
||||||
|
- 高品质音乐试听(需网易云VIP)
|
||||||
|
- 音乐文件下载(支持右键下载和批量下载)
|
||||||
|
- 🚀 技术特性
|
||||||
|
- 本地化服务,无需依赖在线API (基于 netease-cloud-music-api)
|
||||||
|
- 自动更新检测
|
||||||
|
- 全平台适配(Desktop & Web & Mobile Web)
|
||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
一个基于 electron typescript vue3 的桌面音乐播放器 适配 web端 桌面端 web移动端
|
一个基于 electron typescript vue3 的桌面音乐播放器 适配 web端 桌面端 web移动端
|
||||||
@@ -26,6 +33,7 @@ QQ群:789288579
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
|
|||||||
BIN
docs/image4.png
Normal file
BIN
docs/image4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "AlgerMusicPlayer",
|
"name": "AlgerMusicPlayer",
|
||||||
"version": "3.6.0",
|
"version": "3.7.1",
|
||||||
"description": "Alger Music Player",
|
"description": "Alger Music Player",
|
||||||
"author": "Alger <algerkc@qq.com>",
|
"author": "Alger <algerkc@qq.com>",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { electronApp, optimizer } from '@electron-toolkit/utils';
|
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 { join } from 'path';
|
||||||
|
|
||||||
import { loadLyricWindow } from './lyric';
|
import { loadLyricWindow } from './lyric';
|
||||||
import { initializeCacheManager } from './modules/cache';
|
import { initializeCacheManager } from './modules/cache';
|
||||||
import { initializeConfig } from './modules/config';
|
import { initializeConfig } from './modules/config';
|
||||||
import { initializeFileManager } from './modules/fileManager';
|
import { initializeFileManager } from './modules/fileManager';
|
||||||
|
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
||||||
import { initializeTray } from './modules/tray';
|
import { initializeTray } from './modules/tray';
|
||||||
import { createMainWindow, initializeWindowManager } from './modules/window';
|
import { createMainWindow, initializeWindowManager } from './modules/window';
|
||||||
import { startMusicApi } from './server';
|
import { startMusicApi } from './server';
|
||||||
@@ -44,6 +45,9 @@ function initialize() {
|
|||||||
|
|
||||||
// 加载歌词窗口
|
// 加载歌词窗口
|
||||||
loadLyricWindow(ipcMain, mainWindow);
|
loadLyricWindow(ipcMain, mainWindow);
|
||||||
|
|
||||||
|
// 初始化快捷键
|
||||||
|
initializeShortcuts(mainWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用程序准备就绪时的处理
|
// 应用程序准备就绪时的处理
|
||||||
@@ -65,15 +69,9 @@ app.whenReady().then(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 应用程序准备就绪后的快捷键设置
|
// 监听快捷键更新
|
||||||
app.on('ready', () => {
|
ipcMain.on('update-shortcuts', () => {
|
||||||
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
registerShortcuts(mainWindow);
|
||||||
if (mainWindow.isVisible()) {
|
|
||||||
mainWindow.hide();
|
|
||||||
} else {
|
|
||||||
mainWindow.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 所有窗口关闭时的处理
|
// 所有窗口关闭时的处理
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { app, ipcMain } from 'electron';
|
|||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
|
|
||||||
import set from '../set.json';
|
import set from '../set.json';
|
||||||
|
import { defaultShortcuts } from './shortcuts';
|
||||||
|
|
||||||
interface StoreType {
|
interface StoreType {
|
||||||
set: {
|
set: {
|
||||||
@@ -12,6 +13,7 @@ interface StoreType {
|
|||||||
authorUrl: string;
|
authorUrl: string;
|
||||||
musicApiPort: number;
|
musicApiPort: number;
|
||||||
};
|
};
|
||||||
|
shortcuts: typeof defaultShortcuts;
|
||||||
}
|
}
|
||||||
|
|
||||||
let store: Store<StoreType>;
|
let store: Store<StoreType>;
|
||||||
@@ -23,7 +25,8 @@ export function initializeConfig() {
|
|||||||
store = new Store<StoreType>({
|
store = new Store<StoreType>({
|
||||||
name: 'config',
|
name: 'config',
|
||||||
defaults: {
|
defaults: {
|
||||||
set
|
set,
|
||||||
|
shortcuts: defaultShortcuts
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,3 +44,7 @@ export function initializeConfig() {
|
|||||||
|
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStore() {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|||||||
@@ -317,8 +317,8 @@ async function downloadMusic(
|
|||||||
|
|
||||||
// 等待下载完成
|
// 等待下载完成
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
writer!.on('finish', resolve);
|
writer!.on('finish', () => resolve(undefined));
|
||||||
writer!.on('error', reject);
|
writer!.on('error', (error) => reject(error));
|
||||||
response.data.pipe(writer!);
|
response.data.pipe(writer!);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
32
src/renderer/components.d.ts
vendored
32
src/renderer/components.d.ts
vendored
@@ -7,38 +7,6 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
|
||||||
NBadge: typeof import('naive-ui')['NBadge']
|
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
|
||||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
|
||||||
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
|
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
|
||||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
|
||||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
|
||||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
|
||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
|
||||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
|
||||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
|
||||||
NForm: typeof import('naive-ui')['NForm']
|
|
||||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
|
||||||
NImage: typeof import('naive-ui')['NImage']
|
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
|
||||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
|
||||||
NLayout: typeof import('naive-ui')['NLayout']
|
|
||||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
|
||||||
NModal: typeof import('naive-ui')['NModal']
|
|
||||||
NPopover: typeof import('naive-ui')['NPopover']
|
|
||||||
NProgress: typeof import('naive-ui')['NProgress']
|
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
|
||||||
NSelect: typeof import('naive-ui')['NSelect']
|
|
||||||
NSlider: typeof import('naive-ui')['NSlider']
|
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
|
||||||
NSpin: typeof import('naive-ui')['NSpin']
|
|
||||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
|
||||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
|
||||||
NTabs: typeof import('naive-ui')['NTabs']
|
|
||||||
NTag: typeof import('naive-ui')['NTag']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
mask-closable
|
mask-closable
|
||||||
:style="{ backgroundColor: 'transparent' }"
|
:style="{ backgroundColor: 'transparent' }"
|
||||||
:to="`#layout-main`"
|
:to="`#layout-main`"
|
||||||
:z-index="9998"
|
:z-index="zIndex"
|
||||||
@mask-click="close"
|
@mask-click="close"
|
||||||
>
|
>
|
||||||
<div class="music-page">
|
<div class="music-page">
|
||||||
@@ -89,6 +89,7 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
zIndex?: number;
|
||||||
songList: any[];
|
songList: any[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
listInfo?: {
|
listInfo?: {
|
||||||
@@ -99,7 +100,8 @@ const props = withDefaults(
|
|||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
loading: false,
|
loading: false,
|
||||||
cover: true
|
cover: true,
|
||||||
|
zIndex: 9996
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
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>
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
v-for="album in albums"
|
v-for="album in albums"
|
||||||
:key="album.id"
|
:key="album.id"
|
||||||
shape="square"
|
shape="square"
|
||||||
|
:z-index="9998"
|
||||||
:item="{
|
:item="{
|
||||||
id: album.id,
|
id: album.id,
|
||||||
picUrl: album.picUrl,
|
picUrl: album.picUrl,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
:song-list="songList"
|
:song-list="songList"
|
||||||
:list-info="listInfo"
|
:list-info="listInfo"
|
||||||
:cover="false"
|
:cover="false"
|
||||||
|
:z-index="zIndex"
|
||||||
/>
|
/>
|
||||||
<mv-player
|
<mv-player
|
||||||
v-if="item.type === 'mv'"
|
v-if="item.type === 'mv'"
|
||||||
@@ -52,6 +53,7 @@ import MusicList from '../MusicList.vue';
|
|||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
shape?: 'square' | 'rectangle';
|
shape?: 'square' | 'rectangle';
|
||||||
|
zIndex?: number;
|
||||||
item: {
|
item: {
|
||||||
picUrl: string;
|
picUrl: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -328,11 +328,11 @@ const artists = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-like {
|
&-like {
|
||||||
@apply mr-2 cursor-pointer ml-4;
|
@apply mr-2 cursor-pointer ml-4 transition-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.like-active {
|
.like-active {
|
||||||
@apply text-red-500;
|
@apply text-red-500 dark:text-red-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-play {
|
&-play {
|
||||||
|
|||||||
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>
|
||||||
@@ -72,9 +72,13 @@ watch(
|
|||||||
const setupAudioListeners = () => {
|
const setupAudioListeners = () => {
|
||||||
let interval: any = null;
|
let interval: any = null;
|
||||||
|
|
||||||
|
// 清理所有事件监听器
|
||||||
|
audioService.clearAllListeners();
|
||||||
|
|
||||||
// 监听播放
|
// 监听播放
|
||||||
audioService.on('play', () => {
|
audioService.on('play', () => {
|
||||||
store.commit('setPlayMusic', true);
|
store.commit('setPlayMusic', true);
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
nowTime.value = sound.value?.seek() as number;
|
nowTime.value = sound.value?.seek() as number;
|
||||||
allTime.value = sound.value?.duration() as number;
|
allTime.value = sound.value?.duration() as number;
|
||||||
@@ -95,7 +99,10 @@ const setupAudioListeners = () => {
|
|||||||
// 监听暂停
|
// 监听暂停
|
||||||
audioService.on('pause', () => {
|
audioService.on('pause', () => {
|
||||||
store.commit('setPlayMusic', false);
|
store.commit('setPlayMusic', false);
|
||||||
clearInterval(interval);
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
}
|
||||||
if (isElectron && isLyricWindowOpen.value) {
|
if (isElectron && isLyricWindowOpen.value) {
|
||||||
sendLyricToWin();
|
sendLyricToWin();
|
||||||
}
|
}
|
||||||
@@ -103,9 +110,17 @@ const setupAudioListeners = () => {
|
|||||||
|
|
||||||
// 监听结束
|
// 监听结束
|
||||||
audioService.on('end', () => {
|
audioService.on('end', () => {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (store.state.playMode === 1) {
|
if (store.state.playMode === 1) {
|
||||||
// 单曲循环模式
|
// 单曲循环模式
|
||||||
sound.value?.play();
|
if (sound.value) {
|
||||||
|
sound.value.seek(0);
|
||||||
|
sound.value.play();
|
||||||
|
}
|
||||||
} else if (store.state.playMode === 2) {
|
} else if (store.state.playMode === 2) {
|
||||||
// 随机播放模式
|
// 随机播放模式
|
||||||
const { playList } = store.state;
|
const { playList } = store.state;
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useThrottleFn } from '@vueuse/core';
|
import { useThrottleFn } from '@vueuse/core';
|
||||||
import { useTemplateRef } from 'vue';
|
import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
import SongItem from '@/components/common/SongItem.vue';
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
@@ -168,6 +168,7 @@ import {
|
|||||||
} from '@/hooks/MusicHook';
|
} from '@/hooks/MusicHook';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';
|
import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';
|
||||||
|
import { showShortcutToast } from '@/utils/shortcutToast';
|
||||||
|
|
||||||
import MusicFull from './MusicFull.vue';
|
import MusicFull from './MusicFull.vue';
|
||||||
|
|
||||||
@@ -338,6 +339,52 @@ const handleArtistClick = (id: number) => {
|
|||||||
musicFullVisible.value = false;
|
musicFullVisible.value = false;
|
||||||
store.commit('setCurrentArtistId', id);
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -395,7 +442,7 @@ const handleArtistClick = (id: number) => {
|
|||||||
|
|
||||||
&-play {
|
&-play {
|
||||||
@apply flex justify-center items-center w-20 h-12 rounded-full mx-4 transition text-gray-500;
|
@apply flex justify-center items-center w-20 h-12 rounded-full mx-4 transition text-gray-500;
|
||||||
@apply bg-gray-100 bg-opacity-60 hover:bg-gray-200;
|
@apply bg-gray-100 bg-opacity-60 dark:bg-gray-800 dark:bg-opacity-60 hover:bg-gray-200;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
<div id="title-bar" @mousedown="drag">
|
<div id="title-bar" @mousedown="drag">
|
||||||
<div id="title">Alger Music</div>
|
<div id="title">Alger Music</div>
|
||||||
<div id="buttons">
|
<div id="buttons">
|
||||||
<button @click="minimize">
|
<div class="button" @click="minimize">
|
||||||
<i class="iconfont icon-minisize"></i>
|
<i class="iconfont icon-minisize"></i>
|
||||||
</button>
|
</div>
|
||||||
<button @click="close">
|
<div class="button" @click="close">
|
||||||
<i class="iconfont icon-close"></i>
|
<i class="iconfont icon-close"></i>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ const drag = (event: MouseEvent) => {
|
|||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.button {
|
||||||
@apply text-gray-600 dark:text-gray-400 hover:text-green-500;
|
@apply text-gray-600 dark:text-gray-400 hover:text-green-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,10 @@ class AudioService {
|
|||||||
this.updateMediaSessionPositionState();
|
this.updateMediaSessionPositionState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearAllListeners() {
|
||||||
|
this.callbacks = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const audioService = new AudioService();
|
export const audioService = new AudioService();
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,276 +1,289 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-scrollbar>
|
<div class="settings-container">
|
||||||
<div class="set-page">
|
<!-- 左侧导航栏 -->
|
||||||
<div class="set-item">
|
<div class="settings-nav">
|
||||||
<div>
|
|
||||||
<div class="set-item-title">主题模式</div>
|
|
||||||
<div class="set-item-content">切换日间/夜间主题</div>
|
|
||||||
</div>
|
|
||||||
<n-switch v-model:value="isDarkTheme">
|
|
||||||
<template #checked>
|
|
||||||
<i class="ri-moon-line"></i>
|
|
||||||
</template>
|
|
||||||
<template #unchecked>
|
|
||||||
<i class="ri-sun-line"></i>
|
|
||||||
</template>
|
|
||||||
</n-switch>
|
|
||||||
</div>
|
|
||||||
<!-- <div v-if="isElectron" class="set-item">
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">代理</div>
|
|
||||||
<div class="set-item-content">无法听音乐时打开</div>
|
|
||||||
</div>
|
|
||||||
<n-switch v-model:value="setData.isProxy" />
|
|
||||||
</div> -->
|
|
||||||
<div v-if="isElectron" class="set-item">
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">音乐API端口</div>
|
|
||||||
<div class="set-item-content">修改后需要重启应用</div>
|
|
||||||
</div>
|
|
||||||
<n-input-number v-model:value="setData.musicApiPort" />
|
|
||||||
</div>
|
|
||||||
<div class="set-item">
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">动画速度</div>
|
|
||||||
<div class="set-item-content">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<n-switch v-model:value="setData.noAnimate">
|
|
||||||
<template #checked>关闭</template>
|
|
||||||
<template #unchecked>开启</template>
|
|
||||||
</n-switch>
|
|
||||||
<span>是否开启动画</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-sm text-gray-400">{{ setData.animationSpeed }}x</span>
|
|
||||||
<div class="w-40">
|
|
||||||
<n-slider
|
|
||||||
v-model:value="setData.animationSpeed"
|
|
||||||
:min="0.1"
|
|
||||||
:max="3"
|
|
||||||
:step="0.1"
|
|
||||||
:marks="{
|
|
||||||
0.1: '极慢',
|
|
||||||
1: '正常',
|
|
||||||
3: '极快'
|
|
||||||
}"
|
|
||||||
:disabled="setData.noAnimate"
|
|
||||||
class="w-40"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="isElectron" class="set-item">
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">下载目录</div>
|
|
||||||
<div class="set-item-content">
|
|
||||||
{{ setData.downloadPath || '默认下载目录' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<n-button size="small" @click="openDownloadPath">打开目录</n-button>
|
|
||||||
<n-button size="small" @click="selectDownloadPath">修改目录</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="set-item">
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">版本</div>
|
|
||||||
<div class="set-item-content">
|
|
||||||
{{ updateInfo.currentVersion }}
|
|
||||||
<template v-if="updateInfo.hasUpdate">
|
|
||||||
<n-tag type="success" class="ml-2">发现新版本 {{ updateInfo.latestVersion }}</n-tag>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<n-button
|
|
||||||
:type="updateInfo.hasUpdate ? 'primary' : 'default'"
|
|
||||||
size="small"
|
|
||||||
:loading="checking"
|
|
||||||
@click="checkForUpdates(true)"
|
|
||||||
>
|
|
||||||
{{ checking ? '检查中...' : '检查更新' }}
|
|
||||||
</n-button>
|
|
||||||
<n-button
|
|
||||||
v-if="updateInfo.hasUpdate"
|
|
||||||
type="success"
|
|
||||||
size="small"
|
|
||||||
@click="openReleasePage"
|
|
||||||
>
|
|
||||||
前往更新
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="set-item cursor-pointer hover:text-green-500 hover:bg-green-950 transition-all"
|
v-for="section in settingSections"
|
||||||
@click="openAuthor"
|
:key="section.id"
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: currentSection === section.id }"
|
||||||
|
@click="scrollToSection(section.id)"
|
||||||
>
|
>
|
||||||
<div>
|
{{ section.title }}
|
||||||
<coffee>
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">作者</div>
|
|
||||||
<div class="set-item-content">algerkong 点个star🌟呗</div>
|
|
||||||
</div>
|
|
||||||
</coffee>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<n-button size="small" type="primary" @click="openAuthor"
|
|
||||||
><i class="ri-github-line"></i>前往github</n-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="set-item">
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">音质设置</div>
|
|
||||||
<div class="set-item-content">选择音乐播放音质(VIP)</div>
|
|
||||||
</div>
|
|
||||||
<n-select
|
|
||||||
v-model:value="setData.musicQuality"
|
|
||||||
:options="[
|
|
||||||
{ label: '标准', value: 'standard' },
|
|
||||||
{ label: '较高', value: 'higher' },
|
|
||||||
{ label: '极高', value: 'exhigh' },
|
|
||||||
{ label: '无损', value: 'lossless' },
|
|
||||||
{ label: 'Hi-Res', value: 'hires' },
|
|
||||||
{ label: '高清环绕声', value: 'jyeffect' },
|
|
||||||
{ label: '沉浸环绕声', value: 'sky' },
|
|
||||||
{ label: '杜比全景声', value: 'dolby' },
|
|
||||||
{ label: '超清母带', value: 'jymaster' }
|
|
||||||
]"
|
|
||||||
style="width: 160px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="isElectron" class="set-item">
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">关闭行为</div>
|
|
||||||
<div class="set-item-content">
|
|
||||||
{{ closeActionLabels[setData.closeAction] || '每次询问' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<n-select
|
|
||||||
v-model:value="setData.closeAction"
|
|
||||||
:options="[
|
|
||||||
{ label: '每次询问', value: 'ask' },
|
|
||||||
{ label: '最小化到托盘', value: 'minimize' },
|
|
||||||
{ label: '直接退出', value: 'close' }
|
|
||||||
]"
|
|
||||||
style="width: 160px"
|
|
||||||
/>
|
|
||||||
</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="restartApp">重启</n-button>
|
|
||||||
</div>
|
|
||||||
<!-- 缓存管理 -->
|
|
||||||
<!-- <n-card class="set-card" title="缓存管理">
|
|
||||||
<n-space vertical>
|
|
||||||
<n-button type="primary" @click="showClearCacheModal = true"> 清除缓存 </n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card> -->
|
|
||||||
<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="showClearCacheModal = true">
|
|
||||||
清除缓存
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="isElectron" class="set-item">
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">代理设置</div>
|
|
||||||
<div class="set-item-content">无法访问音乐时可以开启代理</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<n-switch v-model:value="setData.proxyConfig.enable">
|
|
||||||
<template #checked>开启</template>
|
|
||||||
<template #unchecked>关闭</template>
|
|
||||||
</n-switch>
|
|
||||||
<n-button size="small" @click="showProxyModal = true">配置</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="isElectron" class="set-item">
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">realIP</div>
|
|
||||||
<div class="set-item-content">
|
|
||||||
由于限制,此项目在国外使用会受到限制可使用realIP参数,传进国内IP解决,如:116.25.146.177
|
|
||||||
即可解决
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<n-switch v-model:value="setData.enableRealIP">
|
|
||||||
<template #checked>开启</template>
|
|
||||||
<template #unchecked>关闭</template>
|
|
||||||
</n-switch>
|
|
||||||
<n-input
|
|
||||||
v-if="setData.enableRealIP"
|
|
||||||
v-model:value="setData.realIP"
|
|
||||||
placeholder="realIP"
|
|
||||||
style="width: 200px"
|
|
||||||
@blur="validateAndSaveRealIP"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="p-4 bg-light dark:bg-dark rounded-lg mb-4 border border-gray-200 dark:border-gray-700 rounded-lg"
|
|
||||||
>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<div class="set-item-title">捐赠支持</div>
|
|
||||||
<div class="set-item-content">感谢您的支持,让我有动力能够持续改进</div>
|
|
||||||
</div>
|
|
||||||
<n-button text @click="toggleDonationList">
|
|
||||||
<template #icon>
|
|
||||||
<i :class="isDonationListVisible ? 'ri-eye-line' : 'ri-eye-off-line'" />
|
|
||||||
</template>
|
|
||||||
{{ isDonationListVisible ? '隐藏列表' : '显示列表' }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
<donation-list v-if="isDonationListVisible" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 清除缓存弹窗 -->
|
|
||||||
<n-modal
|
|
||||||
v-model:show="showClearCacheModal"
|
|
||||||
preset="dialog"
|
|
||||||
title="清除缓存"
|
|
||||||
positive-text="确认"
|
|
||||||
negative-text="取消"
|
|
||||||
@positive-click="clearCache"
|
|
||||||
@negative-click="
|
|
||||||
() => {
|
|
||||||
selectedCacheTypes = [];
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<n-space vertical>
|
|
||||||
<p>请选择要清除的缓存类型:</p>
|
|
||||||
<n-checkbox-group v-model:value="selectedCacheTypes">
|
|
||||||
<n-space vertical>
|
|
||||||
<n-checkbox
|
|
||||||
v-for="option in clearCacheOptions"
|
|
||||||
:key="option.key"
|
|
||||||
:value="option.key"
|
|
||||||
:label="option.label"
|
|
||||||
>
|
|
||||||
<template #default>
|
|
||||||
<div>
|
|
||||||
<div>{{ option.label }}</div>
|
|
||||||
<div class="text-gray-400 text-sm">{{ option.description }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-checkbox>
|
|
||||||
</n-space>
|
|
||||||
</n-checkbox-group>
|
|
||||||
</n-space>
|
|
||||||
</n-modal>
|
|
||||||
</div>
|
</div>
|
||||||
<play-bottom />
|
|
||||||
|
<!-- 右侧内容区 -->
|
||||||
|
<n-scrollbar ref="scrollbarRef" class="settings-content" @scroll="handleScroll">
|
||||||
|
<div class="set-page">
|
||||||
|
<!-- 基础设置 -->
|
||||||
|
<div id="basic" ref="basicRef" class="settings-section">
|
||||||
|
<div class="settings-section-title">基础设置</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">主题模式</div>
|
||||||
|
<div class="set-item-content">切换日间/夜间主题</div>
|
||||||
|
</div>
|
||||||
|
<n-switch v-model:value="isDarkTheme">
|
||||||
|
<template #checked><i class="ri-moon-line"></i></template>
|
||||||
|
<template #unchecked><i class="ri-sun-line"></i></template>
|
||||||
|
</n-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">动画速度</div>
|
||||||
|
<div class="set-item-content">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<n-switch v-model:value="setData.noAnimate">
|
||||||
|
<template #checked>关闭</template>
|
||||||
|
<template #unchecked>开启</template>
|
||||||
|
</n-switch>
|
||||||
|
<span>是否开启动画</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-400">{{ setData.animationSpeed }}x</span>
|
||||||
|
<div class="w-40">
|
||||||
|
<n-slider
|
||||||
|
v-model:value="setData.animationSpeed"
|
||||||
|
:min="0.1"
|
||||||
|
:max="3"
|
||||||
|
:step="0.1"
|
||||||
|
:marks="{
|
||||||
|
0.1: '极慢',
|
||||||
|
1: '正常',
|
||||||
|
3: '极快'
|
||||||
|
}"
|
||||||
|
:disabled="setData.noAnimate"
|
||||||
|
class="w-40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 播放设置 -->
|
||||||
|
<div id="playback" ref="playbackRef" class="settings-section">
|
||||||
|
<div class="settings-section-title">播放设置</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">音质设置</div>
|
||||||
|
<div class="set-item-content">选择音乐播放音质(VIP)</div>
|
||||||
|
</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="setData.musicQuality"
|
||||||
|
:options="[
|
||||||
|
{ label: '标准', value: 'standard' },
|
||||||
|
{ label: '较高', value: 'higher' },
|
||||||
|
{ label: '极高', value: 'exhigh' },
|
||||||
|
{ label: '无损', value: 'lossless' },
|
||||||
|
{ label: 'Hi-Res', value: 'hires' },
|
||||||
|
{ label: '高清环绕声', value: 'jyeffect' },
|
||||||
|
{ label: '沉浸环绕声', value: 'sky' },
|
||||||
|
{ label: '杜比全景声', value: 'dolby' },
|
||||||
|
{ label: '超清母带', value: 'jymaster' }
|
||||||
|
]"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 应用设置 -->
|
||||||
|
<div v-if="isElectron" id="application" ref="applicationRef" class="settings-section">
|
||||||
|
<div class="settings-section-title">应用设置</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">关闭行为</div>
|
||||||
|
<div class="set-item-content">
|
||||||
|
{{ closeActionLabels[setData.closeAction] || '每次询问' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="setData.closeAction"
|
||||||
|
:options="[
|
||||||
|
{ label: '每次询问', value: 'ask' },
|
||||||
|
{ label: '最小化到托盘', value: 'minimize' },
|
||||||
|
{ label: '直接退出', value: 'close' }
|
||||||
|
]"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">快捷键设置</div>
|
||||||
|
<div class="set-item-content">自定义全局快捷键</div>
|
||||||
|
</div>
|
||||||
|
<n-button size="small" @click="showShortcutModal = true">配置</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">下载目录</div>
|
||||||
|
<div class="set-item-content">
|
||||||
|
{{ setData.downloadPath || '默认下载目录' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<n-button size="small" @click="openDownloadPath">打开目录</n-button>
|
||||||
|
<n-button size="small" @click="selectDownloadPath">修改目录</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 网络设置 -->
|
||||||
|
<div v-if="isElectron" id="network" ref="networkRef" class="settings-section">
|
||||||
|
<div class="settings-section-title">网络设置</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">音乐API端口</div>
|
||||||
|
<div class="set-item-content">修改后需要重启应用</div>
|
||||||
|
</div>
|
||||||
|
<n-input-number v-model:value="setData.musicApiPort" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">代理设置</div>
|
||||||
|
<div class="set-item-content">无法访问音乐时可以开启代理</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<n-switch v-model:value="setData.proxyConfig.enable">
|
||||||
|
<template #checked>开启</template>
|
||||||
|
<template #unchecked>关闭</template>
|
||||||
|
</n-switch>
|
||||||
|
<n-button size="small" @click="showProxyModal = true">配置</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">realIP</div>
|
||||||
|
<div class="set-item-content">
|
||||||
|
由于限制,此项目在国外使用会受到限制可使用realIP参数,传进国内IP解决,如:116.25.146.177
|
||||||
|
即可解决
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<n-switch v-model:value="setData.enableRealIP">
|
||||||
|
<template #checked>开启</template>
|
||||||
|
<template #unchecked>关闭</template>
|
||||||
|
</n-switch>
|
||||||
|
<n-input
|
||||||
|
v-if="setData.enableRealIP"
|
||||||
|
v-model:value="setData.realIP"
|
||||||
|
placeholder="realIP"
|
||||||
|
style="width: 200px"
|
||||||
|
@blur="validateAndSaveRealIP"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统管理 -->
|
||||||
|
<div v-if="isElectron" id="system" ref="systemRef" class="settings-section">
|
||||||
|
<div class="settings-section-title">系统管理</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">缓存管理</div>
|
||||||
|
<div class="set-item-content">清除缓存</div>
|
||||||
|
</div>
|
||||||
|
<n-button size="small" @click="showClearCacheModal = true"> 清除缓存 </n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">重启</div>
|
||||||
|
<div class="set-item-content">重启应用</div>
|
||||||
|
</div>
|
||||||
|
<n-button size="small" @click="restartApp">重启</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 关于 -->
|
||||||
|
<div id="about" ref="aboutRef" class="settings-section">
|
||||||
|
<div class="settings-section-title">关于</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">版本</div>
|
||||||
|
<div class="set-item-content">
|
||||||
|
{{ updateInfo.currentVersion }}
|
||||||
|
<template v-if="updateInfo.hasUpdate">
|
||||||
|
<n-tag type="success" class="ml-2"
|
||||||
|
>发现新版本 {{ updateInfo.latestVersion }}</n-tag
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<n-button size="small" :loading="checking" @click="checkForUpdates(true)">
|
||||||
|
{{ checking ? '检查中...' : '检查更新' }}
|
||||||
|
</n-button>
|
||||||
|
<n-button v-if="updateInfo.hasUpdate" size="small" @click="openReleasePage">
|
||||||
|
前往更新
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="set-item cursor-pointer hover:text-green-500 hover:bg-green-950 transition-all"
|
||||||
|
@click="openAuthor"
|
||||||
|
>
|
||||||
|
<coffee>
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">作者</div>
|
||||||
|
<div class="set-item-content">algerkong 点个star🌟呗</div>
|
||||||
|
</div>
|
||||||
|
</coffee>
|
||||||
|
<div>
|
||||||
|
<n-button size="small" @click="openAuthor">
|
||||||
|
<i class="ri-github-line"></i>前往github
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 捐赠支持 -->
|
||||||
|
<div id="donation" ref="donationRef" class="settings-section">
|
||||||
|
<div class="settings-section-title">捐赠支持</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">捐赠支持</div>
|
||||||
|
<div class="set-item-content">感谢您的支持,让我有动力能够持续改进</div>
|
||||||
|
</div>
|
||||||
|
<n-button text @click="toggleDonationList">
|
||||||
|
<template #icon>
|
||||||
|
<i :class="isDonationListVisible ? 'ri-eye-line' : 'ri-eye-off-line'" />
|
||||||
|
</template>
|
||||||
|
{{ isDonationListVisible ? '隐藏列表' : '显示列表' }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<donation-list v-if="isDonationListVisible" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<play-bottom />
|
||||||
|
</n-scrollbar>
|
||||||
|
|
||||||
|
<!-- 快捷键设置弹窗 -->
|
||||||
|
<shortcut-settings v-model:show="showShortcutModal" @change="handleShortcutsChange" />
|
||||||
|
|
||||||
|
<!-- 代理设置弹窗 -->
|
||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="showProxyModal"
|
v-model:show="showProxyModal"
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
@@ -312,19 +325,55 @@
|
|||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</n-scrollbar>
|
<!-- 清除缓存弹窗 -->
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showClearCacheModal"
|
||||||
|
preset="dialog"
|
||||||
|
title="清除缓存"
|
||||||
|
positive-text="确认"
|
||||||
|
negative-text="取消"
|
||||||
|
@positive-click="clearCache"
|
||||||
|
@negative-click="
|
||||||
|
() => {
|
||||||
|
selectedCacheTypes = [];
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<n-space vertical>
|
||||||
|
<p>请选择要清除的缓存类型:</p>
|
||||||
|
<n-checkbox-group v-model:value="selectedCacheTypes">
|
||||||
|
<n-space vertical>
|
||||||
|
<n-checkbox
|
||||||
|
v-for="option in clearCacheOptions"
|
||||||
|
:key="option.key"
|
||||||
|
:value="option.key"
|
||||||
|
:label="option.label"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div>
|
||||||
|
<div>{{ option.label }}</div>
|
||||||
|
<div class="text-gray-400 text-sm">{{ option.description }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-checkbox>
|
||||||
|
</n-space>
|
||||||
|
</n-checkbox-group>
|
||||||
|
</n-space>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormRules } from 'naive-ui';
|
import type { FormRules } from 'naive-ui';
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
import localData from '@/../main/set.json';
|
import localData from '@/../main/set.json';
|
||||||
import Coffee from '@/components/Coffee.vue';
|
import Coffee from '@/components/Coffee.vue';
|
||||||
import DonationList from '@/components/common/DonationList.vue';
|
import DonationList from '@/components/common/DonationList.vue';
|
||||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||||
|
import ShortcutSettings from '@/components/settings/ShortcutSettings.vue';
|
||||||
import { isElectron } from '@/utils';
|
import { isElectron } from '@/utils';
|
||||||
import { openDirectory, selectDirectory } from '@/utils/fileOperation';
|
import { openDirectory, selectDirectory } from '@/utils/fileOperation';
|
||||||
import { checkUpdate, UpdateResult } from '@/utils/update';
|
import { checkUpdate, UpdateResult } from '@/utils/update';
|
||||||
@@ -631,15 +680,137 @@ const clearCache = async () => {
|
|||||||
showClearCacheModal.value = false;
|
showClearCacheModal.value = false;
|
||||||
selectedCacheTypes.value = [];
|
selectedCacheTypes.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showShortcutModal = ref(false);
|
||||||
|
|
||||||
|
const handleShortcutsChange = (shortcuts: any) => {
|
||||||
|
console.log('快捷键已更新:', shortcuts);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定义设置分类
|
||||||
|
const settingSections = [
|
||||||
|
{ id: 'basic', title: '基础设置' },
|
||||||
|
{ id: 'playback', title: '播放设置' },
|
||||||
|
{ id: 'application', title: '应用设置', electron: true },
|
||||||
|
{ id: 'network', title: '网络设置', electron: true },
|
||||||
|
{ id: 'system', title: '系统管理', electron: true },
|
||||||
|
{ id: 'about', title: '关于' },
|
||||||
|
{ id: 'donation', title: '捐赠支持' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 当前激活的分类
|
||||||
|
const currentSection = ref('basic');
|
||||||
|
const scrollbarRef = ref();
|
||||||
|
|
||||||
|
// 各个分类的ref
|
||||||
|
const basicRef = ref();
|
||||||
|
const playbackRef = ref();
|
||||||
|
const applicationRef = ref();
|
||||||
|
const networkRef = ref();
|
||||||
|
const systemRef = ref();
|
||||||
|
const aboutRef = ref();
|
||||||
|
const donationRef = ref();
|
||||||
|
|
||||||
|
// 滚动到指定分类
|
||||||
|
const scrollToSection = async (sectionId: string) => {
|
||||||
|
currentSection.value = sectionId;
|
||||||
|
const sectionRef = {
|
||||||
|
basic: basicRef,
|
||||||
|
playback: playbackRef,
|
||||||
|
application: applicationRef,
|
||||||
|
network: networkRef,
|
||||||
|
system: systemRef,
|
||||||
|
about: aboutRef,
|
||||||
|
donation: donationRef
|
||||||
|
}[sectionId];
|
||||||
|
|
||||||
|
if (sectionRef?.value) {
|
||||||
|
await nextTick();
|
||||||
|
scrollbarRef.value?.scrollTo({
|
||||||
|
top: sectionRef.value.offsetTop - 20,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理滚动,更新当前激活的分类
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollTop = scrollbarRef.value?.containerRef.scrollTop;
|
||||||
|
const sections = [
|
||||||
|
{ id: 'basic', ref: basicRef },
|
||||||
|
{ id: 'playback', ref: playbackRef },
|
||||||
|
{ id: 'application', ref: applicationRef },
|
||||||
|
{ id: 'network', ref: networkRef },
|
||||||
|
{ id: 'system', ref: systemRef },
|
||||||
|
{ id: 'about', ref: aboutRef },
|
||||||
|
{ id: 'donation', ref: donationRef }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const section of sections) {
|
||||||
|
if (section.ref?.value) {
|
||||||
|
const { offsetTop } = section.ref.value;
|
||||||
|
const offsetBottom = offsetTop + section.ref.value.offsetHeight;
|
||||||
|
|
||||||
|
if (scrollTop >= offsetTop - 100 && scrollTop < offsetBottom) {
|
||||||
|
currentSection.value = section.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.settings-container {
|
||||||
|
@apply flex h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav {
|
||||||
|
@apply w-32 h-full flex-shrink-0 border-r border-gray-200 dark:border-gray-700;
|
||||||
|
@apply bg-light dark:bg-dark;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
@apply px-4 py-2.5 cursor-pointer text-sm;
|
||||||
|
@apply text-gray-600 dark:text-gray-400;
|
||||||
|
@apply transition-colors duration-200;
|
||||||
|
@apply border-l-2 border-transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply text-primary dark:text-white bg-gray-50 dark:bg-dark-100;
|
||||||
|
@apply border-l-2 border-gray-200 dark:border-gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
@apply text-primary dark:text-white bg-gray-50 dark:bg-dark-100;
|
||||||
|
@apply border-l-2 border-gray-200 dark:border-gray-200;
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
@apply flex-1 h-full;
|
||||||
|
}
|
||||||
|
|
||||||
.set-page {
|
.set-page {
|
||||||
@apply p-4 bg-light dark:bg-dark pb-20;
|
@apply p-4 pb-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
@apply mb-6 scroll-mt-4;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
@apply text-base font-medium mb-4;
|
||||||
|
@apply text-gray-600 dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
@apply space-y-4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.set-item {
|
.set-item {
|
||||||
@apply flex items-center justify-between p-4 rounded-lg mb-4 transition-all;
|
@apply flex items-center justify-between p-4 rounded-lg transition-all;
|
||||||
@apply bg-light dark:bg-dark text-gray-900 dark:text-white;
|
@apply bg-light dark:bg-dark text-gray-900 dark:text-white;
|
||||||
@apply border border-gray-200 dark:border-gray-700;
|
@apply border border-gray-200 dark:border-gray-700;
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,6 @@ watch(
|
|||||||
// 页面挂载时检查登录状态
|
// 页面挂载时检查登录状态
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkLoginStatus();
|
checkLoginStatus();
|
||||||
loadPage();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 展示歌单
|
// 展示歌单
|
||||||
@@ -275,7 +274,7 @@ const handlePlay = () => {
|
|||||||
.record-list {
|
.record-list {
|
||||||
@apply rounded-2xl;
|
@apply rounded-2xl;
|
||||||
@apply bg-light dark:bg-black;
|
@apply bg-light dark:bg-black;
|
||||||
height: calc(100% - 3.75rem);
|
height: calc(100% - 100px);
|
||||||
|
|
||||||
.record-item {
|
.record-item {
|
||||||
@apply flex items-center px-4;
|
@apply flex items-center px-4;
|
||||||
|
|||||||
Reference in New Issue
Block a user