From c82ffd0c7dc625fbb16ea941bccae1ba0634038c Mon Sep 17 00:00:00 2001 From: alger Date: Tue, 29 Apr 2025 23:21:16 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=B7=BB=E5=8A=A0=E8=BF=9C?= =?UTF-8?q?=E7=A8=8B=E6=8E=A7=E5=88=B6=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=BF=9C=E7=A8=8B=E6=8E=A7=E5=88=B6=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- resources/html/remote-control.html | 486 +++++++++++++++++++ src/i18n/lang/en-US/common.ts | 1 + src/i18n/lang/en-US/settings.ts | 14 +- src/i18n/lang/zh-CN/common.ts | 1 + src/i18n/lang/zh-CN/settings.json | 5 - src/i18n/lang/zh-CN/settings.ts | 14 +- src/main/index.ts | 4 + src/main/modules/remoteControl.ts | 227 +++++++++ src/renderer/components.d.ts | 3 + src/renderer/components/home/TopBanner.vue | 1 - src/renderer/views/artist/detail.vue | 2 +- src/renderer/views/set/index.vue | 17 + src/renderer/views/setting/ServerSetting.vue | 226 +++++++++ 14 files changed, 995 insertions(+), 10 deletions(-) create mode 100644 resources/html/remote-control.html delete mode 100644 src/i18n/lang/zh-CN/settings.json create mode 100644 src/main/modules/remoteControl.ts create mode 100644 src/renderer/views/setting/ServerSetting.vue diff --git a/package.json b/package.json index 8b85075..8fa399f 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", "@unblockneteasemusic/server": "^0.27.8-patch.1", + "cors": "^2.8.5", "electron-store": "^8.1.0", + "express": "^4.18.2", "electron-updater": "^6.6.2", "font-list": "^1.5.1", "netease-cloud-music-api-alger": "^4.26.1", @@ -54,7 +56,7 @@ "autoprefixer": "^10.4.20", "axios": "^1.7.7", "cross-env": "^7.0.3", - "electron": "^35.2.0", + "electron": "^36.0.0", "electron-builder": "^25.1.8", "electron-vite": "^3.1.0", "eslint": "^9.0.0", diff --git a/resources/html/remote-control.html b/resources/html/remote-control.html new file mode 100644 index 0000000..59c2e7a --- /dev/null +++ b/resources/html/remote-control.html @@ -0,0 +1,486 @@ + + + + + + AlgerMusicPlayer 远程控制 + + + +
+

AlgerMusicPlayer 远程控制

+
+ +
+
+
+ 封面 +
+

未在播放

+

--

