mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-05 23:50:52 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5213aa13c5 | ||
|
|
d870d0198f | ||
|
|
976a9afd2f | ||
|
|
018218a5bf | ||
|
|
38a9d6ed31 |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,21 +1,8 @@
|
||||
# 更新日志
|
||||
|
||||
## [v3.0.0] - 2024-03-21
|
||||
## [v3.1.0] - 2024-01-02
|
||||
|
||||
### ✨ 新功能
|
||||
- 新增自动更新检测功能
|
||||
- 新增 GitHub Actions 自动构建和发布
|
||||
- 新增主题色切换功能,支持日间/夜间模式 (#19, #21)
|
||||
- 新增随机播放功能 (#20)
|
||||
- 优化主题效果和图片清晰度
|
||||
|
||||
### 🏗️ 架构重构
|
||||
- 重构整个项目架构
|
||||
- 优化打包配置
|
||||
- 修改后台服务为本地运行
|
||||
- 优化项目结构
|
||||
|
||||
### 🐞 问题修复
|
||||
- 修复 web 移动端页面空白问题 (#24)
|
||||
- 修复无用导入问题
|
||||
- 优化错误处理
|
||||
- 优化主入口代码,添加歌曲下载功能 (018218a)
|
||||
- 完善网页版安装应用功能 (38a9d6e)
|
||||
- 修改更新检查功能 (8dab799)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "AlgerMusicPlayer",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"description": "Alger Music Player",
|
||||
"author": "Alger <algerkc@qq.com>",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -26,7 +26,6 @@
|
||||
"@unblockneteasemusic/server": "^0.27.8-patch.1",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
|
||||
"netease-cloud-music-api-alger": "^4.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils';
|
||||
import { app, BrowserWindow, globalShortcut, ipcMain, Menu, nativeImage, shell, Tray } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils';
|
||||
import { app, globalShortcut, ipcMain, nativeImage } from 'electron';
|
||||
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 icon = nativeImage.createFromPath(
|
||||
@@ -13,109 +19,50 @@ const icon = nativeImage.createFromPath(
|
||||
: join(iconPath, 'icon.png')
|
||||
);
|
||||
|
||||
import { loadLyricWindow } from './lyric';
|
||||
import { startMusicApi } from './server';
|
||||
let mainWindow: Electron.BrowserWindow;
|
||||
|
||||
let mainWindow: BrowserWindow;
|
||||
function createWindow(): void {
|
||||
startMusicApi();
|
||||
// Create the browser window.
|
||||
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'));
|
||||
}
|
||||
// 初始化应用
|
||||
function initialize() {
|
||||
// 初始化各个模块
|
||||
initializeConfig();
|
||||
initializeFileManager();
|
||||
|
||||
// 创建托盘图标
|
||||
const trayIcon = nativeImage.createFromPath(join(iconPath, 'icon_16x16.png')).resize({ width: 16, height: 16 });
|
||||
const 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();
|
||||
}
|
||||
});
|
||||
|
||||
// 创建主窗口
|
||||
mainWindow = createMainWindow(icon);
|
||||
|
||||
// 初始化窗口管理
|
||||
initializeWindowManager();
|
||||
|
||||
// 初始化托盘
|
||||
initializeTray(iconPath, mainWindow);
|
||||
|
||||
// 启动音乐API
|
||||
startMusicApi();
|
||||
|
||||
// 加载歌词窗口
|
||||
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(() => {
|
||||
// Set app user model id for windows
|
||||
// 设置应用ID
|
||||
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) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
// IPC test
|
||||
ipcMain.on('ping', () => console.log('pong'));
|
||||
|
||||
createWindow();
|
||||
// 初始化应用
|
||||
initialize();
|
||||
|
||||
// macOS 激活应用时的处理
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
if (mainWindow === null) initialize();
|
||||
});
|
||||
});
|
||||
|
||||
// 应用程序准备就绪后的快捷键设置
|
||||
app.on('ready', () => {
|
||||
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
||||
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', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
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', () => {
|
||||
app.relaunch();
|
||||
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) => {
|
||||
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;
|
||||
}
|
||||
81
src/main/modules/window.ts
Normal file
81
src/main/modules/window.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { BrowserWindow, shell, ipcMain } from 'electron';
|
||||
import { is } from '@electron-toolkit/utils';
|
||||
import { join } from 'path';
|
||||
|
||||
/**
|
||||
* 初始化窗口管理相关的IPC监听
|
||||
*/
|
||||
export function initializeWindowManager() {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('mini-tray', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (win) {
|
||||
win.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建主窗口
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@@ -49,7 +49,8 @@
|
||||
|
||||
<script setup>
|
||||
import { NButton, NImage, NPopover } from 'naive-ui';
|
||||
|
||||
import alipay from '@/assets/alipay.png';
|
||||
import wechat from '@/assets/wechat.png';
|
||||
const message = useMessage();
|
||||
const copyQQ = () => {
|
||||
navigator.clipboard.writeText('789288579');
|
||||
@@ -59,11 +60,11 @@ const copyQQ = () => {
|
||||
defineProps({
|
||||
alipayQR: {
|
||||
type: String,
|
||||
required: true
|
||||
default: alipay
|
||||
},
|
||||
wechatQR: {
|
||||
type: String,
|
||||
required: true
|
||||
default: wechat
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -39,13 +39,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { isElectron, isMobile } from '@/utils';
|
||||
|
||||
import config from '../../../../package.json';
|
||||
import { getLatestReleaseInfo } from '@/utils/update';
|
||||
|
||||
const showModal = ref(false);
|
||||
const noPrompt = ref(false);
|
||||
const releaseInfo = ref<any>(null);
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
@@ -54,7 +54,7 @@ const closeModal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 如果是 electron 环境,不显示安装提示
|
||||
if (isElectron || isMobile.value) {
|
||||
return;
|
||||
@@ -65,33 +65,59 @@ onMounted(() => {
|
||||
if (isDismissed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取最新版本信息
|
||||
releaseInfo.value = await getLatestReleaseInfo();
|
||||
showModal.value = true;
|
||||
});
|
||||
|
||||
const handleInstall = async (): Promise<void> => {
|
||||
const assets = releaseInfo.value?.assets || [];
|
||||
const { userAgent } = navigator;
|
||||
console.log('userAgent', userAgent);
|
||||
const isMac: boolean = userAgent.includes('Mac');
|
||||
const isWindows: boolean = userAgent.includes('Win');
|
||||
const isARM: boolean =
|
||||
userAgent.includes('ARM') || userAgent.includes('arm') || userAgent.includes('OS X');
|
||||
const isX64: boolean =
|
||||
userAgent.includes('x86_64') || userAgent.includes('Win64') || userAgent.includes('WOW64');
|
||||
const isX86: boolean =
|
||||
!isX64 &&
|
||||
(userAgent.includes('i686') || userAgent.includes('i386') || userAgent.includes('Win32'));
|
||||
const isMac = userAgent.toLowerCase().includes('mac');
|
||||
const isWindows = userAgent.toLowerCase().includes('win');
|
||||
const isLinux = userAgent.toLowerCase().includes('linux');
|
||||
const isX64 = userAgent.includes('x86_64') ||
|
||||
userAgent.includes('Win64') ||
|
||||
userAgent.includes('WOW64');
|
||||
|
||||
const getDownloadUrl = (os: string, arch: string): string => {
|
||||
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;
|
||||
let downloadUrl = '';
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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
|
||||
v-if="item.picUrl"
|
||||
ref="songImg"
|
||||
@@ -57,17 +57,30 @@
|
||||
<i v-else class="iconfont icon-playfill"></i>
|
||||
</div>
|
||||
</div>
|
||||
<n-dropdown
|
||||
v-if="isElectron"
|
||||
:show="showDropdown"
|
||||
:options="dropdownOptions"
|
||||
:x="dropdownX"
|
||||
:y="dropdownY"
|
||||
placement="bottom-start"
|
||||
@clickoutside="showDropdown = false"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { computed, h, ref, useTemplateRef } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
|
||||
import { audioService } from '@/services/audioService';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl } from '@/utils';
|
||||
import { getImgUrl, isElectron } from '@/utils';
|
||||
import { getImageBackground } from '@/utils/linearColor';
|
||||
import { getSongUrl } from '@/hooks/MusicListHook';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -84,22 +97,96 @@ const props = withDefaults(
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
const message = useMessage();
|
||||
|
||||
const play = computed(() => store.state.play as boolean);
|
||||
|
||||
const playMusic = computed(() => store.state.playMusic);
|
||||
|
||||
const playLoading = computed(
|
||||
() => playMusic.value.id === props.item.id && playMusic.value.playLoading
|
||||
);
|
||||
|
||||
// 判断是否为正在播放的音乐
|
||||
const isPlaying = computed(() => {
|
||||
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 imageLoad = async () => {
|
||||
@@ -207,6 +294,14 @@ const toggleFavorite = async (e: Event) => {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getImageLinearBackground } from '@/utils/linearColor';
|
||||
const musicHistory = useMusicHistory();
|
||||
|
||||
// 获取歌曲url
|
||||
const getSongUrl = async (id: number) => {
|
||||
export const getSongUrl = async (id: number) => {
|
||||
const { data } = await getMusicUrl(id);
|
||||
let url = '';
|
||||
try {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<!-- 底部音乐播放 -->
|
||||
<play-bar v-if="isPlay" />
|
||||
</div>
|
||||
<install-app-modal></install-app-modal>
|
||||
<install-app-modal v-if="!isElectron"></install-app-modal>
|
||||
<update-modal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
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,17 @@
|
||||
import axios, { InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
const setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set')
|
||||
let setData: any = null;
|
||||
|
||||
if (window.electron) {
|
||||
setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
|
||||
}
|
||||
|
||||
// 扩展请求配置接口
|
||||
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||
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({
|
||||
baseURL,
|
||||
|
||||
@@ -55,6 +55,18 @@
|
||||
</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>
|
||||
<div class="set-item-title">版本</div>
|
||||
@@ -89,12 +101,18 @@
|
||||
@click="openAuthor"
|
||||
>
|
||||
<div>
|
||||
<div class="set-item-title">作者</div>
|
||||
<div class="set-item-content">algerkong github</div>
|
||||
<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>{{ setData.author }}</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">重启应用</div>
|
||||
@@ -112,8 +130,10 @@ import { useStore } from 'vuex';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { isElectron } from '@/utils';
|
||||
import { checkUpdate, UpdateResult } from '@/utils/update';
|
||||
import { selectDirectory, openDirectory } from '@/utils/fileOperation';
|
||||
import config from '../../../../package.json';
|
||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||
import Coffee from '@/components/Coffee.vue';
|
||||
|
||||
const store = useStore();
|
||||
const checking = ref(false);
|
||||
@@ -169,6 +189,20 @@ const openReleasePage = () => {
|
||||
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);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkForUpdates();
|
||||
});
|
||||
@@ -176,7 +210,7 @@ onMounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.set-page {
|
||||
@apply p-4 bg-light dark:bg-dark;
|
||||
@apply p-4 bg-light dark:bg-dark pb-20;
|
||||
}
|
||||
|
||||
.set-item {
|
||||
|
||||
Reference in New Issue
Block a user