mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 06:30:49 +08:00
Compare commits
18 Commits
v4.3.0-dev
...
v4.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2cdd1d8d7 | ||
|
|
c90cfbf3cd | ||
|
|
2c5bfac439 | ||
|
|
1865bd95bc | ||
|
|
fd37015466 | ||
|
|
7df1c25168 | ||
|
|
ed9cf9c4c5 | ||
|
|
35b9cbfdbd | ||
|
|
df6da2eb9e | ||
|
|
2d966036bb | ||
|
|
499857a679 | ||
|
|
7624a1a71e | ||
|
|
05b85c4b7b | ||
|
|
27d5bd8f81 | ||
|
|
c5da42b67d | ||
|
|
5e484334de | ||
|
|
25b90fafdc | ||
|
|
a676136f48 |
@@ -3,7 +3,7 @@ description: 这个规则是项目描述
|
|||||||
globs:
|
globs:
|
||||||
alwaysApply: false
|
alwaysApply: false
|
||||||
---
|
---
|
||||||
您是 TypeScript、Node.j、Vue3、Electron、naive-ui、VueUse 和 Tailwind 方面的专家。
|
您是 TypeScript、Node.js、Vue3、Electron、naive-ui、VueUse 和 Tailwind 方面的专家。
|
||||||
|
|
||||||
项目结构
|
项目结构
|
||||||
- 这是 Electron 项目,使用 Vue3 和 Pinia 进行开发的第三方网易云音乐播放器。
|
- 这是 Electron 项目,使用 Vue3 和 Pinia 进行开发的第三方网易云音乐播放器。
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ require('@rushstack/eslint-patch/modern-module-resolution');
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'eslint-config-airbnb-base',
|
|
||||||
'@vue/typescript/recommended',
|
'@vue/typescript/recommended',
|
||||||
'plugin:vue/vue3-recommended',
|
'plugin:vue/vue3-recommended',
|
||||||
'plugin:vue-scoped-css/base',
|
'plugin:vue-scoped-css/base',
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,3 +25,5 @@ out
|
|||||||
.cursorrules
|
.cursorrules
|
||||||
|
|
||||||
.github/deploy_keys
|
.github/deploy_keys
|
||||||
|
|
||||||
|
resources/android/**/*
|
||||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,17 +1,23 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
## v4.3.0
|
## v4.4.0
|
||||||
|
## 更新时间 2025 年 4 月 23 日 00:16
|
||||||
|
> 如果更新遇到问题,请前往 <a href="http://donate.alger.fun/download" target="_blank">下载 AlgerMusicPlayer</a>
|
||||||
|
|
||||||
|
> 帮我点个 star <a href="https://github.com/algerkong/AlgerMusicPlayer" target="_blank">github star</a>
|
||||||
|
|
||||||
|
> 请我喝咖啡 ☕️ <a href="http://donate.alger.fun/donate" target="_blank">赏你</a>
|
||||||
|
|
||||||
|
> QQ群 976962720
|
||||||
|
|
||||||
### ✨ 新功能
|
### ✨ 新功能
|
||||||
- 歌曲下载内置封面歌词歌曲信息,添加无限制下载功能,优化下载页面添加下载记录清除功能 ([3b1488f](https://github.com/algerkong/AlgerMusicPlayer/commit/3b1488f)) (#123) ([988418e](https://github.com/algerkong/AlgerMusicPlayer/commit/988418e))
|
- 优化音源解析功能,添加音源配置,添加GD音乐台解析支持 ([ed9cf9c](https://github.com/algerkong/AlgerMusicPlayer/commit/ed9cf9c))
|
||||||
- 添加搜索功能至歌曲列表,支持名称、歌手、专辑搜索,支持拼音匹配 ([b593ca3](https://github.com/algerkong/AlgerMusicPlayer/commit/b593ca3)) (#126)
|
|
||||||
- 添加快捷键管理功能,支持全局和应用内快捷键的启用/禁用 ([c2983ba](https://github.com/algerkong/AlgerMusicPlayer/commit/c2983ba)) (#119)
|
|
||||||
- 优化歌单加载、播放逻辑,提升大型歌单加载性能 ([7bc8405](https://github.com/algerkong/AlgerMusicPlayer/commit/7bc8405))、([d7fea7f](https://github.com/algerkong/AlgerMusicPlayer/commit/d7fea7f))
|
|
||||||
- 添加直接播放首页歌单功能 ([5f4b53c](https://github.com/algerkong/AlgerMusicPlayer/commit/5f4b53c))
|
|
||||||
- 添加统计服务([a7f2045](https://github.com/algerkong/AlgerMusicPlayer/commit/a7f2045))
|
|
||||||
- 优化历史和收藏视图的加载体验 ([09f8837](https://github.com/algerkong/AlgerMusicPlayer/commit/09f8837))
|
|
||||||
- 优化歌词界面配置,提供更好的用户体验 ([55b50d7](https://github.com/algerkong/AlgerMusicPlayer/commit/55b50d7))
|
|
||||||
|
|
||||||
### 🐛 Bug 修复
|
### 🐛 Bug 修复
|
||||||
- 优化音乐封面显示逻辑,确保在缺失封面时使用默认图片 ([bb7d1e3](https://github.com/algerkong/AlgerMusicPlayer/commit/bb7d1e3))
|
- 修复下载管理切换 tab 程序卡死问题 ([27d5bd8](https://github.com/algerkong/AlgerMusicPlayer/commit/27d5bd8)) (#153)
|
||||||
- 优化桌面歌词行动态样式计算,提升歌词显示效果 ([541ff2b](https://github.com/algerkong/AlgerMusicPlayer/commit/541ff2b))
|
- 修复歌曲播放地址缓存导致播放失败问题,添加过期时间 ([76e55d4](https://github.com/algerkong/AlgerMusicPlayer/commit/76e55d4))
|
||||||
|
- 修复 Electron 版本更新导致的桌面歌词窗口出现边框的问题 ([a676136](https://github.com/algerkong/AlgerMusicPlayer/commit/a676136))
|
||||||
|
- 优化底部菜单位置和间距 修复移动端菜单不显示问题 ([25b90fa](https://github.com/algerkong/AlgerMusicPlayer/commit/25b90fa))
|
||||||
|
|
||||||
|
### 🔧 其他变更
|
||||||
|
- 更新项目依赖 ([35b9cbf](https://github.com/algerkong/AlgerMusicPlayer/commit/35b9cbf))([7624a1a](https://github.com/algerkong/AlgerMusicPlayer/commit/7624a1a))
|
||||||
101
android/.gitignore
vendored
Normal file
101
android/.gitignore
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||||
|
|
||||||
|
# Built application files
|
||||||
|
*.apk
|
||||||
|
*.aar
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# Files for the ART/Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||||
|
# release/
|
||||||
|
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Proguard folder generated by Eclipse
|
||||||
|
proguard/
|
||||||
|
|
||||||
|
# Log Files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Android Studio Navigation editor temp files
|
||||||
|
.navigation/
|
||||||
|
|
||||||
|
# Android Studio captures folder
|
||||||
|
captures/
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/tasks.xml
|
||||||
|
.idea/gradle.xml
|
||||||
|
.idea/assetWizardSettings.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
.idea/libraries
|
||||||
|
# Android Studio 3 in .gitignore file.
|
||||||
|
.idea/caches
|
||||||
|
.idea/modules.xml
|
||||||
|
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||||
|
.idea/navEditor.xml
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||||
|
#*.jks
|
||||||
|
#*.keystore
|
||||||
|
|
||||||
|
# External native build folder generated in Android Studio 2.2 and later
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Google Services (e.g. APIs or Firebase)
|
||||||
|
# google-services.json
|
||||||
|
|
||||||
|
# Freeline
|
||||||
|
freeline.py
|
||||||
|
freeline/
|
||||||
|
freeline_project_description.json
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots
|
||||||
|
fastlane/test_output
|
||||||
|
fastlane/readme.md
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
vcs.xml
|
||||||
|
|
||||||
|
# lint
|
||||||
|
lint/intermediates/
|
||||||
|
lint/generated/
|
||||||
|
lint/outputs/
|
||||||
|
lint/tmp/
|
||||||
|
# lint/reports/
|
||||||
|
|
||||||
|
# Android Profiling
|
||||||
|
*.hprof
|
||||||
|
|
||||||
|
# Cordova plugins for Capacitor
|
||||||
|
capacitor-cordova-android-plugins
|
||||||
|
|
||||||
|
# Copied web assets
|
||||||
|
app/src/main/assets/public
|
||||||
|
|
||||||
|
# Generated Config files
|
||||||
|
app/src/main/assets/capacitor.config.json
|
||||||
|
app/src/main/assets/capacitor.plugins.json
|
||||||
|
app/src/main/res/xml/config.xml
|
||||||
39
package.json
39
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "AlgerMusicPlayer",
|
"name": "AlgerMusicPlayer",
|
||||||
"version": "4.3.0",
|
"version": "4.4.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",
|
||||||
@@ -21,50 +21,49 @@
|
|||||||
"build:linux": "npm run build && electron-builder --linux"
|
"build:linux": "npm run build && electron-builder --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@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.6.2",
|
||||||
"font-list": "^1.5.1",
|
"font-list": "^1.5.1",
|
||||||
"netease-cloud-music-api-alger": "^4.26.1",
|
"netease-cloud-music-api-alger": "^4.26.1",
|
||||||
"node-id3": "^0.2.9",
|
"node-id3": "^0.2.9",
|
||||||
"node-machine-id": "^1.1.12",
|
"node-machine-id": "^1.1.12",
|
||||||
"vue-i18n": "9"
|
"vue-i18n": "^11.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
"@electron-toolkit/eslint-config": "^2.1.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
"@rushstack/eslint-patch": "^1.10.3",
|
"@rushstack/eslint-patch": "^1.10.3",
|
||||||
"@tailwindcss/postcss7-compat": "^2.2.4",
|
"@tailwindcss/postcss7-compat": "^2.2.4",
|
||||||
"@types/howler": "^2.2.12",
|
"@types/howler": "^2.2.12",
|
||||||
"@types/node": "^20.14.8",
|
"@types/node": "^20.14.8",
|
||||||
"@types/tinycolor2": "^1.4.6",
|
"@types/tinycolor2": "^1.4.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
||||||
"@typescript-eslint/parser": "^7.0.0",
|
"@typescript-eslint/parser": "^8.30.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/compiler-sfc": "^3.5.0",
|
"@vue/compiler-sfc": "^3.5.0",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
"@vue/eslint-config-typescript": "^14.5.0",
|
||||||
"@vue/runtime-core": "^3.5.0",
|
"@vue/runtime-core": "^3.5.0",
|
||||||
"@vueuse/core": "^11.0.3",
|
"@vueuse/core": "^11.3.0",
|
||||||
"@vueuse/electron": "^11.0.3",
|
"@vueuse/electron": "^11.3.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"electron": "^35.0.2",
|
"electron": "^35.2.0",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"electron-vite": "^3.0.0",
|
"electron-vite": "^3.1.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-prettier": "^10.1.2",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||||
"eslint-plugin-vue": "^9.26.0",
|
"eslint-plugin-vue": "^10.0.0",
|
||||||
"eslint-plugin-vue-scoped-css": "^2.7.2",
|
"eslint-plugin-vue-scoped-css": "^2.9.0",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "^15.0.4",
|
"marked": "^15.0.4",
|
||||||
|
|||||||
@@ -44,5 +44,6 @@ export default {
|
|||||||
message: {
|
message: {
|
||||||
downloadComplete: '{filename} download completed',
|
downloadComplete: '{filename} download completed',
|
||||||
downloadFailed: '{filename} download failed: {error}'
|
downloadFailed: '{filename} download failed: {error}'
|
||||||
}
|
},
|
||||||
|
loading: 'Loading...'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,6 +56,15 @@ export default {
|
|||||||
dolby: 'Dolby Atmos',
|
dolby: 'Dolby Atmos',
|
||||||
jymaster: 'Master'
|
jymaster: 'Master'
|
||||||
},
|
},
|
||||||
|
musicSources: 'Music Sources',
|
||||||
|
musicSourcesDesc: 'Select music sources for song resolution',
|
||||||
|
musicSourcesWarning: 'At least one music source must be selected',
|
||||||
|
musicUnblockEnable: 'Enable Music Unblocking',
|
||||||
|
musicUnblockEnableDesc: 'When enabled, attempts to resolve unplayable songs',
|
||||||
|
configureMusicSources: 'Configure Sources',
|
||||||
|
selectedMusicSources: 'Selected sources:',
|
||||||
|
noMusicSources: 'No sources selected',
|
||||||
|
gdmusicInfo: 'GD Music Station intelligently resolves music from multiple platforms automatically',
|
||||||
autoPlay: 'Auto Play',
|
autoPlay: 'Auto Play',
|
||||||
autoPlayDesc: 'Auto resume playback when reopening the app'
|
autoPlayDesc: 'Auto resume playback when reopening the app'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -43,5 +43,6 @@ export default {
|
|||||||
message: {
|
message: {
|
||||||
downloadComplete: '{filename} 下载完成',
|
downloadComplete: '{filename} 下载完成',
|
||||||
downloadFailed: '{filename} 下载失败: {error}'
|
downloadFailed: '{filename} 下载失败: {error}'
|
||||||
}
|
},
|
||||||
|
loading: '加载中...'
|
||||||
};
|
};
|
||||||
|
|||||||
5
src/i18n/lang/zh-CN/settings.json
Normal file
5
src/i18n/lang/zh-CN/settings.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"playback": {
|
||||||
|
"musicSources": "音源设置",
|
||||||
|
"musicSourcesDesc": "选择音乐解析使用的音源平台",
|
||||||
|
"musicSourcesWarning": "至少需要选择一个音源平台"
|
||||||
|
}
|
||||||
@@ -56,6 +56,15 @@ export default {
|
|||||||
dolby: '杜比全景声',
|
dolby: '杜比全景声',
|
||||||
jymaster: '超清母带'
|
jymaster: '超清母带'
|
||||||
},
|
},
|
||||||
|
musicSources: '音源设置',
|
||||||
|
musicSourcesDesc: '选择音乐解析使用的音源平台',
|
||||||
|
musicSourcesWarning: '至少需要选择一个音源平台',
|
||||||
|
musicUnblockEnable: '启用音乐解析',
|
||||||
|
musicUnblockEnableDesc: '开启后将尝试解析无法播放的音乐',
|
||||||
|
configureMusicSources: '配置音源',
|
||||||
|
selectedMusicSources: '已选音源:',
|
||||||
|
noMusicSources: '未选择音源',
|
||||||
|
gdmusicInfo: 'GD音乐台可自动解析多个平台音源,自动选择最佳结果',
|
||||||
autoPlay: '自动播放',
|
autoPlay: '自动播放',
|
||||||
autoPlayDesc: '重新打开应用时是否自动继续播放'
|
autoPlayDesc: '重新打开应用时是否自动继续播放'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -84,15 +84,18 @@ const createWin = () => {
|
|||||||
frame: false,
|
frame: false,
|
||||||
show: false,
|
show: false,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
opacity: 1,
|
||||||
hasShadow: false,
|
hasShadow: false,
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: true,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
|
roundedCorners: false,
|
||||||
// 添加跨屏幕支持选项
|
// 添加跨屏幕支持选项
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
contextIsolation: true
|
contextIsolation: true
|
||||||
}
|
},
|
||||||
|
backgroundColor: '#00000000'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听窗口关闭事件
|
// 监听窗口关闭事件
|
||||||
|
|||||||
@@ -122,20 +122,37 @@ export function initializeFileManager() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 获取已下载音乐列表
|
// 获取已下载音乐列表
|
||||||
ipcMain.handle('get-downloaded-music', () => {
|
ipcMain.handle('get-downloaded-music', async () => {
|
||||||
try {
|
try {
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||||
|
|
||||||
// 过滤出实际存在的文件
|
// 异步处理文件存在性检查
|
||||||
const validSongs = Object.entries(songInfos)
|
const entriesArray = Object.entries(songInfos);
|
||||||
.filter(([path]) => fs.existsSync(path))
|
const validEntriesPromises = await Promise.all(
|
||||||
.map(([_, info]) => info)
|
entriesArray.map(async ([path, info]) => {
|
||||||
|
try {
|
||||||
|
const exists = await fs.promises.access(path)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
return exists ? info : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking file existence:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 过滤有效的歌曲并排序
|
||||||
|
const validSongs = validEntriesPromises
|
||||||
|
.filter(song => song !== null)
|
||||||
.sort((a, b) => (b.downloadTime || 0) - (a.downloadTime || 0));
|
.sort((a, b) => (b.downloadTime || 0) - (a.downloadTime || 0));
|
||||||
|
|
||||||
// 更新存储,移除不存在的文件记录
|
// 更新存储,移除不存在的文件记录
|
||||||
const newSongInfos = validSongs.reduce((acc, song) => {
|
const newSongInfos = validSongs.reduce((acc, song) => {
|
||||||
acc[song.path] = song;
|
if (song && song.path) {
|
||||||
|
acc[song.path] = song;
|
||||||
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
store.set('downloadedSongs', newSongInfos);
|
store.set('downloadedSongs', newSongInfos);
|
||||||
@@ -175,6 +192,13 @@ export function initializeFileManager() {
|
|||||||
downloadStore.set('history', []);
|
downloadStore.set('history', []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加清除已下载音乐记录的处理函数
|
||||||
|
ipcMain.handle('clear-downloaded-music', () => {
|
||||||
|
const store = new Store();
|
||||||
|
store.set('downloadedSongs', {});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// 添加清除音频缓存的处理函数
|
// 添加清除音频缓存的处理函数
|
||||||
ipcMain.on('clear-audio-cache', () => {
|
ipcMain.on('clear-audio-cache', () => {
|
||||||
audioCacheStore.set('cache', {});
|
audioCacheStore.set('cache', {});
|
||||||
|
|||||||
@@ -5,16 +5,22 @@ import server from 'netease-cloud-music-api-alger/server';
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { unblockMusic } from './unblockMusic';
|
import { unblockMusic, type Platform } from './unblockMusic';
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
|
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
|
||||||
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
|
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理解锁音乐请求
|
// 设置音乐解析的处理程序
|
||||||
ipcMain.handle('unblock-music', async (_, id, data) => {
|
ipcMain.handle('unblock-music', async (_event, id, songData, enabledSources) => {
|
||||||
return unblockMusic(id, data);
|
try {
|
||||||
|
const result = await unblockMusic(id, songData, 1, enabledSources as Platform[]);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('音乐解析失败:', error);
|
||||||
|
return { error: (error as Error).message || '未知错误' };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function startMusicApi(): Promise<void> {
|
async function startMusicApi(): Promise<void> {
|
||||||
|
|||||||
@@ -21,5 +21,7 @@
|
|||||||
"downloadPath": "",
|
"downloadPath": "",
|
||||||
"language": "zh-CN",
|
"language": "zh-CN",
|
||||||
"alwaysShowDownloadButton": false,
|
"alwaysShowDownloadButton": false,
|
||||||
"unlimitedDownload": false
|
"unlimitedDownload": false,
|
||||||
|
"enableMusicUnblock": true,
|
||||||
|
"enabledMusicSources": ["migu", "kugou", "pyncmd", "bilibili", "youtube"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ interface SongData {
|
|||||||
name: string;
|
name: string;
|
||||||
artists: Array<{ name: string }>;
|
artists: Array<{ name: string }>;
|
||||||
album?: { name: string };
|
album?: { name: string };
|
||||||
|
ar?: Array<{ name: string }>;
|
||||||
|
al?: { name: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResponseData {
|
interface ResponseData {
|
||||||
@@ -27,24 +29,29 @@ interface UnblockResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 所有可用平台
|
||||||
|
export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'kuwo', 'bilibili', 'youtube'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 音乐解析函数
|
* 音乐解析函数
|
||||||
* @param id 歌曲ID
|
* @param id 歌曲ID
|
||||||
* @param songData 歌曲信息
|
* @param songData 歌曲信息
|
||||||
* @param retryCount 重试次数
|
* @param retryCount 重试次数
|
||||||
|
* @param enabledPlatforms 启用的平台列表,默认为所有平台
|
||||||
* @returns Promise<UnblockResult>
|
* @returns Promise<UnblockResult>
|
||||||
*/
|
*/
|
||||||
const unblockMusic = async (
|
const unblockMusic = async (
|
||||||
id: number | string,
|
id: number | string,
|
||||||
songData: SongData,
|
songData: SongData,
|
||||||
retryCount = 3
|
retryCount = 1,
|
||||||
|
enabledPlatforms?: Platform[]
|
||||||
): Promise<UnblockResult> => {
|
): Promise<UnblockResult> => {
|
||||||
// 所有可用平台
|
const platforms = enabledPlatforms || ALL_PLATFORMS;
|
||||||
const platforms: Platform[] = ['migu', 'kugou', 'pyncmd', 'joox', 'kuwo', 'bilibili', 'youtube'];
|
songData.album = songData.album || songData.al;
|
||||||
|
songData.artists = songData.artists || songData.ar;
|
||||||
const retry = async (attempt: number): Promise<UnblockResult> => {
|
const retry = async (attempt: number): Promise<UnblockResult> => {
|
||||||
try {
|
try {
|
||||||
const data = await match(parseInt(String(id), 10), platforms, songData);
|
const data = await match(parseInt(String(id), 10), platforms,songData);
|
||||||
const result: UnblockResult = {
|
const result: UnblockResult = {
|
||||||
data: {
|
data: {
|
||||||
data,
|
data,
|
||||||
@@ -58,7 +65,7 @@ const unblockMusic = async (
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (attempt < retryCount) {
|
if (attempt < retryCount) {
|
||||||
// 延迟重试,每次重试增加延迟时间
|
// 延迟重试,每次重试增加延迟时间
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
|
||||||
return retry(attempt + 1);
|
return retry(attempt + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
src/preload/index.d.ts
vendored
2
src/preload/index.d.ts
vendored
@@ -14,7 +14,7 @@ interface API {
|
|||||||
openLyric: () => void;
|
openLyric: () => void;
|
||||||
sendLyric: (data: any) => void;
|
sendLyric: (data: any) => void;
|
||||||
sendSong: (data: any) => void;
|
sendSong: (data: any) => void;
|
||||||
unblockMusic: (id: number, data: any) => Promise<any>;
|
unblockMusic: (id: number, data: any, enabledSources?: string[]) => Promise<any>;
|
||||||
onLyricWindowClosed: (callback: () => void) => void;
|
onLyricWindowClosed: (callback: () => void) => void;
|
||||||
startDownload: (url: string) => void;
|
startDownload: (url: string) => void;
|
||||||
onDownloadProgress: (callback: (progress: number, status: string) => void) => void;
|
onDownloadProgress: (callback: (progress: number, status: string) => void) => void;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const api = {
|
|||||||
openLyric: () => ipcRenderer.send('open-lyric'),
|
openLyric: () => ipcRenderer.send('open-lyric'),
|
||||||
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
||||||
sendSong: (data) => ipcRenderer.send('update-current-song', data),
|
sendSong: (data) => ipcRenderer.send('update-current-song', data),
|
||||||
unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id),
|
unblockMusic: (id, data, enabledSources) => ipcRenderer.invoke('unblock-music', id, data, enabledSources),
|
||||||
// 歌词窗口关闭事件
|
// 歌词窗口关闭事件
|
||||||
onLyricWindowClosed: (callback: () => void) => {
|
onLyricWindowClosed: (callback: () => void) => {
|
||||||
ipcRenderer.on('lyric-window-closed', () => callback());
|
ipcRenderer.on('lyric-window-closed', () => callback());
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { isElectron, isLyricWindow } from '@/utils';
|
|||||||
import { initAudioListeners } from './hooks/MusicHook';
|
import { initAudioListeners } from './hooks/MusicHook';
|
||||||
import { isMobile } from './utils';
|
import { isMobile } from './utils';
|
||||||
import { useAppShortcuts } from './utils/appShortcuts';
|
import { useAppShortcuts } from './utils/appShortcuts';
|
||||||
import { initShortcut } from './utils/shortcut';
|
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
@@ -120,8 +119,6 @@ onMounted(async () => {
|
|||||||
window.api.sendSong(cloneDeep(playerStore.playMusic));
|
window.api.sendSong(cloneDeep(playerStore.playMusic));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 初始化快捷键
|
|
||||||
initShortcut();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
189
src/renderer/api/gdmusic.ts
Normal file
189
src/renderer/api/gdmusic.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import type { MusicSourceType } from '@/type/music';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GD音乐台解析服务
|
||||||
|
*/
|
||||||
|
export interface GDMusicResponse {
|
||||||
|
url: string;
|
||||||
|
br: number;
|
||||||
|
size: number;
|
||||||
|
md5: string;
|
||||||
|
platform: string;
|
||||||
|
gain: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedMusicResult {
|
||||||
|
data: {
|
||||||
|
data: GDMusicResponse;
|
||||||
|
params: {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从GD音乐台解析音乐URL
|
||||||
|
* @param id 音乐ID
|
||||||
|
* @param data 音乐数据,包含名称和艺术家信息
|
||||||
|
* @param quality 音质设置
|
||||||
|
* @returns 解析后的音乐URL及相关信息
|
||||||
|
*/
|
||||||
|
export const parseFromGDMusic = async (
|
||||||
|
id: number,
|
||||||
|
data: any,
|
||||||
|
quality: string = '320'
|
||||||
|
): Promise<ParsedMusicResult | null> => {
|
||||||
|
try {
|
||||||
|
// 处理不同数据结构
|
||||||
|
if (!data) {
|
||||||
|
console.error('GD音乐台解析:歌曲数据为空');
|
||||||
|
throw new Error('歌曲数据为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
const songName = data.name || '';
|
||||||
|
let artistNames = '';
|
||||||
|
|
||||||
|
// 处理不同的艺术家字段结构
|
||||||
|
if (data.artists && Array.isArray(data.artists)) {
|
||||||
|
artistNames = data.artists.map(artist => artist.name).join(' ');
|
||||||
|
} else if (data.ar && Array.isArray(data.ar)) {
|
||||||
|
artistNames = data.ar.map(artist => artist.name).join(' ');
|
||||||
|
} else if (data.artist) {
|
||||||
|
artistNames = typeof data.artist === 'string' ? data.artist : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchQuery = `${songName} ${artistNames}`.trim();
|
||||||
|
|
||||||
|
if (!searchQuery || searchQuery.length < 2) {
|
||||||
|
console.error('GD音乐台解析:搜索查询过短', { name: songName, artists: artistNames });
|
||||||
|
throw new Error('搜索查询过短');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有可用的音乐源
|
||||||
|
const allSources = [
|
||||||
|
'tencent', 'kugou', 'kuwo', 'migu', 'netease',
|
||||||
|
'joox', 'ytmusic', 'spotify', 'qobuz', 'deezer'
|
||||||
|
] as MusicSourceType[];
|
||||||
|
|
||||||
|
console.log('GD音乐台开始搜索:', searchQuery);
|
||||||
|
|
||||||
|
// 依次尝试所有音源
|
||||||
|
for (const source of allSources) {
|
||||||
|
try {
|
||||||
|
const result = await searchAndGetUrl(source, searchQuery, quality);
|
||||||
|
if (result) {
|
||||||
|
console.log(`GD音乐台成功通过 ${result.source} 解析音乐!`);
|
||||||
|
// 返回符合原API格式的数据
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
url: result.url.replace(/\\/g, ''),
|
||||||
|
br: parseInt(result.br, 10) * 1000 || 320000,
|
||||||
|
size: result.size || 0,
|
||||||
|
md5: '',
|
||||||
|
platform: 'gdmusic',
|
||||||
|
gain: 0
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
id: parseInt(String(id), 10),
|
||||||
|
type: 'song'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`GD音乐台 ${source} 音源解析失败:`, error);
|
||||||
|
// 该音源失败,继续尝试下一个音源
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('GD音乐台所有音源均解析失败');
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GD音乐台解析完全失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取音质映射
|
||||||
|
* @param qualitySetting 设置中的音质选项
|
||||||
|
* @returns 映射到GD音乐台的音质参数
|
||||||
|
*/
|
||||||
|
export const getQualityMapping = (qualitySetting: string): string => {
|
||||||
|
const qualityMap: Record<string, string> = {
|
||||||
|
standard: '128',
|
||||||
|
higher: '320',
|
||||||
|
exhigh: '320',
|
||||||
|
lossless: '740',
|
||||||
|
hires: '999',
|
||||||
|
jyeffect: '999',
|
||||||
|
sky: '999',
|
||||||
|
dolby: '999',
|
||||||
|
jymaster: '999'
|
||||||
|
};
|
||||||
|
return qualityMap[qualitySetting] || '320';
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GDMusicUrlResult {
|
||||||
|
url: string;
|
||||||
|
br: string;
|
||||||
|
size: number;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = 'https://music-api.gdstudio.xyz/api.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在指定音源搜索歌曲并获取URL
|
||||||
|
* @param source 音源
|
||||||
|
* @param searchQuery 搜索关键词
|
||||||
|
* @param quality 音质
|
||||||
|
* @returns 音乐URL结果
|
||||||
|
*/
|
||||||
|
async function searchAndGetUrl(
|
||||||
|
source: MusicSourceType,
|
||||||
|
searchQuery: string,
|
||||||
|
quality: string
|
||||||
|
): Promise<GDMusicUrlResult | null> {
|
||||||
|
// 1. 搜索歌曲
|
||||||
|
const searchUrl = `${baseUrl}?types=search&source=${source}&name=${encodeURIComponent(searchQuery)}&count=1&pages=1`;
|
||||||
|
console.log(`GD音乐台尝试音源 ${source} 搜索:`, searchUrl);
|
||||||
|
|
||||||
|
const searchResponse = await axios.get(searchUrl, { timeout: 5000 });
|
||||||
|
|
||||||
|
if (searchResponse.data && Array.isArray(searchResponse.data) && searchResponse.data.length > 0) {
|
||||||
|
const firstResult = searchResponse.data[0];
|
||||||
|
if (!firstResult || !firstResult.id) {
|
||||||
|
console.log(`GD音乐台 ${source} 搜索结果无效`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackId = firstResult.id;
|
||||||
|
const trackSource = firstResult.source || source;
|
||||||
|
|
||||||
|
// 2. 获取歌曲URL
|
||||||
|
const songUrl = `${baseUrl}?types=url&source=${trackSource}&id=${trackId}&br=${quality}`;
|
||||||
|
console.log(`GD音乐台尝试获取 ${trackSource} 歌曲URL:`, songUrl);
|
||||||
|
|
||||||
|
const songResponse = await axios.get(songUrl, { timeout: 5000 });
|
||||||
|
|
||||||
|
if (songResponse.data && songResponse.data.url) {
|
||||||
|
return {
|
||||||
|
url: songResponse.data.url,
|
||||||
|
br: songResponse.data.br,
|
||||||
|
size: songResponse.data.size || 0,
|
||||||
|
source: trackSource
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log(`GD音乐台 ${trackSource} 未返回有效URL`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`GD音乐台 ${source} 搜索结果为空`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import type { ILyric } from '@/type/lyric';
|
|||||||
import { isElectron } from '@/utils';
|
import { isElectron } from '@/utils';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import requestMusic from '@/utils/request_music';
|
import requestMusic from '@/utils/request_music';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { parseFromGDMusic, getQualityMapping } from './gdmusic';
|
||||||
|
|
||||||
const { addData, getData, deleteData } = musicDB;
|
const { addData, getData, deleteData } = musicDB;
|
||||||
|
|
||||||
@@ -78,10 +80,39 @@ export const getMusicLrc = async (id: number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getParsingMusicUrl = (id: number, data: any) => {
|
export const getParsingMusicUrl = async (id: number, data: any) => {
|
||||||
if (isElectron) {
|
const settingStore = useSettingsStore();
|
||||||
return window.api.unblockMusic(id, data);
|
|
||||||
|
// 如果禁用了音乐解析功能,则直接返回空结果
|
||||||
|
if (!settingStore.setData.enableMusicUnblock) {
|
||||||
|
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否选择了GD音乐台解析
|
||||||
|
const enabledSources = settingStore.setData.enabledMusicSources || [];
|
||||||
|
if (enabledSources.includes('gdmusic')) {
|
||||||
|
// 获取音质设置并转换为GD音乐台格式
|
||||||
|
try {
|
||||||
|
const quality = getQualityMapping(settingStore.setData.musicQuality || 'higher');
|
||||||
|
|
||||||
|
// 调用封装的GD音乐台解析服务
|
||||||
|
const gdResult = await parseFromGDMusic(id, data, quality);
|
||||||
|
if (gdResult) {
|
||||||
|
return gdResult;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GD音乐台解析失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('GD音乐台所有音源均解析失败,尝试使用unblockMusic');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果GD音乐台解析失败或者未启用,尝试使用unblockMusic
|
||||||
|
if (isElectron) {
|
||||||
|
const filteredSources = enabledSources.filter(source => source !== 'gdmusic');
|
||||||
|
return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources));
|
||||||
|
}
|
||||||
|
|
||||||
return requestMusic.get<any>('/music', { params: { id } });
|
return requestMusic.get<any>('/music', { params: { id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2
src/renderer/components.d.ts
vendored
2
src/renderer/components.d.ts
vendored
@@ -28,6 +28,8 @@ declare module 'vue' {
|
|||||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NForm: typeof import('naive-ui')['NForm']
|
NForm: typeof import('naive-ui')['NForm']
|
||||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
|
NGrid: typeof import('naive-ui')['NGrid']
|
||||||
|
NGridItem: typeof import('naive-ui')['NGridItem']
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
NImage: typeof import('naive-ui')['NImage']
|
NImage: typeof import('naive-ui')['NImage']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
|||||||
@@ -90,7 +90,11 @@
|
|||||||
<!-- 已下载列表 -->
|
<!-- 已下载列表 -->
|
||||||
<n-tab-pane name="downloaded" :tab="t('download.tabs.downloaded')" class="h-full">
|
<n-tab-pane name="downloaded" :tab="t('download.tabs.downloaded')" class="h-full">
|
||||||
<div class="downloaded-list">
|
<div class="downloaded-list">
|
||||||
<div v-if="downloadedList.length === 0" class="empty-tip">
|
<div v-if="isLoadingDownloaded" class="loading-tip">
|
||||||
|
<n-spin size="medium" />
|
||||||
|
<span class="loading-text">{{ t('download.loading') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="downloadedList.length === 0" class="empty-tip">
|
||||||
<n-empty :description="t('download.empty.noDownloaded')" />
|
<n-empty :description="t('download.empty.noDownloaded')" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="downloaded-content">
|
<div v-else class="downloaded-content">
|
||||||
@@ -262,9 +266,7 @@ const downloadedList = ref<DownloadedItem[]>(
|
|||||||
JSON.parse(localStorage.getItem('downloadedList') || '[]')
|
JSON.parse(localStorage.getItem('downloadedList') || '[]')
|
||||||
);
|
);
|
||||||
|
|
||||||
const downList = computed(() => {
|
const downList = computed(() => downloadedList.value);
|
||||||
return (downloadedList.value as DownloadedItem[]).reverse();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算下载中的任务数量
|
// 计算下载中的任务数量
|
||||||
const downloadingCount = computed(() => {
|
const downloadingCount = computed(() => {
|
||||||
@@ -350,38 +352,25 @@ const handleDelete = (item: DownloadedItem) => {
|
|||||||
|
|
||||||
// 确认删除
|
// 确认删除
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (!itemToDelete.value) return;
|
const item = itemToDelete.value;
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const success = await window.electron.ipcRenderer.invoke(
|
const success = await window.electron.ipcRenderer.invoke(
|
||||||
'delete-downloaded-music',
|
'delete-downloaded-music',
|
||||||
itemToDelete.value.path
|
item.path
|
||||||
);
|
);
|
||||||
|
|
||||||
// 无论删除文件是否成功,都从记录中移除
|
|
||||||
localStorage.setItem(
|
|
||||||
'downloadedList',
|
|
||||||
JSON.stringify(
|
|
||||||
downloadedList.value.filter((item) => item.id !== (itemToDelete.value as DownloadedItem).id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await refreshDownloadedList();
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
const newList = downloadedList.value.filter(i => i.id !== item.id);
|
||||||
|
downloadedList.value = newList;
|
||||||
|
localStorage.setItem('downloadedList', JSON.stringify(newList));
|
||||||
message.success(t('download.delete.success'));
|
message.success(t('download.delete.success'));
|
||||||
} else {
|
} else {
|
||||||
message.warning(t('download.delete.fileNotFound'));
|
message.warning(t('download.delete.fileNotFound'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete music:', error);
|
console.error('Failed to delete music:', error);
|
||||||
// 即使删除文件出错,也从记录中移除
|
|
||||||
localStorage.setItem(
|
|
||||||
'downloadedList',
|
|
||||||
JSON.stringify(
|
|
||||||
downloadedList.value.filter((item) => item.id !== (itemToDelete.value as DownloadedItem).id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await refreshDownloadedList();
|
|
||||||
message.warning(t('download.delete.recordRemoved'));
|
message.warning(t('download.delete.recordRemoved'));
|
||||||
} finally {
|
} finally {
|
||||||
showDeleteConfirm.value = false;
|
showDeleteConfirm.value = false;
|
||||||
@@ -393,11 +382,18 @@ const confirmDelete = async () => {
|
|||||||
const showClearConfirm = ref(false);
|
const showClearConfirm = ref(false);
|
||||||
|
|
||||||
// 清空下载记录
|
// 清空下载记录
|
||||||
const clearDownloadRecords = () => {
|
const clearDownloadRecords = async () => {
|
||||||
localStorage.setItem('downloadedList', '[]');
|
try {
|
||||||
downloadedList.value = [];
|
downloadedList.value = [];
|
||||||
message.success(t('download.clear.success'));
|
localStorage.setItem('downloadedList', '[]');
|
||||||
showClearConfirm.value = false;
|
await window.electron.ipcRenderer.invoke('clear-downloaded-music');
|
||||||
|
message.success(t('download.clear.success'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear download records:', error);
|
||||||
|
message.error(t('download.clear.failed'));
|
||||||
|
} finally {
|
||||||
|
showClearConfirm.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 播放音乐
|
// 播放音乐
|
||||||
@@ -407,65 +403,64 @@ const clearDownloadRecords = () => {
|
|||||||
// playerStore.setIsPlay(true);
|
// playerStore.setIsPlay(true);
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
// 添加加载状态
|
||||||
|
const isLoadingDownloaded = ref(false);
|
||||||
|
|
||||||
// 获取已下载音乐列表
|
// 获取已下载音乐列表
|
||||||
const refreshDownloadedList = async () => {
|
const refreshDownloadedList = async () => {
|
||||||
|
if (isLoadingDownloaded.value) return; // 防止重复加载
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let saveList: any = [];
|
isLoadingDownloaded.value = true;
|
||||||
const list = await window.electron.ipcRenderer.invoke('get-downloaded-music');
|
const list = await window.electron.ipcRenderer.invoke('get-downloaded-music');
|
||||||
|
|
||||||
if (!Array.isArray(list) || list.length === 0) {
|
if (!Array.isArray(list) || list.length === 0) {
|
||||||
saveList = [];
|
downloadedList.value = [];
|
||||||
|
localStorage.setItem('downloadedList', '[]');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const songIds = list.filter((item) => item.id).map((item) => item.id);
|
const songIds = list.filter(item => item.id).map(item => item.id);
|
||||||
|
if (songIds.length === 0) {
|
||||||
// 如果有歌曲ID,获取详细信息
|
downloadedList.value = list;
|
||||||
if (songIds.length > 0) {
|
localStorage.setItem('downloadedList', JSON.stringify(list));
|
||||||
try {
|
return;
|
||||||
const detailRes = await getMusicDetail(songIds);
|
}
|
||||||
const songDetails = detailRes.data.songs.reduce((acc, song) => {
|
|
||||||
acc[song.id] = song;
|
try {
|
||||||
return acc;
|
const detailRes = await getMusicDetail(songIds);
|
||||||
}, {});
|
const songDetails = detailRes.data.songs.reduce((acc, song) => {
|
||||||
|
acc[song.id] = song;
|
||||||
saveList = list.map((item) => {
|
return acc;
|
||||||
const songDetail = songDetails[item.id];
|
}, {});
|
||||||
return {
|
|
||||||
...item,
|
const updatedList = list.map(item => ({
|
||||||
picUrl: songDetail?.al?.picUrl || item.picUrl || '/images/default_cover.png',
|
...item,
|
||||||
ar: songDetail?.ar || item.ar || [{ name: t('download.localMusic') }]
|
picUrl: songDetails[item.id]?.al?.picUrl || item.picUrl || '/images/default_cover.png',
|
||||||
};
|
ar: songDetails[item.id]?.ar || item.ar || [{ name: t('download.localMusic') }]
|
||||||
});
|
}));
|
||||||
} catch (detailError) {
|
|
||||||
console.error('Failed to get music details:', detailError);
|
downloadedList.value = updatedList;
|
||||||
saveList = list;
|
localStorage.setItem('downloadedList', JSON.stringify(updatedList));
|
||||||
}
|
} catch (error) {
|
||||||
} else {
|
console.error('Failed to get music details:', error);
|
||||||
saveList = list;
|
downloadedList.value = list;
|
||||||
|
localStorage.setItem('downloadedList', JSON.stringify(list));
|
||||||
}
|
}
|
||||||
setLocalDownloadedList(saveList);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get downloaded music list:', error);
|
console.error('Failed to get downloaded music list:', error);
|
||||||
downloadedList.value = [];
|
downloadedList.value = [];
|
||||||
|
localStorage.setItem('downloadedList', '[]');
|
||||||
|
} finally {
|
||||||
|
isLoadingDownloaded.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setLocalDownloadedList = (list: DownloadedItem[]) => {
|
|
||||||
const localList = localStorage.getItem('downloadedList');
|
|
||||||
// 合并 去重
|
|
||||||
const saveList = [...(localList ? JSON.parse(localList) : []), ...list];
|
|
||||||
const uniqueList = saveList.filter(
|
|
||||||
(item, index, self) => index === self.findIndex((t) => t.id === item.id)
|
|
||||||
);
|
|
||||||
localStorage.setItem('downloadedList', JSON.stringify(uniqueList));
|
|
||||||
downloadedList.value = uniqueList;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听抽屉显示状态
|
// 监听抽屉显示状态
|
||||||
watch(
|
watch(
|
||||||
() => showDrawer.value,
|
() => showDrawer.value,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (newVal) {
|
if (newVal && !isLoadingDownloaded.value) {
|
||||||
refreshDownloadedList();
|
refreshDownloadedList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -503,15 +498,14 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 监听下载完成
|
// 监听下载完成
|
||||||
window.electron.ipcRenderer.on('music-download-complete', (_, data) => {
|
window.electron.ipcRenderer.on('music-download-complete', async (_, data) => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// 从下载列表中移除
|
downloadList.value = downloadList.value.filter(item => item.filename !== data.filename);
|
||||||
downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);
|
// 延迟刷新已下载列表,避免文件系统未完全写入
|
||||||
// 刷新已下载列表
|
setTimeout(() => refreshDownloadedList(), 500);
|
||||||
refreshDownloadedList();
|
|
||||||
message.success(t('download.message.downloadComplete', { filename: data.filename }));
|
message.success(t('download.message.downloadComplete', { filename: data.filename }));
|
||||||
} else {
|
} else {
|
||||||
const existingItem = downloadList.value.find((item) => item.filename === data.filename);
|
const existingItem = downloadList.value.find(item => item.filename === data.filename);
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
Object.assign(existingItem, {
|
Object.assign(existingItem, {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
@@ -519,12 +513,10 @@ onMounted(() => {
|
|||||||
progress: 0
|
progress: 0
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);
|
downloadList.value = downloadList.value.filter(item => item.filename !== data.filename);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
message.error(
|
message.error(t('download.message.downloadFailed', { filename: data.filename, error: data.error }));
|
||||||
t('download.message.downloadFailed', { filename: data.filename, error: data.error })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ provide('openPlaylistDrawer', openPlaylistDrawer);
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: block;
|
display: block;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
padding-bottom: 70px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -113,8 +113,10 @@ const isText = ref(false);
|
|||||||
.app-menu {
|
.app-menu {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
position: relative;
|
position: fixed;
|
||||||
z-index: 999999;
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 99999;
|
||||||
@apply bg-light dark:bg-black border-t border-gray-200 dark:border-gray-700;
|
@apply bg-light dark:bg-black border-t border-gray-200 dark:border-gray-700;
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
@@ -122,7 +124,7 @@ const isText = ref(false);
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-list {
|
&-list {
|
||||||
@apply flex justify-between;
|
@apply flex justify-between px-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export const getSongUrl = async (
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error);
|
console.error('error', error);
|
||||||
|
url = data.data[0].url || '';
|
||||||
}
|
}
|
||||||
if (isDownloaded) {
|
if (isDownloaded) {
|
||||||
return songDetail;
|
return songDetail;
|
||||||
@@ -344,7 +345,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
(item: SongResult) => item.id === music.id && item.source === music.source
|
(item: SongResult) => item.id === music.id && item.source === music.source
|
||||||
);
|
);
|
||||||
|
|
||||||
fetchSongs(playList.value, playListIndex.value + 1, playListIndex.value + 6);
|
fetchSongs(playList.value, playListIndex.value + 1, playListIndex.value + 3);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPlay = async (song: SongResult) => {
|
const setPlay = async (song: SongResult) => {
|
||||||
@@ -453,7 +454,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await handlePlayMusic(prevSong);
|
await handlePlayMusic(prevSong);
|
||||||
await fetchSongs(playList.value, playListIndex.value - 5, nowPlayListIndex);
|
await fetchSongs(playList.value, playListIndex.value - 3, nowPlayListIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePlayMode = () => {
|
const togglePlayMode = () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep, merge } from 'lodash';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
@@ -7,17 +7,6 @@ import { isElectron } from '@/utils';
|
|||||||
import { applyTheme, getCurrentTheme, ThemeType } from '@/utils/theme';
|
import { applyTheme, getCurrentTheme, ThemeType } from '@/utils/theme';
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
// 初始化时先从存储中读取设置
|
|
||||||
const getInitialSettings = () => {
|
|
||||||
if (isElectron) {
|
|
||||||
const savedSettings = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
|
|
||||||
return savedSettings || setDataDefault;
|
|
||||||
}
|
|
||||||
const savedSettings = localStorage.getItem('appSettings');
|
|
||||||
return savedSettings ? JSON.parse(savedSettings) : setDataDefault;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setData = ref(getInitialSettings());
|
|
||||||
const theme = ref<ThemeType>(getCurrentTheme());
|
const theme = ref<ThemeType>(getCurrentTheme());
|
||||||
const isMobile = ref(false);
|
const isMobile = ref(false);
|
||||||
const isMiniMode = ref(false);
|
const isMiniMode = ref(false);
|
||||||
@@ -28,7 +17,11 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
{ label: '系统默认', value: 'system-ui' }
|
{ label: '系统默认', value: 'system-ui' }
|
||||||
]);
|
]);
|
||||||
const showDownloadDrawer = ref(false);
|
const showDownloadDrawer = ref(false);
|
||||||
|
|
||||||
|
// 先声明 setData ref 但不初始化
|
||||||
|
const setData = ref<any>({});
|
||||||
|
|
||||||
|
// 先定义 setSetData 函数
|
||||||
const setSetData = (data: any) => {
|
const setSetData = (data: any) => {
|
||||||
// 合并现有设置和新设置
|
// 合并现有设置和新设置
|
||||||
const mergedData = {
|
const mergedData = {
|
||||||
@@ -44,6 +37,24 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
setData.value = cloneDeep(mergedData);
|
setData.value = cloneDeep(mergedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 初始化时先从存储中读取设置
|
||||||
|
const getInitialSettings = () => {
|
||||||
|
// 从存储中获取保存的设置
|
||||||
|
const savedSettings = isElectron
|
||||||
|
? window.electron.ipcRenderer.sendSync('get-store-value', 'set')
|
||||||
|
: JSON.parse(localStorage.getItem('appSettings') || '{}');
|
||||||
|
|
||||||
|
// 合并默认设置和保存的设置
|
||||||
|
const mergedSettings = merge({}, setDataDefault, savedSettings);
|
||||||
|
|
||||||
|
// 更新设置并返回
|
||||||
|
setSetData(mergedSettings);
|
||||||
|
return mergedSettings;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化 setData
|
||||||
|
setData.value = getInitialSettings();
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
theme.value = theme.value === 'dark' ? 'light' : 'dark';
|
theme.value = theme.value === 'dark' ? 'light' : 'dark';
|
||||||
applyTheme(theme.value);
|
applyTheme(theme.value);
|
||||||
|
|||||||
@@ -239,3 +239,19 @@ export interface IArtists {
|
|||||||
img1v1: number;
|
img1v1: number;
|
||||||
trans: null;
|
trans: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 音乐源类型定义
|
||||||
|
export type MusicSourceType =
|
||||||
|
| 'tencent'
|
||||||
|
| 'kugou'
|
||||||
|
| 'kuwo'
|
||||||
|
| 'migu'
|
||||||
|
| 'netease'
|
||||||
|
| 'joox'
|
||||||
|
| 'ytmusic'
|
||||||
|
| 'spotify'
|
||||||
|
| 'qobuz'
|
||||||
|
| 'deezer'
|
||||||
|
| 'gdmusic';
|
||||||
|
|
||||||
|
// 更多音乐相关的类型可以在这里定义
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { isElectron } from '.';
|
|
||||||
import { handleShortcutAction } from './appShortcuts';
|
|
||||||
|
|
||||||
export function initShortcut() {
|
|
||||||
if (isElectron) {
|
|
||||||
window.electron.ipcRenderer.on('global-shortcut', async (_, action: string) => {
|
|
||||||
handleShortcutAction(action);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -172,6 +172,16 @@ const handleMouseLeave = () => {
|
|||||||
if (!lyricSetting.value.isLock) return;
|
if (!lyricSetting.value.isLock) return;
|
||||||
isHovering.value = false;
|
isHovering.value = false;
|
||||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', false);
|
windowData.electron.ipcRenderer.send('set-ignore-mouse', false);
|
||||||
|
|
||||||
|
// 强制重置背景色
|
||||||
|
const lyricWindow = document.querySelector('.lyric-window') as HTMLElement;
|
||||||
|
if (lyricWindow) {
|
||||||
|
lyricWindow.style.background = 'transparent';
|
||||||
|
// 使用 requestAnimationFrame 确保在下一帧重置
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
lyricWindow.style.background = 'transparent';
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听锁定状态变化
|
// 监听锁定状态变化
|
||||||
@@ -633,8 +643,12 @@ const handleNext = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
body {
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border: none !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -644,14 +658,13 @@ body {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: transparent;
|
background: transparent !important;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.3s ease;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(44, 44, 44, 0.466);
|
|
||||||
.control-bar {
|
.control-bar {
|
||||||
&-show {
|
&-show {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -670,7 +683,7 @@ body {
|
|||||||
--highlight-color: #1db954;
|
--highlight-color: #1db954;
|
||||||
--control-bg: rgba(124, 124, 124, 0.3);
|
--control-bg: rgba(124, 124, 124, 0.3);
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(44, 44, 44, 0.466);
|
background: rgba(44, 44, 44, 0.466) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,7 +693,7 @@ body {
|
|||||||
--highlight-color: #1db954;
|
--highlight-color: #1db954;
|
||||||
--control-bg: rgba(255, 255, 255, 0.3);
|
--control-bg: rgba(255, 255, 255, 0.3);
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, 0.434);
|
background: rgba(0, 0, 0, 0.434) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,39 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="set-item">
|
||||||
|
<div>
|
||||||
|
<div class="set-item-title">{{ t('settings.playback.musicSources') }}</div>
|
||||||
|
<div class="set-item-content">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<n-switch v-model:value="setData.enableMusicUnblock">
|
||||||
|
<template #checked>{{ t('common.on') }}</template>
|
||||||
|
<template #unchecked>{{ t('common.off') }}</template>
|
||||||
|
</n-switch>
|
||||||
|
<span>{{ t('settings.playback.musicUnblockEnableDesc') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="setData.enableMusicUnblock" class="mt-2">
|
||||||
|
<div class="text-sm">
|
||||||
|
<span class="text-gray-500">{{ t('settings.playback.selectedMusicSources') }}</span>
|
||||||
|
<span v-if="musicSources.length > 0" class="text-gray-400">
|
||||||
|
{{ musicSources.map((source) => getSourceLabel(source)).join(', ') }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-red-500 text-xs">
|
||||||
|
{{ t('settings.playback.noMusicSources') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:disabled="!setData.enableMusicUnblock"
|
||||||
|
@click="showMusicSourcesModal = true"
|
||||||
|
>
|
||||||
|
{{ t('settings.playback.configureMusicSources') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="set-item">
|
<div class="set-item">
|
||||||
<div>
|
<div>
|
||||||
<div class="set-item-title">{{ t('settings.playback.autoPlay') }}</div>
|
<div class="set-item-title">{{ t('settings.playback.autoPlay') }}</div>
|
||||||
@@ -470,6 +503,51 @@
|
|||||||
</n-checkbox-group>
|
</n-checkbox-group>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
||||||
|
<!-- 音源设置弹窗 -->
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showMusicSourcesModal"
|
||||||
|
preset="dialog"
|
||||||
|
:title="t('settings.playback.musicSources')"
|
||||||
|
:positive-text="t('common.confirm')"
|
||||||
|
:negative-text="t('common.cancel')"
|
||||||
|
@positive-click="showMusicSourcesModal = false"
|
||||||
|
@negative-click="showMusicSourcesModal = false"
|
||||||
|
>
|
||||||
|
<n-space vertical>
|
||||||
|
<p>{{ t('settings.playback.musicSourcesDesc') }}</p>
|
||||||
|
<n-checkbox-group v-model:value="musicSources">
|
||||||
|
<n-grid :cols="2" :x-gap="12" :y-gap="8">
|
||||||
|
<n-grid-item v-for="source in musicSourceOptions" :key="source.value">
|
||||||
|
<n-checkbox :value="source.value">
|
||||||
|
{{ source.label }}
|
||||||
|
<template v-if="source.value === 'gdmusic'">
|
||||||
|
<n-tooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon size="16" class="ml-1 text-blue-500 cursor-help">
|
||||||
|
<i class="ri-information-line"></i>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('settings.playback.gdmusicInfo') }}
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
</n-checkbox>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</n-checkbox-group>
|
||||||
|
<div v-if="musicSources.length === 0" class="text-red-500 text-sm">
|
||||||
|
{{ t('settings.playback.musicSourcesWarning') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GD音乐台设置 -->
|
||||||
|
<div v-if="musicSources.includes('gdmusic')" class="mt-4 border-t pt-4 border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 class="text-base font-medium mb-2">GD音乐台(music.gdstudio.xyz)设置</h3>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
GD音乐台将自动尝试多个音乐平台进行解析,无需额外配置。优先级高于其他解析方式,但是请求可能较慢。感谢(music.gdstudio.xyz)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</n-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -494,6 +572,11 @@ import { checkUpdate, UpdateResult } from '@/utils/update';
|
|||||||
|
|
||||||
import config from '../../../../package.json';
|
import config from '../../../../package.json';
|
||||||
|
|
||||||
|
// 手动定义Platform类型,避免从主进程导入的问题
|
||||||
|
type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili' | 'youtube' | 'gdmusic';
|
||||||
|
// 所有平台
|
||||||
|
const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili', 'youtube'];
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
@@ -977,6 +1060,42 @@ onMounted(() => {
|
|||||||
handleScroll({ target: { scrollTop: 0 } });
|
handleScroll({ target: { scrollTop: 0 } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 音源设置相关
|
||||||
|
const musicSourceOptions = ref([
|
||||||
|
{ label: 'MiGu音乐', value: 'migu' },
|
||||||
|
{ label: '酷狗音乐', value: 'kugou' },
|
||||||
|
{ label: 'pyncmd', value: 'pyncmd' },
|
||||||
|
{ label: '酷我音乐', value: 'kuwo' },
|
||||||
|
{ label: 'Bilibili音乐', value: 'bilibili' },
|
||||||
|
{ label: 'YouTube', value: 'youtube' },
|
||||||
|
{ label: 'GD音乐台', value: 'gdmusic' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 已选择的音源列表
|
||||||
|
const musicSources = computed({
|
||||||
|
get: () => {
|
||||||
|
if (!setData.value.enabledMusicSources) {
|
||||||
|
return ALL_PLATFORMS;
|
||||||
|
}
|
||||||
|
return setData.value.enabledMusicSources as Platform[];
|
||||||
|
},
|
||||||
|
set: (newValue: Platform[]) => {
|
||||||
|
// 确保至少选择一个音源
|
||||||
|
const valuesToSet = newValue.length > 0 ? [...new Set(newValue)] : ALL_PLATFORMS;
|
||||||
|
setData.value = {
|
||||||
|
...setData.value,
|
||||||
|
enabledMusicSources: valuesToSet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const showMusicSourcesModal = ref(false);
|
||||||
|
|
||||||
|
const getSourceLabel = (source: Platform) => {
|
||||||
|
const sourceLabel = musicSourceOptions.value.find(s => s.value === source)?.label;
|
||||||
|
return sourceLabel || source;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user