mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-15 07:20:49 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d3992154a | ||
|
|
81e7b67c7f | ||
|
|
d7e94a342b | ||
|
|
46f8067577 | ||
|
|
1dc7d0ceca | ||
|
|
ba64631a17 | ||
|
|
cdb9524f04 | ||
|
|
5213aa13c5 | ||
|
|
d870d0198f | ||
|
|
976a9afd2f | ||
|
|
018218a5bf | ||
|
|
38a9d6ed31 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,21 +1,13 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
## [v3.0.0] - 2024-03-21
|
## [v3.2.0] - 2024-01-04
|
||||||
|
|
||||||
### ✨ 新功能
|
### ✨ 新功能
|
||||||
- 新增自动更新检测功能
|
- 添加代理功能和 realIP配置功能 解决内网访问问题 和 国外访问问题 (d7e94a3)
|
||||||
- 新增 GitHub Actions 自动构建和发布
|
- 关闭应用的提示修改,可存储配置最小化或关闭 (46f8067)
|
||||||
- 新增主题色切换功能,支持日间/夜间模式 (#19, #21)
|
- 解决检查更新请求失败问题 (cdb9524)
|
||||||
- 新增随机播放功能 (#20)
|
|
||||||
- 优化主题效果和图片清晰度
|
|
||||||
|
|
||||||
### 🏗️ 架构重构
|
|
||||||
- 重构整个项目架构
|
|
||||||
- 优化打包配置
|
|
||||||
- 修改后台服务为本地运行
|
|
||||||
- 优化项目结构
|
|
||||||
|
|
||||||
### 🐞 问题修复
|
### 🐞 问题修复
|
||||||
- 修复 web 移动端页面空白问题 (#24)
|
- 修复歌词页面与底栏冲突问题(#26) (1dc7d0c)
|
||||||
- 修复无用导入问题
|
- 修复搜索歌曲列表页面显示错误问题(#33) (1dc7d0c)
|
||||||
- 优化错误处理
|
- 修复搜索类型切换没有重新加载搜索的问题(#25) (ba64631)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "AlgerMusicPlayer",
|
"name": "AlgerMusicPlayer",
|
||||||
"version": "3.0.0",
|
"version": "3.2.0",
|
||||||
"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",
|
||||||
@@ -26,7 +26,6 @@
|
|||||||
"@unblockneteasemusic/server": "^0.27.8-patch.1",
|
"@unblockneteasemusic/server": "^0.27.8-patch.1",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
|
|
||||||
"netease-cloud-music-api-alger": "^4.25.0"
|
"netease-cloud-music-api-alger": "^4.25.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils';
|
import { electronApp, optimizer } from '@electron-toolkit/utils';
|
||||||
import { app, BrowserWindow, globalShortcut, ipcMain, Menu, nativeImage, shell, Tray } from 'electron';
|
import { app, globalShortcut, ipcMain, nativeImage } from 'electron';
|
||||||
import Store from 'electron-store';
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import set from './set.json';
|
|
||||||
|
import { loadLyricWindow } from './lyric';
|
||||||
|
import { startMusicApi } from './server';
|
||||||
|
import { initializeFileManager } from './modules/fileManager';
|
||||||
|
import { initializeTray } from './modules/tray';
|
||||||
|
import { createMainWindow, initializeWindowManager } from './modules/window';
|
||||||
|
import { initializeConfig } from './modules/config';
|
||||||
|
|
||||||
// 导入所有图标
|
// 导入所有图标
|
||||||
const iconPath = join(__dirname, '../../resources');
|
const iconPath = join(__dirname, '../../resources');
|
||||||
const icon = nativeImage.createFromPath(
|
const icon = nativeImage.createFromPath(
|
||||||
@@ -13,109 +19,50 @@ const icon = nativeImage.createFromPath(
|
|||||||
: join(iconPath, 'icon.png')
|
: join(iconPath, 'icon.png')
|
||||||
);
|
);
|
||||||
|
|
||||||
import { loadLyricWindow } from './lyric';
|
let mainWindow: Electron.BrowserWindow;
|
||||||
import { startMusicApi } from './server';
|
|
||||||
|
|
||||||
let mainWindow: BrowserWindow;
|
// 初始化应用
|
||||||
function createWindow(): void {
|
function initialize() {
|
||||||
startMusicApi();
|
// 初始化各个模块
|
||||||
// Create the browser window.
|
initializeConfig();
|
||||||
mainWindow = new BrowserWindow({
|
initializeFileManager();
|
||||||
width: 1200,
|
|
||||||
height: 780,
|
|
||||||
show: false,
|
|
||||||
frame: false,
|
|
||||||
autoHideMenuBar: true,
|
|
||||||
icon,
|
|
||||||
webPreferences: {
|
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
|
||||||
sandbox: false,
|
|
||||||
contextIsolation: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.setMinimumSize(1200, 780);
|
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
|
||||||
mainWindow.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
|
||||||
shell.openExternal(details.url);
|
|
||||||
return { action: 'deny' };
|
|
||||||
});
|
|
||||||
|
|
||||||
// HMR for renderer base on electron-vite cli.
|
|
||||||
// Load the remote URL for development or the local html file for production.
|
|
||||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
|
||||||
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
|
||||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
|
||||||
} else {
|
|
||||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建托盘图标
|
// 创建主窗口
|
||||||
const trayIcon = nativeImage.createFromPath(join(iconPath, 'icon_16x16.png')).resize({ width: 16, height: 16 });
|
mainWindow = createMainWindow(icon);
|
||||||
const tray = new Tray(trayIcon);
|
|
||||||
|
// 初始化窗口管理
|
||||||
// 创建一个上下文菜单
|
initializeWindowManager();
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
|
||||||
{
|
// 初始化托盘
|
||||||
label: '显示',
|
initializeTray(iconPath, mainWindow);
|
||||||
click: () => {
|
|
||||||
mainWindow.show();
|
// 启动音乐API
|
||||||
},
|
startMusicApi();
|
||||||
},
|
|
||||||
{
|
// 加载歌词窗口
|
||||||
label: '退出',
|
|
||||||
click: () => {
|
|
||||||
mainWindow.destroy();
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 设置系统托盘图标的上下文菜单
|
|
||||||
tray.setContextMenu(contextMenu);
|
|
||||||
|
|
||||||
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
|
|
||||||
tray.on('click', () => {
|
|
||||||
if (mainWindow.isVisible()) {
|
|
||||||
mainWindow.hide();
|
|
||||||
} else {
|
|
||||||
mainWindow.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loadLyricWindow(ipcMain, mainWindow);
|
loadLyricWindow(ipcMain, mainWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// 应用程序准备就绪时的处理
|
||||||
// initialization and is ready to create browser windows.
|
|
||||||
// Some APIs can only be used after this event occurs.
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Set app user model id for windows
|
// 设置应用ID
|
||||||
electronApp.setAppUserModelId('com.alger.music');
|
electronApp.setAppUserModelId('com.alger.music');
|
||||||
|
|
||||||
// Default open or close DevTools by F12 in development
|
// 监听窗口创建事件
|
||||||
// and ignore CommandOrControl + R in production.
|
|
||||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window);
|
optimizer.watchWindowShortcuts(window);
|
||||||
});
|
});
|
||||||
|
|
||||||
// IPC test
|
// 初始化应用
|
||||||
ipcMain.on('ping', () => console.log('pong'));
|
initialize();
|
||||||
|
|
||||||
createWindow();
|
|
||||||
|
|
||||||
|
// macOS 激活应用时的处理
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
if (mainWindow === null) initialize();
|
||||||
// dock icon is clicked and there are no other windows open.
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 应用程序准备就绪后的快捷键设置
|
||||||
app.on('ready', () => {
|
app.on('ready', () => {
|
||||||
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
||||||
if (mainWindow.isVisible()) {
|
if (mainWindow.isVisible()) {
|
||||||
@@ -126,84 +73,20 @@ app.on('ready', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// 所有窗口关闭时的处理
|
||||||
// for applications and their menu bar to stay active until the user quits
|
|
||||||
// explicitly with Cmd + Q.
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('minimize-window', (event) => {
|
// 重启应用
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
|
||||||
if (win) {
|
|
||||||
win.minimize();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('maximize-window', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
|
||||||
if (win) {
|
|
||||||
if (win.isMaximized()) {
|
|
||||||
win.unmaximize();
|
|
||||||
} else {
|
|
||||||
win.maximize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('close-window', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
|
||||||
if (win) {
|
|
||||||
win.destroy();
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('drag-start', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
|
||||||
if (win) {
|
|
||||||
win.webContents.beginFrameSubscription((frameBuffer) => {
|
|
||||||
event.reply('frame-buffer', frameBuffer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('mini-tray', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
|
||||||
if (win) {
|
|
||||||
win.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 重启
|
|
||||||
ipcMain.on('restart', () => {
|
ipcMain.on('restart', () => {
|
||||||
app.relaunch();
|
app.relaunch();
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = new Store({
|
// 获取系统架构信息
|
||||||
name: 'config', // 配置文件名
|
|
||||||
defaults: {
|
|
||||||
set: set
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 定义ipcRenderer监听事件
|
|
||||||
ipcMain.on('set-store-value', (_, key, value) => {
|
|
||||||
store.set(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('get-store-value', (_, key) => {
|
|
||||||
const value = store.get(key);
|
|
||||||
_.returnValue = value || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加 IPC 处理程序
|
|
||||||
ipcMain.on('get-arch', (event) => {
|
ipcMain.on('get-arch', (event) => {
|
||||||
event.returnValue = process.arch;
|
event.returnValue = process.arch;
|
||||||
});
|
});
|
||||||
|
|
||||||
// In this file you can include the rest of your app"s specific main process
|
|
||||||
// code. You can also put them in separate files and require them here.
|
|
||||||
|
|||||||
42
src/main/modules/config.ts
Normal file
42
src/main/modules/config.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { app, ipcMain } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
import set from '../set.json';
|
||||||
|
|
||||||
|
interface StoreType {
|
||||||
|
set: {
|
||||||
|
isProxy: boolean;
|
||||||
|
noAnimate: boolean;
|
||||||
|
animationSpeed: number;
|
||||||
|
author: string;
|
||||||
|
authorUrl: string;
|
||||||
|
musicApiPort: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let store: Store<StoreType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化配置管理
|
||||||
|
*/
|
||||||
|
export function initializeConfig() {
|
||||||
|
store = new Store<StoreType>({
|
||||||
|
name: 'config',
|
||||||
|
defaults: {
|
||||||
|
set: set
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
store.get('set.downloadPath') || store.set('set.downloadPath', app.getPath('downloads'));
|
||||||
|
|
||||||
|
// 定义ipcRenderer监听事件
|
||||||
|
ipcMain.on('set-store-value', (_, key, value) => {
|
||||||
|
store.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('get-store-value', (_, key) => {
|
||||||
|
const value = store.get(key);
|
||||||
|
_.returnValue = value || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
69
src/main/modules/fileManager.ts
Normal file
69
src/main/modules/fileManager.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { app, dialog, shell, ipcMain } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化文件管理相关的IPC监听
|
||||||
|
*/
|
||||||
|
export function initializeFileManager() {
|
||||||
|
// 通用的选择目录处理
|
||||||
|
ipcMain.handle('select-directory', async () => {
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
properties: ['openDirectory'],
|
||||||
|
title: '选择目录'
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通用的打开目录处理
|
||||||
|
ipcMain.on('open-directory', (_, path) => {
|
||||||
|
shell.openPath(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载音乐处理
|
||||||
|
ipcMain.on('download-music', downloadMusic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载音乐功能
|
||||||
|
*/
|
||||||
|
async function downloadMusic(event: Electron.IpcMainEvent, { url, filename }: { url: string; filename: string }) {
|
||||||
|
try {
|
||||||
|
const store = new Store();
|
||||||
|
const downloadPath = store.get('set.downloadPath') as string || app.getPath('downloads');
|
||||||
|
|
||||||
|
// 直接使用配置的下载路径
|
||||||
|
const filePath = path.join(downloadPath, `${filename}.mp3`);
|
||||||
|
|
||||||
|
// 检查文件是否已存在,如果存在则添加序号
|
||||||
|
let finalFilePath = filePath;
|
||||||
|
let counter = 1;
|
||||||
|
while (fs.existsSync(finalFilePath)) {
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const nameWithoutExt = filePath.slice(0, -ext.length);
|
||||||
|
finalFilePath = `${nameWithoutExt} (${counter})${ext}`;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'stream'
|
||||||
|
});
|
||||||
|
|
||||||
|
const writer = fs.createWriteStream(finalFilePath);
|
||||||
|
response.data.pipe(writer);
|
||||||
|
|
||||||
|
writer.on('finish', () => {
|
||||||
|
event.reply('music-download-complete', { success: true, path: finalFilePath });
|
||||||
|
});
|
||||||
|
|
||||||
|
writer.on('error', (err) => {
|
||||||
|
event.reply('music-download-complete', { success: false, error: err.message });
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
event.reply('music-download-complete', { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/modules/tray.ts
Normal file
43
src/main/modules/tray.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { app, Menu, nativeImage, Tray, BrowserWindow } from 'electron';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
let tray: Tray | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化系统托盘
|
||||||
|
*/
|
||||||
|
export function initializeTray(iconPath: string, mainWindow: BrowserWindow) {
|
||||||
|
const trayIcon = nativeImage.createFromPath(join(iconPath, 'icon_16x16.png')).resize({ width: 16, height: 16 });
|
||||||
|
tray = new Tray(trayIcon);
|
||||||
|
|
||||||
|
// 创建一个上下文菜单
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: '显示',
|
||||||
|
click: () => {
|
||||||
|
mainWindow.show();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出',
|
||||||
|
click: () => {
|
||||||
|
mainWindow.destroy();
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 设置系统托盘图标的上下文菜单
|
||||||
|
tray.setContextMenu(contextMenu);
|
||||||
|
|
||||||
|
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
|
||||||
|
tray.on('click', () => {
|
||||||
|
if (mainWindow.isVisible()) {
|
||||||
|
mainWindow.hide();
|
||||||
|
} else {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tray;
|
||||||
|
}
|
||||||
119
src/main/modules/window.ts
Normal file
119
src/main/modules/window.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { BrowserWindow, shell, ipcMain, app, session } from 'electron';
|
||||||
|
import { is } from '@electron-toolkit/utils';
|
||||||
|
import { join } from 'path';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
|
||||||
|
const store = new Store();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化代理设置
|
||||||
|
*/
|
||||||
|
function initializeProxy() {
|
||||||
|
const defaultConfig = {
|
||||||
|
enable: false,
|
||||||
|
protocol: 'http',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 7890
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyConfig = store.get('set.proxyConfig', defaultConfig) as {
|
||||||
|
enable: boolean;
|
||||||
|
protocol: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proxyConfig?.enable) {
|
||||||
|
const proxyRules = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`;
|
||||||
|
session.defaultSession.setProxy({ proxyRules });
|
||||||
|
} else {
|
||||||
|
session.defaultSession.setProxy({ proxyRules: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化窗口管理相关的IPC监听
|
||||||
|
*/
|
||||||
|
export function initializeWindowManager() {
|
||||||
|
// 初始化代理设置
|
||||||
|
initializeProxy();
|
||||||
|
|
||||||
|
ipcMain.on('minimize-window', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
win.minimize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('maximize-window', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
if (win.isMaximized()) {
|
||||||
|
win.unmaximize();
|
||||||
|
} else {
|
||||||
|
win.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('close-window', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
win.destroy();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('mini-tray', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
win.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听代理设置变化
|
||||||
|
store.onDidChange('set.proxyConfig', () => {
|
||||||
|
initializeProxy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主窗口
|
||||||
|
*/
|
||||||
|
export function createMainWindow(icon: Electron.NativeImage): BrowserWindow {
|
||||||
|
const mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 780,
|
||||||
|
show: false,
|
||||||
|
frame: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
icon,
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false,
|
||||||
|
contextIsolation: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.setMinimumSize(1200, 780);
|
||||||
|
|
||||||
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
|
||||||
|
// HMR for renderer base on electron-vite cli.
|
||||||
|
// Load the remote URL for development or the local html file for production.
|
||||||
|
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||||
|
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainWindow;
|
||||||
|
}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
{
|
{
|
||||||
"isProxy": false,
|
"isProxy": false,
|
||||||
|
"proxyConfig": {
|
||||||
|
"enable": false,
|
||||||
|
"protocol": "http",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 7890
|
||||||
|
},
|
||||||
|
"enableRealIP":false,
|
||||||
|
"realIP":"",
|
||||||
"noAnimate": false,
|
"noAnimate": false,
|
||||||
"animationSpeed": 1,
|
"animationSpeed": 1,
|
||||||
"author": "Alger",
|
"author": "Alger",
|
||||||
"authorUrl": "https://github.com/algerkong",
|
"authorUrl": "https://github.com/algerkong",
|
||||||
"musicApiPort": 30488
|
"musicApiPort": 30488,
|
||||||
|
"closeAction": "ask"
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/renderer/components.d.ts
vendored
3
src/renderer/components.d.ts
vendored
@@ -17,6 +17,8 @@ declare module 'vue' {
|
|||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
|
NForm: typeof import('naive-ui')['NForm']
|
||||||
|
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
NImage: typeof import('naive-ui')['NImage']
|
NImage: typeof import('naive-ui')['NImage']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
@@ -25,6 +27,7 @@ declare module 'vue' {
|
|||||||
NModal: typeof import('naive-ui')['NModal']
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
NPopover: typeof import('naive-ui')['NPopover']
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
NSlider: typeof import('naive-ui')['NSlider']
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
NSpin: typeof import('naive-ui')['NSpin']
|
NSpin: typeof import('naive-ui')['NSpin']
|
||||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
|
|||||||
@@ -49,7 +49,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { NButton, NImage, NPopover } from 'naive-ui';
|
import { NButton, NImage, NPopover } from 'naive-ui';
|
||||||
|
import alipay from '@/assets/alipay.png';
|
||||||
|
import wechat from '@/assets/wechat.png';
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const copyQQ = () => {
|
const copyQQ = () => {
|
||||||
navigator.clipboard.writeText('789288579');
|
navigator.clipboard.writeText('789288579');
|
||||||
@@ -59,11 +60,11 @@ const copyQQ = () => {
|
|||||||
defineProps({
|
defineProps({
|
||||||
alipayQR: {
|
alipayQR: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
default: alipay
|
||||||
},
|
},
|
||||||
wechatQR: {
|
wechatQR: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
default: wechat
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -313,4 +313,13 @@ watch(
|
|||||||
.double-item {
|
.double-item {
|
||||||
@apply mb-2 bg-light-100 bg-opacity-20 dark:bg-dark-100 dark:bg-opacity-20 rounded-3xl;
|
@apply mb-2 bg-light-100 bg-opacity-20 dark:bg-dark-100 dark:bg-opacity-20 rounded-3xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
.music-info {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
.music-list-content {
|
||||||
|
@apply pb-[100px];
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -39,13 +39,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { isElectron, isMobile } from '@/utils';
|
import { isElectron, isMobile } from '@/utils';
|
||||||
|
|
||||||
import config from '../../../../package.json';
|
import config from '../../../../package.json';
|
||||||
|
import { getLatestReleaseInfo } from '@/utils/update';
|
||||||
|
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const noPrompt = ref(false);
|
const noPrompt = ref(false);
|
||||||
|
const releaseInfo = ref<any>(null);
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
showModal.value = false;
|
showModal.value = false;
|
||||||
@@ -54,7 +54,7 @@ const closeModal = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
// 如果是 electron 环境,不显示安装提示
|
// 如果是 electron 环境,不显示安装提示
|
||||||
if (isElectron || isMobile.value) {
|
if (isElectron || isMobile.value) {
|
||||||
return;
|
return;
|
||||||
@@ -65,33 +65,59 @@ onMounted(() => {
|
|||||||
if (isDismissed) {
|
if (isDismissed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取最新版本信息
|
||||||
|
releaseInfo.value = await getLatestReleaseInfo();
|
||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleInstall = async (): Promise<void> => {
|
const handleInstall = async (): Promise<void> => {
|
||||||
|
const assets = releaseInfo.value?.assets || [];
|
||||||
const { userAgent } = navigator;
|
const { userAgent } = navigator;
|
||||||
console.log('userAgent', userAgent);
|
const isMac = userAgent.toLowerCase().includes('mac');
|
||||||
const isMac: boolean = userAgent.includes('Mac');
|
const isWindows = userAgent.toLowerCase().includes('win');
|
||||||
const isWindows: boolean = userAgent.includes('Win');
|
const isLinux = userAgent.toLowerCase().includes('linux');
|
||||||
const isARM: boolean =
|
const isX64 = userAgent.includes('x86_64') ||
|
||||||
userAgent.includes('ARM') || userAgent.includes('arm') || userAgent.includes('OS X');
|
userAgent.includes('Win64') ||
|
||||||
const isX64: boolean =
|
userAgent.includes('WOW64');
|
||||||
userAgent.includes('x86_64') || userAgent.includes('Win64') || userAgent.includes('WOW64');
|
|
||||||
const isX86: boolean =
|
|
||||||
!isX64 &&
|
|
||||||
(userAgent.includes('i686') || userAgent.includes('i386') || userAgent.includes('Win32'));
|
|
||||||
|
|
||||||
const getDownloadUrl = (os: string, arch: string): string => {
|
let downloadUrl = '';
|
||||||
const version = config.version as string;
|
|
||||||
const setup = os !== 'mac' ? 'Setup_' : '';
|
|
||||||
return `https://gh.llkk.cc/https://github.com/algerkong/AlgerMusicPlayer/releases/download/${version}/AlgerMusic_${version}_${setup}${arch}.${os === 'mac' ? 'dmg' : 'exe'}`;
|
|
||||||
};
|
|
||||||
const osType: string | null = isMac ? 'mac' : isWindows ? 'windows' : null;
|
|
||||||
const archType: string | null = isARM ? 'arm64' : isX64 ? 'x64' : isX86 ? 'x86' : null;
|
|
||||||
|
|
||||||
const downloadUrl: string | null = osType && archType ? getDownloadUrl(osType, archType) : null;
|
// 根据平台和架构选择对应的安装包
|
||||||
|
if (isMac) {
|
||||||
|
// macOS
|
||||||
|
const macAsset = assets.find(asset =>
|
||||||
|
asset.name.includes('mac')
|
||||||
|
);
|
||||||
|
downloadUrl = macAsset?.browser_download_url || '';
|
||||||
|
} else if (isWindows) {
|
||||||
|
// Windows
|
||||||
|
let winAsset = assets.find(asset =>
|
||||||
|
asset.name.includes('win') &&
|
||||||
|
(isX64 ? asset.name.includes('x64') : asset.name.includes('ia32'))
|
||||||
|
);
|
||||||
|
if(!winAsset){
|
||||||
|
winAsset = assets.find(asset =>
|
||||||
|
asset.name.includes('win.exe')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
downloadUrl = winAsset?.browser_download_url || '';
|
||||||
|
} else if (isLinux) {
|
||||||
|
// Linux
|
||||||
|
const linuxAsset = assets.find(asset =>
|
||||||
|
(asset.name.endsWith('.AppImage') || asset.name.endsWith('.deb')) &&
|
||||||
|
asset.name.includes('x64')
|
||||||
|
);
|
||||||
|
downloadUrl = linuxAsset?.browser_download_url || '';
|
||||||
|
}
|
||||||
|
|
||||||
window.open(downloadUrl || 'https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
|
if (downloadUrl) {
|
||||||
|
window.open(`https://ghproxy.cn/${downloadUrl}`, '_blank');
|
||||||
|
} else {
|
||||||
|
// 如果没有找到对应的安装包,跳转到 release 页面
|
||||||
|
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="song-item" :class="{ 'song-mini': mini, 'song-list': list }">
|
<div class="song-item" :class="{ 'song-mini': mini, 'song-list': list }" @contextmenu.prevent="handleContextMenu">
|
||||||
<n-image
|
<n-image
|
||||||
v-if="item.picUrl"
|
v-if="item.picUrl"
|
||||||
ref="songImg"
|
ref="songImg"
|
||||||
@@ -57,17 +57,30 @@
|
|||||||
<i v-else class="iconfont icon-playfill"></i>
|
<i v-else class="iconfont icon-playfill"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<n-dropdown
|
||||||
|
v-if="isElectron"
|
||||||
|
:show="showDropdown"
|
||||||
|
:options="dropdownOptions"
|
||||||
|
:x="dropdownX"
|
||||||
|
:y="dropdownY"
|
||||||
|
placement="bottom-start"
|
||||||
|
@clickoutside="showDropdown = false"
|
||||||
|
@select="handleSelect"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, useTemplateRef } from 'vue';
|
import { computed, h, ref, useTemplateRef } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
import { useMessage } from 'naive-ui';
|
||||||
|
import type { MenuOption } from 'naive-ui';
|
||||||
|
|
||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
import { getImgUrl } from '@/utils';
|
import { getImgUrl, isElectron } from '@/utils';
|
||||||
import { getImageBackground } from '@/utils/linearColor';
|
import { getImageBackground } from '@/utils/linearColor';
|
||||||
|
import { getSongUrl } from '@/hooks/MusicListHook';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -84,22 +97,96 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
const play = computed(() => store.state.play as boolean);
|
const play = computed(() => store.state.play as boolean);
|
||||||
|
|
||||||
const playMusic = computed(() => store.state.playMusic);
|
const playMusic = computed(() => store.state.playMusic);
|
||||||
|
|
||||||
const playLoading = computed(
|
const playLoading = computed(
|
||||||
() => playMusic.value.id === props.item.id && playMusic.value.playLoading
|
() => playMusic.value.id === props.item.id && playMusic.value.playLoading
|
||||||
);
|
);
|
||||||
|
|
||||||
// 判断是否为正在播放的音乐
|
|
||||||
const isPlaying = computed(() => {
|
const isPlaying = computed(() => {
|
||||||
return playMusic.value.id === props.item.id;
|
return playMusic.value.id === props.item.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['play']);
|
const showDropdown = ref(false);
|
||||||
|
const dropdownX = ref(0);
|
||||||
|
const dropdownY = ref(0);
|
||||||
|
|
||||||
|
const isDownloading = ref(false);
|
||||||
|
|
||||||
|
const dropdownOptions = computed<MenuOption[]>(() => [
|
||||||
|
{
|
||||||
|
label: isDownloading.value ? '下载中...' : '下载 ' + props.item.name,
|
||||||
|
key: 'download',
|
||||||
|
icon: () => h('i', { class: 'iconfont ri-download-line' }),
|
||||||
|
disabled: isDownloading.value
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleContextMenu = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
showDropdown.value = true;
|
||||||
|
dropdownX.value = e.clientX;
|
||||||
|
dropdownY.value = e.clientY;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (key: string | number) => {
|
||||||
|
showDropdown.value = false;
|
||||||
|
if (key === 'download') {
|
||||||
|
downloadMusic();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下载音乐
|
||||||
|
const downloadMusic = async () => {
|
||||||
|
if (isDownloading.value) {
|
||||||
|
message.warning('正在下载中,请稍候...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isDownloading.value = true;
|
||||||
|
const loadingMessage = message.loading('正在下载中...', { duration: 0 });
|
||||||
|
|
||||||
|
const url = await getSongUrl(props.item.id);
|
||||||
|
if (!url) {
|
||||||
|
loadingMessage.destroy();
|
||||||
|
message.error('获取音乐下载地址失败');
|
||||||
|
isDownloading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先移除可能存在的旧监听器
|
||||||
|
window.electron.ipcRenderer.removeAllListeners('music-download-complete');
|
||||||
|
|
||||||
|
// 发送下载请求
|
||||||
|
window.electron.ipcRenderer.send('download-music', {
|
||||||
|
url,
|
||||||
|
filename: `${props.item.name} - ${(props.item.ar || props.item.song?.artists)?.map(a => a.name).join(',')}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加新的一次性监听器
|
||||||
|
window.electron.ipcRenderer.once('music-download-complete', (_, result) => {
|
||||||
|
isDownloading.value = false;
|
||||||
|
loadingMessage.destroy();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
message.success('下载成功');
|
||||||
|
} else if (result.canceled) {
|
||||||
|
// 用户取消了保存
|
||||||
|
message.info('已取消下载');
|
||||||
|
} else {
|
||||||
|
message.error(`下载失败: ${result.error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
isDownloading.value = false;
|
||||||
|
message.destroyAll();
|
||||||
|
message.error('下载失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const emits = defineEmits(['play']);
|
||||||
const songImageRef = useTemplateRef('songImg');
|
const songImageRef = useTemplateRef('songImg');
|
||||||
|
|
||||||
const imageLoad = async () => {
|
const imageLoad = async () => {
|
||||||
@@ -207,6 +294,14 @@ const toggleFavorite = async (e: Event) => {
|
|||||||
@apply bg-green-500 border-green-500 text-white;
|
@apply bg-green-500 border-green-500 text-white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-download {
|
||||||
|
@apply mr-2 cursor-pointer;
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ const handleUpdate = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (downloadUrl) {
|
if (downloadUrl) {
|
||||||
window.open(downloadUrl, '_blank');
|
window.open(`https://ghproxy.cn/${downloadUrl}`, '_blank');
|
||||||
} else {
|
} else {
|
||||||
// 如果没有找到对应的安装包,跳转到 release 页面
|
// 如果没有找到对应的安装包,跳转到 release 页面
|
||||||
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { getImageLinearBackground } from '@/utils/linearColor';
|
|||||||
const musicHistory = useMusicHistory();
|
const musicHistory = useMusicHistory();
|
||||||
|
|
||||||
// 获取歌曲url
|
// 获取歌曲url
|
||||||
const getSongUrl = async (id: number) => {
|
export const getSongUrl = async (id: number) => {
|
||||||
const { data } = await getMusicUrl(id);
|
const { data } = await getMusicUrl(id);
|
||||||
let url = '';
|
let url = '';
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -21,13 +21,13 @@
|
|||||||
</router-view>
|
</router-view>
|
||||||
</div>
|
</div>
|
||||||
<play-bottom height="5rem" />
|
<play-bottom height="5rem" />
|
||||||
<app-menu v-if="isMobile" class="menu" :menus="menus" />
|
<app-menu v-if="isMobile && !store.state.musicFull" class="menu" :menus="menus" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 底部音乐播放 -->
|
<!-- 底部音乐播放 -->
|
||||||
<play-bar v-if="isPlay" />
|
<play-bar v-if="isPlay" :style="isMobile && store.state.musicFull ? 'bottom: 0;' : ''" />
|
||||||
</div>
|
</div>
|
||||||
<install-app-modal></install-app-modal>
|
<install-app-modal v-if="!isElectron"></install-app-modal>
|
||||||
<update-modal />
|
<update-modal />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ import {
|
|||||||
textColors,
|
textColors,
|
||||||
useLyricProgress
|
useLyricProgress
|
||||||
} from '@/hooks/MusicHook';
|
} from '@/hooks/MusicHook';
|
||||||
import { getImgUrl } from '@/utils';
|
import { getImgUrl, isMobile } from '@/utils';
|
||||||
import { animateGradient, getHoverBackgroundColor, getTextColors } from '@/utils/linearColor';
|
import { animateGradient, getHoverBackgroundColor, getTextColors } from '@/utils/linearColor';
|
||||||
|
|
||||||
// 定义 refs
|
// 定义 refs
|
||||||
@@ -116,9 +116,11 @@ const lrcScroll = (behavior = 'smooth') => {
|
|||||||
const debouncedLrcScroll = useDebounceFn(lrcScroll, 200);
|
const debouncedLrcScroll = useDebounceFn(lrcScroll, 200);
|
||||||
|
|
||||||
const mouseOverLayout = () => {
|
const mouseOverLayout = () => {
|
||||||
|
if(isMobile.value) {return}
|
||||||
isMouse.value = true;
|
isMouse.value = true;
|
||||||
};
|
};
|
||||||
const mouseLeaveLayout = () => {
|
const mouseLeaveLayout = () => {
|
||||||
|
if(isMobile.value) {return}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isMouse.value = false;
|
isMouse.value = false;
|
||||||
lrcScroll();
|
lrcScroll();
|
||||||
@@ -321,6 +323,9 @@ defineExpose({
|
|||||||
.music-lrc-text {
|
.music-lrc-text {
|
||||||
@apply text-xl text-center;
|
@apply text-xl text-center;
|
||||||
}
|
}
|
||||||
|
.music-content {
|
||||||
|
@apply h-[calc(100vh-120px)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 展开全屏 -->
|
<!-- 展开全屏 -->
|
||||||
<music-full ref="MusicFullRef" v-model:music-full="musicFullVisible" :background="background" />
|
|
||||||
<!-- 底部播放栏 -->
|
<!-- 底部播放栏 -->
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -141,6 +140,7 @@
|
|||||||
</n-popover>
|
</n-popover>
|
||||||
</div>
|
</div>
|
||||||
<!-- 播放音乐 -->
|
<!-- 播放音乐 -->
|
||||||
|
<music-full ref="MusicFullRef" v-model:music-full="musicFullVisible" :background="background" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -294,6 +294,7 @@ const musicFullVisible = ref(false);
|
|||||||
// 设置musicFull
|
// 设置musicFull
|
||||||
const setMusicFull = () => {
|
const setMusicFull = () => {
|
||||||
musicFullVisible.value = !musicFullVisible.value;
|
musicFullVisible.value = !musicFullVisible.value;
|
||||||
|
store.commit('setMusicFull', musicFullVisible.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const palyListRef = useTemplateRef('palyListRef');
|
const palyListRef = useTemplateRef('palyListRef');
|
||||||
@@ -432,8 +433,7 @@ const openLyricWindow = () => {
|
|||||||
|
|
||||||
.mobile {
|
.mobile {
|
||||||
.music-play-bar {
|
.music-play-bar {
|
||||||
@apply px-4;
|
@apply px-4 bottom-[70px] transition-all duration-300;
|
||||||
bottom: 70px;
|
|
||||||
}
|
}
|
||||||
.music-time {
|
.music-time {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -175,13 +175,17 @@ const search = () => {
|
|||||||
router.push({
|
router.push({
|
||||||
path: '/search',
|
path: '/search',
|
||||||
query: {
|
query: {
|
||||||
keyword: value
|
keyword: value,
|
||||||
|
type: store.state.searchType
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectSearchType = (key: number) => {
|
const selectSearchType = (key: number) => {
|
||||||
store.state.searchType = key;
|
store.state.searchType = key;
|
||||||
|
if (searchValue.value) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchTypeOptions = ref(SEARCH_TYPES);
|
const searchTypeOptions = ref(SEARCH_TYPES);
|
||||||
|
|||||||
@@ -10,14 +10,37 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showCloseModal"
|
||||||
|
preset="dialog"
|
||||||
|
title="关闭应用"
|
||||||
|
:style="{ width: '400px' }"
|
||||||
|
:mask-closable="true"
|
||||||
|
>
|
||||||
|
<div class="close-dialog-content">
|
||||||
|
<p>请选择关闭方式</p>
|
||||||
|
<div class="remember-choice">
|
||||||
|
<n-checkbox v-model:checked="rememberChoice">记住我的选择</n-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #action>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<n-button type="primary" @click="handleAction('minimize')">最小化到托盘</n-button>
|
||||||
|
<n-button @click="handleAction('close')">退出应用</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDialog } from 'naive-ui';
|
import { ref } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
import { isElectron } from '@/utils';
|
import { isElectron } from '@/utils';
|
||||||
|
|
||||||
const dialog = useDialog();
|
const store = useStore();
|
||||||
|
const showCloseModal = ref(false);
|
||||||
|
const rememberChoice = ref(false);
|
||||||
|
|
||||||
const minimize = () => {
|
const minimize = () => {
|
||||||
if (!isElectron) {
|
if (!isElectron) {
|
||||||
@@ -26,22 +49,40 @@ const minimize = () => {
|
|||||||
window.api.minimize();
|
window.api.minimize();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAction = (action: 'minimize' | 'close') => {
|
||||||
|
if (rememberChoice.value) {
|
||||||
|
store.commit('setSetData', {
|
||||||
|
...store.state.setData,
|
||||||
|
closeAction: action
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'minimize') {
|
||||||
|
window.api.miniTray();
|
||||||
|
} else {
|
||||||
|
window.api.close();
|
||||||
|
}
|
||||||
|
showCloseModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
if (!isElectron) {
|
if (!isElectron) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dialog.warning({
|
|
||||||
title: '提示',
|
const closeAction = store.state.setData.closeAction;
|
||||||
content: '确定要退出吗?',
|
|
||||||
positiveText: '最小化',
|
if (closeAction === 'minimize') {
|
||||||
negativeText: '关闭',
|
window.api.miniTray();
|
||||||
onPositiveClick: () => {
|
return;
|
||||||
window.api.minimize();
|
}
|
||||||
},
|
|
||||||
onNegativeClick: () => {
|
if (closeAction === 'close') {
|
||||||
window.api.close();
|
window.api.close();
|
||||||
}
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
showCloseModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const drag = (event: MouseEvent) => {
|
const drag = (event: MouseEvent) => {
|
||||||
@@ -68,4 +109,12 @@ const drag = (event: MouseEvent) => {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-dialog-content {
|
||||||
|
@apply flex flex-col gap-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
@apply flex gap-4 justify-end;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ interface State {
|
|||||||
favoriteList: number[];
|
favoriteList: number[];
|
||||||
playMode: number;
|
playMode: number;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
|
musicFull: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: State = {
|
const state: State = {
|
||||||
@@ -55,7 +56,8 @@ const state: State = {
|
|||||||
searchType: 1,
|
searchType: 1,
|
||||||
favoriteList: getLocalStorageItem('favoriteList', []),
|
favoriteList: getLocalStorageItem('favoriteList', []),
|
||||||
playMode: getLocalStorageItem('playMode', 0),
|
playMode: getLocalStorageItem('playMode', 0),
|
||||||
theme: getCurrentTheme()
|
theme: getCurrentTheme(),
|
||||||
|
musicFull: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
|
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
|
||||||
@@ -73,6 +75,9 @@ const mutations = {
|
|||||||
setPlayMusic(state: State, play: boolean) {
|
setPlayMusic(state: State, play: boolean) {
|
||||||
state.play = play;
|
state.play = play;
|
||||||
},
|
},
|
||||||
|
setMusicFull(state: State, musicFull: boolean) {
|
||||||
|
state.musicFull = musicFull;
|
||||||
|
},
|
||||||
setPlayList(state: State, playList: SongResult[]) {
|
setPlayList(state: State, playList: SongResult[]) {
|
||||||
state.playListIndex = playList.findIndex((item) => item.id === state.playMusic.id);
|
state.playListIndex = playList.findIndex((item) => item.id === state.playMusic.id);
|
||||||
state.playList = playList;
|
state.playList = playList;
|
||||||
|
|||||||
32
src/renderer/utils/fileOperation.ts
Normal file
32
src/renderer/utils/fileOperation.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { MessageApi } from 'naive-ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择目录
|
||||||
|
* @param message MessageApi 实例
|
||||||
|
* @returns Promise<string | undefined> 返回选择的目录路径,如果取消则返回 undefined
|
||||||
|
*/
|
||||||
|
export const selectDirectory = async (message: MessageApi): Promise<string | undefined> => {
|
||||||
|
try {
|
||||||
|
const result = await window.electron.ipcRenderer.invoke('select-directory');
|
||||||
|
if (result.filePaths?.[0]) {
|
||||||
|
return result.filePaths[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('选择目录失败');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开目录
|
||||||
|
* @param path 要打开的目录路径
|
||||||
|
* @param message MessageApi 实例
|
||||||
|
* @param showTip 是否显示提示信息
|
||||||
|
*/
|
||||||
|
export const openDirectory = (path: string | undefined, message: MessageApi, showTip = true) => {
|
||||||
|
if (path) {
|
||||||
|
window.electron.ipcRenderer.send('open-directory', path);
|
||||||
|
} else if (showTip) {
|
||||||
|
message.info('目录不存在');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
import axios, { InternalAxiosRequestConfig } from 'axios';
|
import axios, { InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import { isElectron } from '.';
|
||||||
|
|
||||||
const setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set')
|
let setData: any = null;
|
||||||
|
|
||||||
|
const getSetData = ()=>{
|
||||||
|
if (window.electron) {
|
||||||
|
setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getSetData()
|
||||||
// 扩展请求配置接口
|
// 扩展请求配置接口
|
||||||
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||||
retryCount?: number;
|
retryCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = window.electron ? `http://127.0.0.1:${setData.musicApiPort}` : import.meta.env.VITE_API;
|
const baseURL = window.electron ? `http://127.0.0.1:${setData?.musicApiPort}` : import.meta.env.VITE_API;
|
||||||
|
|
||||||
const request = axios.create({
|
const request = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
@@ -22,15 +29,18 @@ const RETRY_DELAY = 500;
|
|||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
request.interceptors.request.use(
|
request.interceptors.request.use(
|
||||||
(config: CustomAxiosRequestConfig) => {
|
(config: CustomAxiosRequestConfig) => {
|
||||||
// 初始化重试次数
|
getSetData();
|
||||||
config.retryCount = 0;
|
// 只在retryCount未定义时初始化为0
|
||||||
|
if (config.retryCount === undefined) {
|
||||||
|
config.retryCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// 在请求发送之前做一些处理
|
// 在请求发送之前做一些处理
|
||||||
// 在get请求params中添加timestamp
|
// 在get请求params中添加timestamp
|
||||||
if (config.method === 'get') {
|
if (config.method === 'get') {
|
||||||
config.params = {
|
config.params = {
|
||||||
...config.params,
|
...config.params,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -38,6 +48,16 @@ request.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(isElectron){
|
||||||
|
const proxyConfig = setData?.proxyConfig
|
||||||
|
if (proxyConfig?.enable && ['http', 'https'].includes(proxyConfig?.protocol)) {
|
||||||
|
config.params.proxy = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`
|
||||||
|
}
|
||||||
|
if(setData.enableRealIP && setData.realIP){
|
||||||
|
config.params.realIP = setData.realIP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -54,14 +74,15 @@ request.interceptors.response.use(
|
|||||||
async (error) => {
|
async (error) => {
|
||||||
const config = error.config as CustomAxiosRequestConfig;
|
const config = error.config as CustomAxiosRequestConfig;
|
||||||
|
|
||||||
// 如果没有配置重试次数,则初始化为0
|
// 如果没有配置,直接返回错误
|
||||||
if (!config || !config.retryCount) {
|
if (!config) {
|
||||||
config.retryCount = 0;
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否还可以重试
|
// 检查是否还可以重试
|
||||||
if (config.retryCount < MAX_RETRIES) {
|
if (config.retryCount !== undefined && config.retryCount < MAX_RETRIES) {
|
||||||
config.retryCount++;
|
config.retryCount++;
|
||||||
|
console.log(`请求重试第 ${config.retryCount} 次`);
|
||||||
|
|
||||||
// 延迟重试
|
// 延迟重试
|
||||||
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
|
||||||
@@ -70,6 +91,7 @@ request.interceptors.response.use(
|
|||||||
return request(config);
|
return request(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`重试${MAX_RETRIES}次后仍然失败`);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,8 +34,16 @@ export interface UpdateResult {
|
|||||||
*/
|
*/
|
||||||
export const getLatestReleaseInfo = async (): Promise<GithubReleaseInfo | null> => {
|
export const getLatestReleaseInfo = async (): Promise<GithubReleaseInfo | null> => {
|
||||||
try {
|
try {
|
||||||
|
const token = import.meta.env.VITE_GITHUB_TOKEN;
|
||||||
|
const headers = {};
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `token ${token}`;
|
||||||
|
}
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
'https://api.github.com/repos/algerkong/AlgerMusicPlayer/releases/latest'
|
'https://api.github.com/repos/algerkong/AlgerMusicPlayer/releases/latest',
|
||||||
|
{
|
||||||
|
headers
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
<!-- 歌单列表 -->
|
<!-- 歌单列表 -->
|
||||||
<n-scrollbar class="recommend" :size="100" @scroll="handleScroll">
|
<n-scrollbar class="recommend" style="height: calc(100% - 55px)" :size="100" @scroll="handleScroll">
|
||||||
<div v-loading="loading" class="recommend-list">
|
<div v-loading="loading" class="recommend-list">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in recommendList"
|
v-for="(item, index) in recommendList"
|
||||||
@@ -218,8 +218,6 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.recommend {
|
.recommend {
|
||||||
@apply w-full h-full;
|
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
@apply text-lg font-bold pb-2;
|
@apply text-lg font-bold pb-2;
|
||||||
@apply text-gray-900 dark:text-white;
|
@apply text-gray-900 dark:text-white;
|
||||||
@@ -325,5 +323,8 @@ watch(
|
|||||||
.play-list-type {
|
.play-list-type {
|
||||||
@apply mx-0 w-full;
|
@apply mx-0 w-full;
|
||||||
}
|
}
|
||||||
|
.categories-wrapper {
|
||||||
|
@apply pl-4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -292,4 +292,13 @@ const isPrevDisabled = computed(() => currentIndex.value === 0);
|
|||||||
@apply text-center py-4 col-span-full;
|
@apply text-center py-4 col-span-full;
|
||||||
@apply text-gray-500 dark:text-gray-400;
|
@apply text-gray-500 dark:text-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
.mv-list-content {
|
||||||
|
@apply pl-4 pr-4;
|
||||||
|
}
|
||||||
|
.categories-wrapper {
|
||||||
|
@apply pl-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -104,6 +104,15 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => searchType.value,
|
||||||
|
() => {
|
||||||
|
if (store.state.searchValue) {
|
||||||
|
loadSearch(store.state.searchValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;
|
const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;
|
||||||
const loadSearch = async (keywords: any, type: any = null) => {
|
const loadSearch = async (keywords: any, type: any = null) => {
|
||||||
hotKeyword.value = keywords;
|
hotKeyword.value = keywords;
|
||||||
|
|||||||
@@ -55,6 +55,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="set-item" v-if="isElectron">
|
||||||
|
<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 class="set-item">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">版本</div>
|
<div class="set-item-title">版本</div>
|
||||||
@@ -89,31 +101,125 @@
|
|||||||
@click="openAuthor"
|
@click="openAuthor"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">作者</div>
|
<Coffee>
|
||||||
<div class="set-item-content">algerkong github</div>
|
<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>{{ setData.author }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="set-item">
|
<div class="set-item" v-if="isElectron">
|
||||||
|
<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" v-if="isElectron">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">重启</div>
|
<div class="set-item-title">重启</div>
|
||||||
<div class="set-item-content">重启应用</div>
|
<div class="set-item-content">重启应用</div>
|
||||||
</div>
|
</div>
|
||||||
<n-button type="primary" @click="restartApp">重启</n-button>
|
<n-button type="primary" @click="restartApp">重启</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="set-item" v-if="isElectron">
|
||||||
|
<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" v-if="isElectron">
|
||||||
|
<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"
|
||||||
|
@blur="validateAndSaveRealIP"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PlayBottom/>
|
<PlayBottom/>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showProxyModal"
|
||||||
|
preset="dialog"
|
||||||
|
title="代理设置"
|
||||||
|
positive-text="确认"
|
||||||
|
negative-text="取消"
|
||||||
|
@positive-click="handleProxyConfirm"
|
||||||
|
@negative-click="showProxyModal = false"
|
||||||
|
:show-icon="false"
|
||||||
|
>
|
||||||
|
<n-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="proxyForm"
|
||||||
|
:rules="proxyRules"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="80"
|
||||||
|
require-mark-placement="right-hanging"
|
||||||
|
>
|
||||||
|
<n-form-item label="代理协议" path="protocol">
|
||||||
|
<n-select
|
||||||
|
v-model:value="proxyForm.protocol"
|
||||||
|
:options="[
|
||||||
|
{ label: 'HTTP', value: 'http' },
|
||||||
|
{ label: 'HTTPS', value: 'https' },
|
||||||
|
{ label: 'SOCKS5', value: 'socks5' }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="代理地址" path="host">
|
||||||
|
<n-input v-model:value="proxyForm.host" placeholder="请输入代理地址" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="代理端口" path="port">
|
||||||
|
<n-input-number v-model:value="proxyForm.port" placeholder="请输入代理端口" :min="1" :max="65535" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-modal>
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted } from 'vue';
|
import { computed, ref, onMounted, watch } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
|
import type { FormRules } from 'naive-ui';
|
||||||
import { isElectron } from '@/utils';
|
import { isElectron } from '@/utils';
|
||||||
import { checkUpdate, UpdateResult } from '@/utils/update';
|
import { checkUpdate, UpdateResult } from '@/utils/update';
|
||||||
|
import { selectDirectory, openDirectory } from '@/utils/fileOperation';
|
||||||
import config from '../../../../package.json';
|
import config from '../../../../package.json';
|
||||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||||
|
import Coffee from '@/components/Coffee.vue';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const checking = ref(false);
|
const checking = ref(false);
|
||||||
@@ -124,7 +230,25 @@ const updateInfo = ref<UpdateResult>({
|
|||||||
releaseInfo: null
|
releaseInfo: null
|
||||||
});
|
});
|
||||||
|
|
||||||
const setData = computed(() => store.state.setData);
|
const closeActionLabels = {
|
||||||
|
ask: '每次询问',
|
||||||
|
minimize: '最小化到托盘',
|
||||||
|
close: '直接退出'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const setData = computed(() => {
|
||||||
|
const data = store.state.setData;
|
||||||
|
// 确保代理配置存在
|
||||||
|
if (!data.proxyConfig) {
|
||||||
|
data.proxyConfig = {
|
||||||
|
enable: false,
|
||||||
|
protocol: 'http',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 7890
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
|
||||||
watch(() => setData.value, (newVal) => {
|
watch(() => setData.value, (newVal) => {
|
||||||
store.commit('setSetData', newVal)
|
store.commit('setSetData', newVal)
|
||||||
@@ -169,14 +293,136 @@ const openReleasePage = () => {
|
|||||||
window.open(updateInfo.value.releaseInfo?.html_url || 'https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
window.open(updateInfo.value.releaseInfo?.html_url || 'https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectDownloadPath = async () => {
|
||||||
|
const path = await selectDirectory(message);
|
||||||
|
if (path) {
|
||||||
|
store.commit('setSetData', {
|
||||||
|
...setData.value,
|
||||||
|
downloadPath: path
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDownloadPath = () => {
|
||||||
|
openDirectory(setData.value.downloadPath, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showProxyModal = ref(false);
|
||||||
|
const formRef = ref();
|
||||||
|
const proxyForm = ref({
|
||||||
|
protocol: 'http',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 7890
|
||||||
|
});
|
||||||
|
|
||||||
|
const proxyRules: FormRules = {
|
||||||
|
protocol: {
|
||||||
|
required: true,
|
||||||
|
message: '请选择代理协议',
|
||||||
|
trigger: ['blur', 'change']
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入代理地址',
|
||||||
|
trigger: ['blur', 'change'],
|
||||||
|
validator: (_rule, value) => {
|
||||||
|
if (!value) return false;
|
||||||
|
// 简单的IP或域名验证
|
||||||
|
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$|^localhost$|^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/;
|
||||||
|
return ipRegex.test(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
port: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入有效的端口号(1-65535)',
|
||||||
|
trigger: ['blur', 'change'],
|
||||||
|
validator: (_rule, value) => {
|
||||||
|
return value >= 1 && value <= 65535;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化时从store获取代理配置
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
|
if (setData.value.proxyConfig) {
|
||||||
|
proxyForm.value = { ...setData.value.proxyConfig };
|
||||||
|
}
|
||||||
|
// 确保enableRealIP有默认值
|
||||||
|
if (setData.value.enableRealIP === undefined) {
|
||||||
|
store.commit('setSetData', {
|
||||||
|
...setData.value,
|
||||||
|
enableRealIP: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听代理配置变化
|
||||||
|
watch(() => setData.value.proxyConfig, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
proxyForm.value = {
|
||||||
|
protocol: newVal.protocol || 'http',
|
||||||
|
host: newVal.host || '127.0.0.1',
|
||||||
|
port: newVal.port || 7890
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
|
const handleProxyConfirm = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate();
|
||||||
|
// 保存代理配置时保留enable状态
|
||||||
|
store.commit('setSetData', {
|
||||||
|
...setData.value,
|
||||||
|
proxyConfig: {
|
||||||
|
enable: setData.value.proxyConfig?.enable || false,
|
||||||
|
protocol: proxyForm.value.protocol,
|
||||||
|
host: proxyForm.value.host,
|
||||||
|
port: proxyForm.value.port
|
||||||
|
}
|
||||||
|
});
|
||||||
|
showProxyModal.value = false;
|
||||||
|
message.success('代理设置已保存,重启应用后生效');
|
||||||
|
} catch (err) {
|
||||||
|
message.error('请检查输入是否正确');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateAndSaveRealIP = () => {
|
||||||
|
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||||
|
if (!setData.value.realIP || ipRegex.test(setData.value.realIP)) {
|
||||||
|
store.commit('setSetData', {
|
||||||
|
...setData.value,
|
||||||
|
realIP: setData.value.realIP,
|
||||||
|
enableRealIP: true
|
||||||
|
});
|
||||||
|
if (setData.value.realIP) {
|
||||||
|
message.success('真实IP设置已保存');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error('请输入有效的IP地址');
|
||||||
|
store.commit('setSetData', {
|
||||||
|
...setData.value,
|
||||||
|
realIP: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听enableRealIP变化,当关闭时清空realIP
|
||||||
|
watch(() => setData.value.enableRealIP, (newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
store.commit('setSetData', {
|
||||||
|
...setData.value,
|
||||||
|
realIP: '',
|
||||||
|
enableRealIP: false
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.set-page {
|
.set-page {
|
||||||
@apply p-4 bg-light dark:bg-dark;
|
@apply p-4 bg-light dark:bg-dark pb-20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.set-item {
|
.set-item {
|
||||||
|
|||||||
Reference in New Issue
Block a user