+
未播放
+
+
+
+ +
+
+ + + +
+ +
+ + +
+
+ +
+
+ + +
+
+
+ +
+ 准备就绪 +
+ + + + \ No newline at end of file diff --git a/src/i18n/lang/en-US/common.ts b/src/i18n/lang/en-US/common.ts index e6bb675..b943617 100644 --- a/src/i18n/lang/en-US/common.ts +++ b/src/i18n/lang/en-US/common.ts @@ -26,6 +26,7 @@ export default { delete: 'Delete', refresh: 'Refresh', retry: 'Retry', + reset: 'Reset', validation: { required: 'This field is required', invalidInput: 'Invalid input', diff --git a/src/i18n/lang/en-US/settings.ts b/src/i18n/lang/en-US/settings.ts index 7235b30..153dde8 100644 --- a/src/i18n/lang/en-US/settings.ts +++ b/src/i18n/lang/en-US/settings.ts @@ -83,7 +83,9 @@ export default { unlimitedDownload: 'Unlimited Download', unlimitedDownloadDesc: 'Enable unlimited download mode for music , default limit 300 songs', downloadPath: 'Download Directory', - downloadPathDesc: 'Choose download location for music files' + downloadPathDesc: 'Choose download location for music files', + remoteControl: 'Remote Control', + remoteControlDesc: 'Set remote control function' }, network: { apiPort: 'Music API Port', @@ -230,5 +232,15 @@ export default { disableAll: 'All shortcuts disabled, please save to apply', enableAll: 'All shortcuts enabled, please save to apply' } + }, + remoteControl: { + title: 'Remote Control', + enable: 'Enable Remote Control', + port: 'Port', + allowedIps: 'Allowed IPs', + addIp: 'Add IP', + emptyListHint: 'Empty list means allow all IPs', + saveSuccess: 'Remote control settings saved', + accessInfo: 'Remote control access address:', } }; diff --git a/src/i18n/lang/zh-CN/common.ts b/src/i18n/lang/zh-CN/common.ts index 2d57b40..cbe7a4f 100644 --- a/src/i18n/lang/zh-CN/common.ts +++ b/src/i18n/lang/zh-CN/common.ts @@ -26,6 +26,7 @@ export default { delete: '删除', refresh: '刷新', retry: '重试', + reset: '重置', validation: { required: '此项是必填的', invalidInput: '输入无效', diff --git a/src/i18n/lang/zh-CN/settings.json b/src/i18n/lang/zh-CN/settings.json deleted file mode 100644 index 090be0a..0000000 --- a/src/i18n/lang/zh-CN/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -"playback": { - "musicSources": "音源设置", - "musicSourcesDesc": "选择音乐解析使用的音源平台", - "musicSourcesWarning": "至少需要选择一个音源平台" -} \ No newline at end of file diff --git a/src/i18n/lang/zh-CN/settings.ts b/src/i18n/lang/zh-CN/settings.ts index 4e888ba..52db911 100644 --- a/src/i18n/lang/zh-CN/settings.ts +++ b/src/i18n/lang/zh-CN/settings.ts @@ -83,7 +83,9 @@ export default { unlimitedDownload: '无限制下载', unlimitedDownloadDesc: '开启后将无限制下载音乐(可能出现下载失败的情况), 默认限制 300 首', downloadPath: '下载目录', - downloadPathDesc: '选择音乐文件的下载位置' + downloadPathDesc: '选择音乐文件的下载位置', + remoteControl: '远程控制', + remoteControlDesc: '设置远程控制功能' }, network: { apiPort: '音乐API端口', @@ -230,5 +232,15 @@ export default { disableAll: '已禁用所有快捷键,请记得保存', enableAll: '已启用所有快捷键,请记得保存' } + }, + remoteControl: { + title: '远程控制', + enable: '启用远程控制', + port: '服务端口', + allowedIps: '允许的IP地址', + addIp: '添加IP', + emptyListHint: '空列表表示允许所有IP访问', + saveSuccess: '远程控制设置已保存', + accessInfo: '远程控制访问地址:', } }; diff --git a/src/main/index.ts b/src/main/index.ts index 542d4ae..70c5510 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -8,6 +8,7 @@ import { loadLyricWindow } from './lyric'; import { initializeConfig } from './modules/config'; import { initializeFileManager } from './modules/fileManager'; import { initializeFonts } from './modules/fonts'; +import { initializeRemoteControl } from './modules/remoteControl'; import { initializeShortcuts, registerShortcuts } from './modules/shortcuts'; import { initializeStats, setupStatsHandlers } from './modules/statsService'; import { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray'; @@ -66,6 +67,9 @@ function initialize() { // 初始化快捷键 initializeShortcuts(mainWindow); + // 初始化远程控制服务 + initializeRemoteControl(mainWindow); + // 初始化更新处理程序 setupUpdateHandlers(mainWindow); } diff --git a/src/main/modules/remoteControl.ts b/src/main/modules/remoteControl.ts new file mode 100644 index 0000000..6a3d3b3 --- /dev/null +++ b/src/main/modules/remoteControl.ts @@ -0,0 +1,227 @@ +import { ipcMain } from 'electron'; +import express from 'express'; +import cors from 'cors'; +import os from 'os'; +import { getStore } from './config'; +import path from 'path'; +import fs from 'fs'; + +// 定义远程控制相关接口 +export interface RemoteControlConfig { + enabled: boolean; + port: number; + allowedIps: string[]; +} + +// 默认配置 +export const defaultRemoteControlConfig: RemoteControlConfig = { + enabled: false, + port: 31888, + allowedIps: [] +}; + +let app: express.Application | null = null; +let server: any = null; +let mainWindowRef: Electron.BrowserWindow | null = null; +let currentSong: any = null; +let isPlaying: boolean = false; + +// 获取本地IP地址 +function getLocalIpAddresses(): string[] { + const interfaces = os.networkInterfaces(); + const addresses: string[] = []; + + for (const key in interfaces) { + const iface = interfaces[key]; + if (iface) { + for (const alias of iface) { + if (alias.family === 'IPv4' && !alias.internal) { + addresses.push(alias.address); + } + } + } + } + + return addresses; +} + +// 初始化远程控制服务 +export function initializeRemoteControl(mainWindow: Electron.BrowserWindow) { + mainWindowRef = mainWindow; + const store = getStore() as any; + let config = store.get('remoteControl') as RemoteControlConfig; + + // 如果配置不存在,使用默认配置 + if (!config) { + config = defaultRemoteControlConfig; + store.set('remoteControl', config); + } + + // 监听当前歌曲变化 + ipcMain.on('update-current-song', (_, song: any) => { + currentSong = song; + }); + + // 监听播放状态变化 + ipcMain.on('update-play-state', (_, playing: boolean) => { + isPlaying = playing; + }); + + // 监听远程控制配置变化 + ipcMain.on('update-remote-control-config', (_, newConfig: RemoteControlConfig) => { + if (server) { + stopServer(); + } + + store.set('remoteControl', newConfig); + + if (newConfig.enabled) { + startServer(newConfig); + } + }); + + // 获取远程控制配置 + ipcMain.handle('get-remote-control-config', () => { + const config = store.get('remoteControl') as RemoteControlConfig; + return config || defaultRemoteControlConfig; + }); + + // 获取本地IP地址 + ipcMain.handle('get-local-ip-addresses', () => { + return getLocalIpAddresses(); + }); + + // 如果启用了远程控制,启动服务器 + if (config.enabled) { + startServer(config); + } +} + +// 启动远程控制服务器 +function startServer(config: RemoteControlConfig) { + if (!mainWindowRef) { + console.error('主窗口未初始化,无法启动远程控制服务'); + return; + } + + app = express(); + + // 跨域配置 + app.use(cors()); + app.use(express.json()); + + // IP 过滤中间件 + app.use((req, res, next) => { + const clientIp = req.ip || req.socket.remoteAddress || ''; + const cleanIp = clientIp.replace(/^::ffff:/, ''); // 移除IPv6前缀 + console.log('config',config) + if (config.allowedIps.length === 0 || config.allowedIps.includes(cleanIp)) { + next(); + } else { + res.status(403).json({ error: '未授权的IP地址' }); + } + }); + + // 路由配置 + setupRoutes(app); + + // 启动服务器 + try { + server = app.listen(config.port, () => { + console.log(`远程控制服务已启动,监听端口: ${config.port}`); + }); + } catch (error) { + console.error('启动远程控制服务失败:', error); + } +} + +// 停止远程控制服务器 +function stopServer() { + if (server) { + server.close(); + server = null; + app = null; + console.log('远程控制服务已停止'); + } +} + +// 设置路由 +function setupRoutes(app: express.Application) { + // 获取当前播放状态 + app.get('/api/status', (_, res) => { + res.json({ + isPlaying, + currentSong + }); + }); + + // 播放/暂停 + app.post('/api/toggle-play', (_, res) => { + if (!mainWindowRef) { + return res.status(500).json({ error: '主窗口未初始化' }); + } + mainWindowRef.webContents.send('global-shortcut', 'togglePlay'); + res.json({ success: true, message: '已发送播放/暂停指令' }); + }); + + // 上一首 + app.post('/api/prev', (_, res) => { + if (!mainWindowRef) { + return res.status(500).json({ error: '主窗口未初始化' }); + } + mainWindowRef.webContents.send('global-shortcut', 'prevPlay'); + res.json({ success: true, message: '已发送上一首指令' }); + }); + + // 下一首 + app.post('/api/next', (_, res) => { + if (!mainWindowRef) { + return res.status(500).json({ error: '主窗口未初始化' }); + } + mainWindowRef.webContents.send('global-shortcut', 'nextPlay'); + res.json({ success: true, message: '已发送下一首指令' }); + }); + + // 音量加 + app.post('/api/volume-up', (_, res) => { + if (!mainWindowRef) { + return res.status(500).json({ error: '主窗口未初始化' }); + } + mainWindowRef.webContents.send('global-shortcut', 'volumeUp'); + res.json({ success: true, message: '已发送音量加指令' }); + }); + + // 音量减 + app.post('/api/volume-down', (_, res) => { + if (!mainWindowRef) { + return res.status(500).json({ error: '主窗口未初始化' }); + } + mainWindowRef.webContents.send('global-shortcut', 'volumeDown'); + res.json({ success: true, message: '已发送音量减指令' }); + }); + + // 收藏/取消收藏 + app.post('/api/toggle-favorite', (_, res) => { + if (!mainWindowRef) { + return res.status(500).json({ error: '主窗口未初始化' }); + } + mainWindowRef.webContents.send('global-shortcut', 'toggleFavorite'); + res.json({ success: true, message: '已发送收藏/取消收藏指令' }); + }); + + // 提供远程控制界面HTML + app.get('/', (_, res) => { + try { + const htmlPath = path.join(process.cwd(), 'resources', 'html', 'remote-control.html'); + if (fs.existsSync(htmlPath)) { + res.sendFile(htmlPath); + } else { + res.status(404).send('远程控制界面文件未找到'); + console.error('远程控制界面文件不存在:', htmlPath); + } + } catch (error) { + console.error('加载远程控制界面失败:', error); + res.status(500).send('加载远程控制界面失败'); + } + }); +} \ No newline at end of file diff --git a/src/renderer/components.d.ts b/src/renderer/components.d.ts index 0d700f4..0c4f1b1 100644 --- a/src/renderer/components.d.ts +++ b/src/renderer/components.d.ts @@ -19,8 +19,10 @@ declare module 'vue' { NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup'] NCollapse: typeof import('naive-ui')['NCollapse'] NCollapseItem: typeof import('naive-ui')['NCollapseItem'] + NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NDialogProvider: typeof import('naive-ui')['NDialogProvider'] + NDivider: typeof import('naive-ui')['NDivider'] NDrawer: typeof import('naive-ui')['NDrawer'] NDrawerContent: typeof import('naive-ui')['NDrawerContent'] NDropdown: typeof import('naive-ui')['NDropdown'] @@ -50,6 +52,7 @@ declare module 'vue' { NTabPane: typeof import('naive-ui')['NTabPane'] NTabs: typeof import('naive-ui')['NTabs'] NTag: typeof import('naive-ui')['NTag'] + NText: typeof import('naive-ui')['NText'] NTooltip: typeof import('naive-ui')['NTooltip'] NVirtualList: typeof import('naive-ui')['NVirtualList'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/src/renderer/components/home/TopBanner.vue b/src/renderer/components/home/TopBanner.vue index 6f9d0e0..c60385c 100644 --- a/src/renderer/components/home/TopBanner.vue +++ b/src/renderer/components/home/TopBanner.vue @@ -168,7 +168,6 @@ import { setBackgroundImg } from '@/utils'; import { getArtistDetail } from '@/api/artist'; -import { cloneDeep } from 'lodash'; const userStore = useUserStore(); const playerStore = usePlayerStore(); diff --git a/src/renderer/views/artist/detail.vue b/src/renderer/views/artist/detail.vue index b6172aa..5c64a33 100644 --- a/src/renderer/views/artist/detail.vue +++ b/src/renderer/views/artist/detail.vue @@ -72,7 +72,7 @@ \ No newline at end of file