Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dbfea240b | ||
|
|
c1344393c3 | ||
|
|
426888f77c | ||
|
|
45cbc15c0f | ||
|
|
072025a543 | ||
|
|
c6427aa3e1 | ||
|
|
632cdb1239 | ||
|
|
8ffe472605 | ||
|
|
8e86d378d0 | ||
|
|
744fd53fb1 | ||
|
|
3c64473dbb | ||
|
|
e70fed37da | ||
|
|
b749854c5e | ||
|
|
d9210cc50a | ||
|
|
f186d34885 | ||
|
|
ba992b7c33 | ||
|
|
24d7c839c7 | ||
|
|
a4f3df80c9 | ||
|
|
866fec6ee3 | ||
|
|
8f7d6fbb8d | ||
|
|
62e26cae7d | ||
|
|
ddb814da10 | ||
|
|
e266ea8ef8 | ||
|
|
a894954641 | ||
|
|
f640ab9969 | ||
|
|
9eb17fd978 | ||
|
|
020aca7384 | ||
|
|
fcc47dc0ff | ||
|
|
17ce268da6 | ||
|
|
43c64b1b43 | ||
|
|
11ced6b418 | ||
|
|
3d3992154a | ||
|
|
81e7b67c7f | ||
|
|
d7e94a342b | ||
|
|
46f8067577 | ||
|
|
1dc7d0ceca | ||
|
|
ba64631a17 | ||
|
|
cdb9524f04 | ||
|
|
5213aa13c5 | ||
|
|
d870d0198f | ||
|
|
976a9afd2f | ||
|
|
018218a5bf | ||
|
|
38a9d6ed31 | ||
|
|
8dab799939 | ||
|
|
1ddbe6f24e | ||
|
|
4d5bcba6c7 | ||
|
|
f833306b60 | ||
|
|
4d92ed9963 | ||
|
|
a22285156a | ||
|
|
d1029f16d6 | ||
|
|
4908555635 | ||
|
|
750cf7a484 | ||
|
|
a334743f6f | ||
|
|
14747cac10 | ||
|
|
cc239aeaba | ||
|
|
eeda296589 | ||
|
|
edb7ea201c | ||
|
|
17d20fa299 | ||
|
|
f8d421c9b1 | ||
|
|
dfdf02a17f | ||
|
|
abdb2bcd50 | ||
|
|
f728191a8f | ||
|
|
dfa8b51a53 | ||
|
|
b2c13121fd | ||
|
|
d28adb61a4 | ||
|
|
9a7d5a3834 | ||
|
|
2037798fbe | ||
|
|
85bd0ad015 | ||
|
|
e1557a51a3 | ||
|
|
1ecc6f136f | ||
|
|
53b3061b03 | ||
|
|
3d2f6a2330 | ||
|
|
3b1470f28f | ||
|
|
100268448a | ||
|
|
51f67bb2c2 | ||
|
|
7be126cf5f | ||
|
|
f2f5d3ac15 | ||
|
|
34c45e0105 | ||
|
|
f9333f5f78 | ||
|
|
7365daf700 | ||
|
|
cebf313075 | ||
|
|
bb99049991 | ||
|
|
df74dafbc5 | ||
|
|
721d2a9704 | ||
|
|
1e60fa9a95 | ||
|
|
f24e8232f8 | ||
|
|
a1b1d861ac | ||
|
|
f24263b416 | ||
|
|
17795e5da2 | ||
|
|
f1030d3a78 | ||
|
|
b979ce250f | ||
|
|
d0d8966875 | ||
|
|
d39ba65263 | ||
|
|
62d400827e | ||
|
|
75b99c46b5 | ||
|
|
e7ae79144c | ||
|
|
04d6cbe7f3 | ||
|
|
bea1e5751f | ||
|
|
f2ebb04fab | ||
|
|
42048764d5 | ||
|
|
e326253fd8 | ||
|
|
edf5c77ea0 | ||
|
|
8870390770 | ||
|
|
c9514e6e19 | ||
|
|
08fa160de4 | ||
|
|
5d4c4922fd | ||
|
|
c5e7c87658 | ||
|
|
f6923b4c47 | ||
|
|
4cf7598a7d | ||
|
|
81b09bef0d | ||
|
|
b21df3de25 | ||
|
|
c49d814182 | ||
|
|
1cb3c72ab7 | ||
|
|
f03372de6a | ||
|
|
d925f40303 | ||
|
|
dc12d895d8 | ||
|
|
0bb14902f2 | ||
|
|
3027a5f6ff | ||
|
|
f320f4760b | ||
|
|
e939933d6f | ||
|
|
06bffe7618 | ||
|
|
7abc087d70 | ||
|
|
eb2ea1981d | ||
|
|
6dc14ec51b | ||
|
|
36f8257a3e | ||
|
|
c55544df46 | ||
|
|
008f2183de | ||
|
|
dd3a3c3bbb | ||
|
|
941eb2e66e | ||
|
|
a98fcb43d6 | ||
|
|
791121ae06 | ||
|
|
0c156e2708 | ||
|
|
017b47fded | ||
|
|
e27ed22c16 | ||
|
|
904d8744ef | ||
|
|
800e0b7360 | ||
|
|
b6a5461a1d | ||
|
|
a4eda61a86 | ||
|
|
a79d0712a4 | ||
|
|
8f782cdc9d | ||
|
|
2f851f3172 |
@@ -1,3 +1,12 @@
|
||||
VITE_API = /api
|
||||
VITE_API_MUSIC = /music
|
||||
VITE_API_PROXY = http://110.42.251.190:9856
|
||||
# 你的接口地址 (必填)
|
||||
VITE_API_LOCAL = ***
|
||||
# 音乐破解接口地址
|
||||
VITE_API_MUSIC = ***
|
||||
# 代理地址
|
||||
VITE_API_PROXY = ***
|
||||
|
||||
|
||||
# 本地运行代理地址
|
||||
VITE_API_PROXY = /api
|
||||
VITE_API_MUSIC_PROXY = /music
|
||||
VITE_API_PROXY_MUSIC = /music_proxy
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
VITE_API = http://110.42.251.190:9898
|
||||
VITE_API_MUSIC = http://110.42.251.190:4100
|
||||
VITE_API_PROXY = http://110.42.251.190:9856
|
||||
@@ -1,13 +1,4 @@
|
||||
snapshot*
|
||||
dist
|
||||
lib
|
||||
es
|
||||
esm
|
||||
node_modules
|
||||
static
|
||||
cypress
|
||||
script/test/cypress
|
||||
_site
|
||||
temp*
|
||||
static/
|
||||
!.prettierrc.js
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
|
||||
133
.eslintrc
@@ -1,133 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"eslint-config-airbnb-base",
|
||||
"@vue/typescript/recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"plugin:vue-scoped-css/base",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jest": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"defineProps": "readonly",
|
||||
"defineEmits": "readonly"
|
||||
},
|
||||
"plugins": [
|
||||
"vue",
|
||||
"@typescript-eslint",
|
||||
"simple-import-sort"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"sourceType": "module",
|
||||
"allowImportExportEverywhere": true,
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"import/extensions": [
|
||||
".js",
|
||||
".jsx",
|
||||
".ts",
|
||||
".tsx"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"no-continue": "off",
|
||||
"no-restricted-syntax": "off",
|
||||
"no-plusplus": "off",
|
||||
"no-param-reassign": "off",
|
||||
"no-shadow": "off",
|
||||
"guard-for-in": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"import/first": "off", // https://github.com/vuejs/vue-eslint-parser/issues/58
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"vue/first-attribute-linebreak": 0,
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"class-methods-use-this": "off", // 因为AxiosCancel必须实例化而能静态化所以加的规则,如果有办法解决可以取消
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.vue"
|
||||
],
|
||||
"rules": {
|
||||
"vue/component-name-in-template-casing": [
|
||||
2,
|
||||
"kebab-case"
|
||||
],
|
||||
"vue/require-default-prop": 0,
|
||||
"vue/multi-word-component-names": 0,
|
||||
"vue/no-reserved-props": 0,
|
||||
"vue/no-v-html": 0,
|
||||
"vue-scoped-css/enforce-style-type": [
|
||||
"error",
|
||||
{
|
||||
"allows": [
|
||||
"scoped"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
], // https://github.com/typescript-eslint eslint-recommended
|
||||
"rules": {
|
||||
"constructor-super": "off", // ts(2335) & ts(2377)
|
||||
"getter-return": "off", // ts(2378)
|
||||
"no-const-assign": "off", // ts(2588)
|
||||
"no-dupe-args": "off", // ts(2300)
|
||||
"no-dupe-class-members": "off", // ts(2393) & ts(2300)
|
||||
"no-dupe-keys": "off", // ts(1117)
|
||||
"no-func-assign": "off", // ts(2539)
|
||||
"no-import-assign": "off", // ts(2539) & ts(2540)
|
||||
"no-new-symbol": "off", // ts(2588)
|
||||
"no-obj-calls": "off", // ts(2349)
|
||||
"no-redeclare": "off", // ts(2451)
|
||||
"no-setter-return": "off", // ts(2408)
|
||||
"no-this-before-super": "off", // ts(2376)
|
||||
"no-undef": "off", // ts(2304)
|
||||
"no-unreachable": "off", // ts(7027)
|
||||
"no-unsafe-negation": "off", // ts(2365) & ts(2360) & ts(2358)
|
||||
"no-var": "error", // ts transpiles let/const to var, so no need for vars any more
|
||||
"prefer-const": "error", // ts provides better types with const
|
||||
"prefer-rest-params": "error", // ts provides better types with rest args over arguments
|
||||
"prefer-spread": "error", // ts transpiles spread to apply, so no need for manual apply
|
||||
"valid-typeof": "off" // ts(2367)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
137
.eslintrc.cjs
Normal file
@@ -0,0 +1,137 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution');
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'eslint-config-airbnb-base',
|
||||
'@vue/typescript/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vue-scoped-css/base',
|
||||
'@electron-toolkit',
|
||||
'@electron-toolkit/eslint-config-ts/eslint-recommended',
|
||||
'plugin:prettier/recommended'
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
jest: true,
|
||||
es6: true
|
||||
},
|
||||
globals: {
|
||||
defineProps: 'readonly',
|
||||
defineEmits: 'readonly'
|
||||
},
|
||||
plugins: ['vue', '@typescript-eslint', 'simple-import-sort'],
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
allowImportExportEverywhere: true,
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
'import/extensions': ['.js', '.jsx', '.ts', '.tsx']
|
||||
},
|
||||
rules: {
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'no-nested-ternary': 'off',
|
||||
'no-console': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'no-continue': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
'no-return-assign': 'off',
|
||||
'no-unused-expressions': 'off',
|
||||
'no-return-await': 'off',
|
||||
'no-plusplus': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'no-shadow': 'off',
|
||||
'guard-for-in': 'off',
|
||||
'import/extensions': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/first': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'vue/first-attribute-linebreak': 0,
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_'
|
||||
}
|
||||
],
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_'
|
||||
}
|
||||
],
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'class-methods-use-this': 'off',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.vue'],
|
||||
rules: {
|
||||
'vue/component-name-in-template-casing': [2, 'kebab-case'],
|
||||
'vue/require-default-prop': 0,
|
||||
'vue/multi-word-component-names': 0,
|
||||
'vue/no-reserved-props': 0,
|
||||
'vue/no-v-html': 0,
|
||||
'vue-scoped-css/enforce-style-type': [
|
||||
'error',
|
||||
{
|
||||
allows: ['scoped']
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
// 需要行尾分号
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
rules: {
|
||||
'max-classes-per-file': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'dot-notation': 'off',
|
||||
'constructor-super': 'off',
|
||||
'getter-return': 'off',
|
||||
'no-const-assign': 'off',
|
||||
'no-dupe-args': 'off',
|
||||
'no-dupe-class-members': 'off',
|
||||
'no-dupe-keys': 'off',
|
||||
'no-func-assign': 'off',
|
||||
'no-import-assign': 'off',
|
||||
'no-new-symbol': 'off',
|
||||
'no-obj-calls': 'off',
|
||||
'no-redeclare': 'off',
|
||||
'no-setter-return': 'off',
|
||||
'no-this-before-super': 'off',
|
||||
'no-undef': 'off',
|
||||
'no-unreachable': 'off',
|
||||
'no-unsafe-negation': 'off',
|
||||
'no-var': 'error',
|
||||
'prefer-const': 'error',
|
||||
'prefer-rest-params': 'error',
|
||||
'prefer-spread': 'error',
|
||||
'valid-typeof': 'off',
|
||||
'consistent-return': 'off',
|
||||
'no-promise-executor-return': 'off',
|
||||
'prefer-promise-reject-errors': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
87
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
# MacOS Build
|
||||
- name: Build MacOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
export ELECTRON_BUILDER_EXTRA_ARGS="--universal"
|
||||
npm run build:mac
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||
DEBUG: electron-builder
|
||||
|
||||
# Windows Build
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: npm run build:win
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Linux Build
|
||||
- name: Build Linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||
npm run build:linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Get version from tag
|
||||
- name: Get version from tag
|
||||
id: get_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
# Read release notes
|
||||
- name: Read release notes
|
||||
id: release_notes
|
||||
run: |
|
||||
NOTES=$(awk "/## \[v${{ env.VERSION }}\]/{p=1;print;next} /## \[v/{p=0}p" CHANGELOG.md)
|
||||
echo "NOTES<<EOF" >> $GITHUB_ENV
|
||||
echo "$NOTES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload artifacts
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
dist/*.dmg
|
||||
dist/*.exe
|
||||
dist/*.deb
|
||||
dist/*.AppImage
|
||||
dist/latest*.yml
|
||||
dist/*.blockmap
|
||||
body: ${{ env.NOTES }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
8
.gitignore
vendored
@@ -14,3 +14,11 @@ package-lock.json
|
||||
dist.zip
|
||||
|
||||
.vscode
|
||||
|
||||
bun.lockb
|
||||
|
||||
.env.*.local
|
||||
|
||||
out
|
||||
|
||||
.cursorrules
|
||||
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
||||
out
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
@@ -1,39 +0,0 @@
|
||||
module.exports = {
|
||||
// 一行最多 120 字符..
|
||||
printWidth: 120,
|
||||
// 使用 2 个空格缩进
|
||||
tabWidth: 2,
|
||||
// 不使用缩进符,而使用空格
|
||||
useTabs: false,
|
||||
// 行尾需要有分号
|
||||
semi: true,
|
||||
// 使用单引号
|
||||
singleQuote: true,
|
||||
// 对象的 key 仅在必要时用引号
|
||||
quoteProps: 'as-needed',
|
||||
// jsx 不使用单引号,而使用双引号
|
||||
jsxSingleQuote: false,
|
||||
// 末尾需要有逗号
|
||||
trailingComma: 'all',
|
||||
// 大括号内的首尾需要空格
|
||||
bracketSpacing: true,
|
||||
// jsx 标签的反尖括号需要换行
|
||||
jsxBracketSameLine: false,
|
||||
// 箭头函数,只有一个参数的时候,也需要括号
|
||||
arrowParens: 'always',
|
||||
// 每个文件格式化的范围是文件的全部内容
|
||||
rangeStart: 0,
|
||||
rangeEnd: Infinity,
|
||||
// 不需要写文件开头的 @prettier
|
||||
requirePragma: false,
|
||||
// 不需要自动在文件开头插入 @prettier
|
||||
insertPragma: false,
|
||||
// 使用默认的折行标准
|
||||
proseWrap: 'preserve',
|
||||
// 根据显示样式决定 html 要不要折行
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
// vue 文件中的 script 和 style 内不用缩进
|
||||
vueIndentScriptAndStyle: false,
|
||||
// 换行符使用 lf
|
||||
endOfLine: 'lf',
|
||||
};
|
||||
5
.prettierrc.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
singleQuote: true
|
||||
semi: true
|
||||
printWidth: 100
|
||||
trailingComma: none
|
||||
endOfLine: auto
|
||||
17
CHANGELOG.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 更新日志
|
||||
|
||||
## v3.7.0
|
||||
### ✨ 新功能
|
||||
- 添加全局快捷键支持以及快捷键管理功能
|
||||
- 优化设置页面样式以及布局
|
||||
|
||||
### 🐞 Bug修复
|
||||
- 修复弹窗层级问题
|
||||
- 修复夜间模式下 歌曲收藏样式无效问题
|
||||
- 优化夜间模式播放按钮颜色
|
||||
|
||||
|
||||
## 咖啡☕️
|
||||
| 微信 | | 支付宝 |
|
||||
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
|
||||
| <img src="https://www.ghproxy.cn/https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/dev_electron/src/renderer/assets/wechat.png" alt="WeChat QRcode" width=200>| | <img src="https://www.ghproxy.cn/https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/dev_electron/src/renderer/assets/alipay.png" alt="Wechat QRcode" width=200> |
|
||||
201
LICENSE
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
84
README.md
@@ -1,41 +1,61 @@
|
||||
# 一个基于 electron typescript vue3 的桌面音乐播放器 适配 web端 桌面端 web移动端
|
||||
# Alger Music Player
|
||||
主要功能如下
|
||||
- 🎵 音乐推荐
|
||||
- 🔐 网易云账号登录与同步
|
||||
- 📝 功能
|
||||
- 播放历史记录
|
||||
- 歌曲收藏管理
|
||||
- 自定义快捷键配置
|
||||
- 🎨 界面与交互
|
||||
- 沉浸式歌词显示(点击左下角封面进入)
|
||||
- 独立桌面歌词窗口
|
||||
- 明暗主题切换
|
||||
- 🎼 音乐功能
|
||||
- 支持歌单、MV、专辑等完整音乐服务
|
||||
- 灰色音乐资源解析(基于 @unblockneteasemusic/server)
|
||||
- 高品质音乐试听(需网易云VIP)
|
||||
- 音乐文件下载(支持右键下载和批量下载)
|
||||
- 🚀 技术特性
|
||||
- 本地化服务,无需依赖在线API (基于 netease-cloud-music-api)
|
||||
- 自动更新检测
|
||||
- 全平台适配(Desktop & Web & Mobile Web)
|
||||
|
||||
- 音乐推荐
|
||||
- 音乐播放
|
||||
- 网易云登录
|
||||
- 播放历史
|
||||
- 桌面歌词
|
||||
- 歌单 mv 搜索 专辑等功能
|
||||
|
||||
## 项目运行
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 运行项目 web
|
||||
npm run dev
|
||||
|
||||
# 运行项目 electron
|
||||
npm run start
|
||||
|
||||
# 打包项目 web
|
||||
npm run build
|
||||
|
||||
# 打包项目 electron
|
||||
npm run win ...
|
||||
# 具体看 package.json
|
||||
```
|
||||
## 项目简介
|
||||
一个基于 electron typescript vue3 的桌面音乐播放器 适配 web端 桌面端 web移动端
|
||||
|
||||
## 预览地址
|
||||
[http://mc.alger.fun/](http://mc.alger.fun/)
|
||||
|
||||
QQ群:789288579
|
||||
|
||||
## 软件截图
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 技术栈
|
||||
|
||||
### 主要框架
|
||||
- Vue 3 - 渐进式 JavaScript 框架
|
||||
- TypeScript - JavaScript 的超集,添加了类型系统
|
||||
- Electron - 跨平台桌面应用开发框架
|
||||
- Vite - 下一代前端构建工具
|
||||
- Naive UI - 基于 Vue 3 的组件库
|
||||
|
||||
|
||||
## 咖啡☕️
|
||||
| 微信 | 支付宝 |
|
||||
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
|
||||
| <img src="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true" alt="WeChat QRcode" width=200> | <img src="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true" alt="Wechat QRcode" width=200> |
|
||||
|
||||
|
||||
## Stargazers over time
|
||||
[](https://starchart.cc/algerkong/AlgerMusicPlayer)
|
||||
|
||||
|
||||
|
||||
|
||||
## 欢迎提Issues
|
||||
|
||||
|
||||
145
app.js
@@ -1,145 +0,0 @@
|
||||
const { app, BrowserWindow, ipcMain, Tray, Menu, globalShortcut, nativeImage } = require('electron');
|
||||
const path = require('path');
|
||||
const Store = require('electron-store');
|
||||
const setJson = require('./electron/set.json');
|
||||
const { loadLyricWindow } = require('./electron/lyric');
|
||||
|
||||
let mainWin = null;
|
||||
function createWindow() {
|
||||
mainWin = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 780,
|
||||
frame: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
// contextIsolation: false,
|
||||
preload: path.join(__dirname, '/electron/preload.js'),
|
||||
},
|
||||
});
|
||||
const win = mainWin;
|
||||
win.setMinimumSize(1200, 780);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
win.webContents.openDevTools({ mode: 'detach' });
|
||||
win.loadURL('http://localhost:4678/');
|
||||
} else {
|
||||
win.loadURL(`file://${__dirname}/dist/index.html`);
|
||||
}
|
||||
const image = nativeImage.createFromPath(path.join(__dirname, 'public/icon.png'));
|
||||
const tray = new Tray(image);
|
||||
|
||||
// 创建一个上下文菜单
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: '显示',
|
||||
click: () => {
|
||||
win.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '退出',
|
||||
click: () => {
|
||||
win.destroy();
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 设置系统托盘图标的上下文菜单
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
|
||||
tray.on('click', () => {
|
||||
if (win.isVisible()) {
|
||||
win.hide();
|
||||
} else {
|
||||
win.show();
|
||||
}
|
||||
});
|
||||
|
||||
const set = store.get('set');
|
||||
// store.set('set', setJson)
|
||||
|
||||
if (!set) {
|
||||
store.set('set', setJson);
|
||||
}
|
||||
|
||||
loadLyricWindow(ipcMain);
|
||||
}
|
||||
|
||||
// 限制只能启动一个应用
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('ready', () => {
|
||||
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
||||
if (mainWin.isVisible()) {
|
||||
mainWin.hide();
|
||||
} else {
|
||||
mainWin.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
ipcMain.on('minimize-window', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
win.minimize();
|
||||
});
|
||||
|
||||
ipcMain.on('maximize-window', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (win.isMaximized()) {
|
||||
win.unmaximize();
|
||||
} else {
|
||||
win.maximize();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('close-window', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
win.destroy();
|
||||
app.quit();
|
||||
});
|
||||
|
||||
ipcMain.on('drag-start', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
win.webContents.beginFrameSubscription((frameBuffer) => {
|
||||
event.reply('frame-buffer', frameBuffer);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('mini-tray', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
win.hide();
|
||||
});
|
||||
|
||||
// 重启
|
||||
ipcMain.on('restart', () => {
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
});
|
||||
|
||||
const store = new Store();
|
||||
|
||||
// 定义ipcRenderer监听事件
|
||||
ipcMain.on('setStore', (_, key, value) => {
|
||||
store.set(key, value);
|
||||
});
|
||||
|
||||
ipcMain.on('getStore', (_, key) => {
|
||||
const value = store.get(key);
|
||||
_.returnValue = value || '';
|
||||
});
|
||||
136
auto-imports.d.ts
vendored
@@ -3,68 +3,88 @@
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useDialog: typeof import('naive-ui')['useDialog']
|
||||
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
||||
const useMessage: typeof import('naive-ui')['useMessage']
|
||||
const useNotification: typeof import('naive-ui')['useNotification']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const EffectScope: (typeof import('vue'))['EffectScope'];
|
||||
const computed: (typeof import('vue'))['computed'];
|
||||
const createApp: (typeof import('vue'))['createApp'];
|
||||
const customRef: (typeof import('vue'))['customRef'];
|
||||
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent'];
|
||||
const defineComponent: (typeof import('vue'))['defineComponent'];
|
||||
const effectScope: (typeof import('vue'))['effectScope'];
|
||||
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance'];
|
||||
const getCurrentScope: (typeof import('vue'))['getCurrentScope'];
|
||||
const h: (typeof import('vue'))['h'];
|
||||
const inject: (typeof import('vue'))['inject'];
|
||||
const isProxy: (typeof import('vue'))['isProxy'];
|
||||
const isReactive: (typeof import('vue'))['isReactive'];
|
||||
const isReadonly: (typeof import('vue'))['isReadonly'];
|
||||
const isRef: (typeof import('vue'))['isRef'];
|
||||
const markRaw: (typeof import('vue'))['markRaw'];
|
||||
const nextTick: (typeof import('vue'))['nextTick'];
|
||||
const onActivated: (typeof import('vue'))['onActivated'];
|
||||
const onBeforeMount: (typeof import('vue'))['onBeforeMount'];
|
||||
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount'];
|
||||
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate'];
|
||||
const onDeactivated: (typeof import('vue'))['onDeactivated'];
|
||||
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured'];
|
||||
const onMounted: (typeof import('vue'))['onMounted'];
|
||||
const onRenderTracked: (typeof import('vue'))['onRenderTracked'];
|
||||
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered'];
|
||||
const onScopeDispose: (typeof import('vue'))['onScopeDispose'];
|
||||
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch'];
|
||||
const onUnmounted: (typeof import('vue'))['onUnmounted'];
|
||||
const onUpdated: (typeof import('vue'))['onUpdated'];
|
||||
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup'];
|
||||
const provide: (typeof import('vue'))['provide'];
|
||||
const reactive: (typeof import('vue'))['reactive'];
|
||||
const readonly: (typeof import('vue'))['readonly'];
|
||||
const ref: (typeof import('vue'))['ref'];
|
||||
const resolveComponent: (typeof import('vue'))['resolveComponent'];
|
||||
const shallowReactive: (typeof import('vue'))['shallowReactive'];
|
||||
const shallowReadonly: (typeof import('vue'))['shallowReadonly'];
|
||||
const shallowRef: (typeof import('vue'))['shallowRef'];
|
||||
const toRaw: (typeof import('vue'))['toRaw'];
|
||||
const toRef: (typeof import('vue'))['toRef'];
|
||||
const toRefs: (typeof import('vue'))['toRefs'];
|
||||
const toValue: (typeof import('vue'))['toValue'];
|
||||
const triggerRef: (typeof import('vue'))['triggerRef'];
|
||||
const unref: (typeof import('vue'))['unref'];
|
||||
const useAttrs: (typeof import('vue'))['useAttrs'];
|
||||
const useCssModule: (typeof import('vue'))['useCssModule'];
|
||||
const useCssVars: (typeof import('vue'))['useCssVars'];
|
||||
const useDialog: (typeof import('naive-ui'))['useDialog'];
|
||||
const useId: (typeof import('vue'))['useId'];
|
||||
const useLoadingBar: (typeof import('naive-ui'))['useLoadingBar'];
|
||||
const useMessage: (typeof import('naive-ui'))['useMessage'];
|
||||
const useModel: (typeof import('vue'))['useModel'];
|
||||
const useNotification: (typeof import('naive-ui'))['useNotification'];
|
||||
const useSlots: (typeof import('vue'))['useSlots'];
|
||||
const useTemplateRef: (typeof import('vue'))['useTemplateRef'];
|
||||
const watch: (typeof import('vue'))['watch'];
|
||||
const watchEffect: (typeof import('vue'))['watchEffect'];
|
||||
const watchPostEffect: (typeof import('vue'))['watchPostEffect'];
|
||||
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect'];
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
export type {
|
||||
Component,
|
||||
ComponentPublicInstance,
|
||||
ComputedRef,
|
||||
DirectiveBinding,
|
||||
ExtractDefaultPropTypes,
|
||||
ExtractPropTypes,
|
||||
ExtractPublicPropTypes,
|
||||
InjectionKey,
|
||||
PropType,
|
||||
Ref,
|
||||
MaybeRef,
|
||||
MaybeRefOrGetter,
|
||||
VNode,
|
||||
WritableComputedRef
|
||||
} from 'vue';
|
||||
import('vue');
|
||||
}
|
||||
|
||||
20
build/entitlements.mac.plist
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
build/icon.icns
Normal file
BIN
build/icon.ico
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
build/icon.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
13
build/installer.nsh
Normal file
@@ -0,0 +1,13 @@
|
||||
# 设置 Windows 7 兼容性
|
||||
ManifestDPIAware true
|
||||
ManifestSupportedOS all
|
||||
|
||||
!macro customInit
|
||||
# 检查系统版本
|
||||
${If} ${AtLeastWin7}
|
||||
# Windows 7 或更高版本
|
||||
${Else}
|
||||
MessageBox MB_OK|MB_ICONSTOP "此应用程序需要 Windows 7 或更高版本。"
|
||||
Abort
|
||||
${EndIf}
|
||||
!macroend
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"appId": "com.alger.music",
|
||||
"productName": "AlgerMusic",
|
||||
"artifactName": "${productName}_${version}_Setup_x86.${ext}",
|
||||
"directories": {
|
||||
"output": "dist_electron/win-x86"
|
||||
},
|
||||
"files": ["dist/**/*", "package.json", "app.js", "electron/**/*"],
|
||||
"win": {
|
||||
"icon": "public/icon.png",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": ["ia32"]
|
||||
}
|
||||
],
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "installer/installer.nsh",
|
||||
"to": "$INSTDIR"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"language": "2052",
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"differentialPackage": true,
|
||||
"shortcutName": "Alger Music"
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"appId": "com.alger.music",
|
||||
"productName": "AlgerMusic",
|
||||
"artifactName": "${productName}_${version}_Setup_x64.${ext}",
|
||||
"directories": {
|
||||
"output": "dist_electron/win-x64"
|
||||
},
|
||||
"files": ["dist/**/*", "package.json", "app.js", "electron/**/*"],
|
||||
"win": {
|
||||
"icon": "public/icon.png",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "installer/installer.nsh",
|
||||
"to": "$INSTDIR"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"language": "2052",
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"differentialPackage": true,
|
||||
"shortcutName": "Alger Music"
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"appId": "com.alger.music",
|
||||
"productName": "AlgerMusic",
|
||||
"artifactName": "${productName}_${version}_Setup_arm64.${ext}",
|
||||
"directories": {
|
||||
"output": "dist_electron/win-arm64"
|
||||
},
|
||||
"files": ["dist/**/*", "package.json", "app.js", "electron/**/*", "!node_modules/**/*"],
|
||||
"win": {
|
||||
"icon": "public/icon.png",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": ["arm64"]
|
||||
}
|
||||
],
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "installer/installer.nsh",
|
||||
"to": "$INSTDIR"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"language": "2052",
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"differentialPackage": true,
|
||||
"shortcutName": "Alger Music"
|
||||
}
|
||||
}
|
||||
20
components.d.ts
vendored
@@ -1,41 +1,33 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
export {};
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
MPop: typeof import('./src/components/common/MPop.vue')['default']
|
||||
MusicList: typeof import('./src/components/MusicList.vue')['default']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
PlayBottom: typeof import('./src/components/common/PlayBottom.vue')['default']
|
||||
PlayListsItem: typeof import('./src/components/common/PlayListsItem.vue')['default']
|
||||
PlaylistType: typeof import('./src/components/PlaylistType.vue')['default']
|
||||
PlayVideo: typeof import('./src/components/common/PlayVideo.vue')['default']
|
||||
RecommendAlbum: typeof import('./src/components/RecommendAlbum.vue')['default']
|
||||
RecommendSinger: typeof import('./src/components/RecommendSinger.vue')['default']
|
||||
RecommendSonglist: typeof import('./src/components/RecommendSonglist.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SearchItem: typeof import('./src/components/common/SearchItem.vue')['default']
|
||||
SongItem: typeof import('./src/components/common/SongItem.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
3
dev-app-update.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
provider: generic
|
||||
url: https://example.com/auto-updates
|
||||
updaterCacheDirName: electron-lan-file-updater
|
||||
BIN
docs/image.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
docs/image1.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
docs/image2.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/image3.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
docs/image4.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 902 KiB |
|
Before Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 478 KiB |
|
Before Width: | Height: | Size: 502 KiB |
45
electron-builder.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
appId: com.electron.app
|
||||
productName: electron-lan-file
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
executableName: electron-lan-file
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
notarize: false
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://example.com/auto-updates
|
||||
electronDownload:
|
||||
mirror: https://npmmirror.com/mirrors/electron/
|
||||
60
electron.vite.config.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||
import { resolve } from 'path';
|
||||
import AutoImport from 'unplugin-auto-import/vite';
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import viteCompression from 'vite-plugin-compression';
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
renderer: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve('src/renderer'),
|
||||
'@renderer': resolve('src/renderer')
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
viteCompression(),
|
||||
// VueDevTools(),
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
{
|
||||
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar']
|
||||
}
|
||||
]
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver()]
|
||||
})
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
// with options
|
||||
[process.env.VITE_API_LOCAL as string]: {
|
||||
target: process.env.VITE_API,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_LOCAL}`), '')
|
||||
},
|
||||
[process.env.VITE_API_MUSIC_PROXY as string]: {
|
||||
target: process.env.VITE_API_MUSIC,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_MUSIC_PROXY}`), '')
|
||||
},
|
||||
[process.env.VITE_API_PROXY_MUSIC as string]: {
|
||||
target: process.env.VITE_API_PROXY,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_PROXY_MUSIC}`), '')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
const { BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
let lyricWindow = null;
|
||||
|
||||
const createWin = () => {
|
||||
lyricWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 300,
|
||||
frame: false,
|
||||
show: false,
|
||||
transparent: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
preload: `${__dirname}/preload.js`,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
const loadLyricWindow = (ipcMain) => {
|
||||
ipcMain.on('open-lyric', () => {
|
||||
if (lyricWindow) {
|
||||
if (lyricWindow.isMinimized()) lyricWindow.restore();
|
||||
lyricWindow.focus();
|
||||
lyricWindow.show();
|
||||
return;
|
||||
}
|
||||
createWin();
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
lyricWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
lyricWindow.loadURL('http://localhost:4678/#/lyric');
|
||||
} else {
|
||||
const distPath = path.resolve(__dirname, '../dist');
|
||||
lyricWindow.loadURL(`file://${distPath}/index.html#/lyric`);
|
||||
}
|
||||
|
||||
lyricWindow.setMinimumSize(600, 200);
|
||||
|
||||
// 隐藏任务栏
|
||||
lyricWindow.setSkipTaskbar(true);
|
||||
|
||||
lyricWindow.show();
|
||||
});
|
||||
|
||||
ipcMain.on('send-lyric', (e, data) => {
|
||||
if (lyricWindow) {
|
||||
lyricWindow.webContents.send('receive-lyric', data);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('top-lyric', (e, data) => {
|
||||
lyricWindow.setAlwaysOnTop(data);
|
||||
});
|
||||
|
||||
ipcMain.on('close-lyric', () => {
|
||||
lyricWindow.close();
|
||||
lyricWindow = null;
|
||||
});
|
||||
|
||||
ipcMain.on('mouseenter-lyric', () => {
|
||||
lyricWindow.setIgnoreMouseEvents(true);
|
||||
});
|
||||
|
||||
ipcMain.on('mouseleave-lyric', () => {
|
||||
lyricWindow.setIgnoreMouseEvents(false);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
loadLyricWindow,
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
const { contextBridge, ipcRenderer, app } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
minimize: () => ipcRenderer.send('minimize-window'),
|
||||
maximize: () => ipcRenderer.send('maximize-window'),
|
||||
close: () => ipcRenderer.send('close-window'),
|
||||
dragStart: (data) => ipcRenderer.send('drag-start', data),
|
||||
miniTray: () => ipcRenderer.send('mini-tray'),
|
||||
restart: () => ipcRenderer.send('restart'),
|
||||
openLyric: () => ipcRenderer.send('open-lyric'),
|
||||
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
||||
});
|
||||
|
||||
const electronHandler = {
|
||||
ipcRenderer: {
|
||||
setStoreValue: (key, value) => {
|
||||
ipcRenderer.send('setStore', key, value);
|
||||
},
|
||||
|
||||
getStoreValue(key) {
|
||||
const resp = ipcRenderer.sendSync('getStore', key);
|
||||
return resp;
|
||||
},
|
||||
},
|
||||
app,
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', electronHandler);
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"version": "1.5.0",
|
||||
"isProxy": false,
|
||||
"author": "alger"
|
||||
}
|
||||
25
index.html
@@ -1,25 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>网抑云 | algerkong</title>
|
||||
<link rel="manifest" href="./public/manifest.json" />
|
||||
<link rel="stylesheet" href="./public/icon/iconfont.css" />
|
||||
<link rel="stylesheet" href="./public/css/animate.css" />
|
||||
<link rel="stylesheet" href="./public/css/base.css" />
|
||||
<style>
|
||||
:root {
|
||||
--animate-delay: 0.5s;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
183
package.json
@@ -1,61 +1,162 @@
|
||||
{
|
||||
"name": "alger-music",
|
||||
"version": "1.5.0",
|
||||
"description": "这是一个用于音乐播放的应用程序。",
|
||||
"name": "AlgerMusicPlayer",
|
||||
"version": "3.7.0",
|
||||
"description": "Alger Music Player",
|
||||
"author": "Alger <algerkc@qq.com>",
|
||||
"main": "app.js",
|
||||
"main": "./out/main/index.js",
|
||||
"homepage": "https://github.com/algerkong/AlgerMusicPlayer",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"start": "set NODE_ENV=development&&electron .",
|
||||
"lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0",
|
||||
"b:win:x64": "electron-builder --config ./build/win64.json",
|
||||
"b:win:x86": "electron-builder --config ./build/win32.json",
|
||||
"b:win:arm": "electron-builder --config ./build/winarm64.json"
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:unpack": "npm run build && electron-builder --dir",
|
||||
"build:win": "npm run build && electron-builder --win",
|
||||
"build:mac": "npm run build && electron-builder --mac",
|
||||
"build:linux": "npm run build && electron-builder --linux"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-store": "^8.1.0"
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@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": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@tailwindcss/postcss7-compat": "^2.2.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vue/compiler-sfc": "^3.3.4",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/runtime-core": "^3.3.4",
|
||||
"@vueuse/core": "^10.7.1",
|
||||
"@vueuse/electron": "^10.9.0",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"axios": "^0.21.1",
|
||||
"electron": "^30.0.0",
|
||||
"electron-builder": "^24.13.0",
|
||||
"eslint": "^8.56.0",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/node": "^20.14.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vue/compiler-sfc": "^3.5.0",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"@vue/runtime-core": "^3.5.0",
|
||||
"@vueuse/core": "^11.0.3",
|
||||
"@vueuse/electron": "^11.0.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.7.7",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^31.0.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-vite": "^2.3.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"eslint-plugin-vue": "^9.21.1",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"eslint-plugin-vue-scoped-css": "^2.7.2",
|
||||
"howler": "^2.2.4",
|
||||
"lodash": "^4.17.21",
|
||||
"naive-ui": "^2.38.2",
|
||||
"postcss": "^7.0.36",
|
||||
"prettier": "^3.2.5",
|
||||
"marked": "^15.0.4",
|
||||
"naive-ui": "^2.41.0",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.3.2",
|
||||
"remixicon": "^4.2.0",
|
||||
"sass": "^1.35.2",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
|
||||
"typescript": "^4.3.2",
|
||||
"unplugin-auto-import": "^0.17.2",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"sass": "^1.82.0",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.5.2",
|
||||
"unplugin-auto-import": "^0.18.2",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vfonts": "^0.1.0",
|
||||
"vite": "^4.4.7",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-devtools": "1.0.0-beta.5",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-tsc": "^0.0.24",
|
||||
"vuex": "^4.1.0"
|
||||
"vite-plugin-vue-devtools": "7.4.0",
|
||||
"vue": "^3.4.30",
|
||||
"vue-router": "^4.4.3",
|
||||
"vue-tsc": "^2.0.22",
|
||||
"vuex": "^4.1.0",
|
||||
"animate.css": "^4.1.1"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.alger.music",
|
||||
"productName": "AlgerMusicPlayer",
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "algerkong",
|
||||
"repo": "AlgerMusicPlayer"
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"icon": "resources/icon.icns",
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"universal"
|
||||
]
|
||||
}
|
||||
],
|
||||
"artifactName": "${productName}-${version}-mac-${arch}.${ext}",
|
||||
"darkModeSupport": true,
|
||||
"hardenedRuntime": false,
|
||||
"gatekeeperAssess": false,
|
||||
"entitlements": "build/entitlements.mac.plist",
|
||||
"entitlementsInherit": "build/entitlements.mac.plist",
|
||||
"notarize": false,
|
||||
"identity": null,
|
||||
"type": "distribution",
|
||||
"binaries": [
|
||||
"Contents/MacOS/AlgerMusicPlayer"
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"icon": "resources/favicon.ico",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64",
|
||||
"ia32"
|
||||
]
|
||||
}
|
||||
],
|
||||
"artifactName": "${productName}-${version}-win-${arch}.${ext}",
|
||||
"requestedExecutionLevel": "asInvoker"
|
||||
},
|
||||
"linux": {
|
||||
"icon": "resources/icon.png",
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "deb",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"artifactName": "${productName}-${version}-linux-${arch}.${ext}",
|
||||
"category": "Audio",
|
||||
"maintainer": "Alger <algerkc@qq.com>"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"installerIcon": "resources/favicon.ico",
|
||||
"uninstallerIcon": "resources/favicon.ico",
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "AlgerMusicPlayer",
|
||||
"include": "build/installer.nsh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
|
||||
7
public/css/animate.css
vendored
@@ -1,3 +0,0 @@
|
||||
body{
|
||||
background-color: #000;
|
||||
}
|
||||
BIN
public/icon1.png
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
BIN
resources/icon.icns
Normal file
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
BIN
resources/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 626 B |
50
src/App.vue
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<div class="app" :class="isMobile ? 'mobile' : ''">
|
||||
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
|
||||
<n-config-provider :theme="darkTheme">
|
||||
<n-dialog-provider>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</n-dialog-provider>
|
||||
</n-config-provider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { darkTheme } from 'naive-ui';
|
||||
|
||||
import store from '@/store';
|
||||
|
||||
import { isMobile } from './utils';
|
||||
|
||||
const playMusicUrl = computed(() => store.state.playMusicUrl as string);
|
||||
// 是否播放
|
||||
const play = computed(() => store.state.play as boolean);
|
||||
const windowData = window as any;
|
||||
onMounted(() => {
|
||||
if (windowData.electron) {
|
||||
const setData = windowData.electron.ipcRenderer.getStoreValue('set');
|
||||
store.commit('setSetData', setData);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.app {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.text-base {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.html:has(.mobile) {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +0,0 @@
|
||||
import { ILyric } from '@/type/lyric';
|
||||
import { IPlayMusicUrl } from '@/type/music';
|
||||
import request from '@/utils/request';
|
||||
import requestMusic from '@/utils/request_music';
|
||||
// 根据音乐Id获取音乐播放URl
|
||||
export const getMusicUrl = (id: number) => {
|
||||
return request.get<IPlayMusicUrl>('/song/url', { params: { id } });
|
||||
};
|
||||
|
||||
// 获取歌曲详情
|
||||
export const getMusicDetail = (ids: Array<number>) => {
|
||||
return request.get('/song/detail', { params: { ids: ids.join(',') } });
|
||||
};
|
||||
|
||||
// 根据音乐Id获取音乐歌词
|
||||
export const getMusicLrc = (id: number) => {
|
||||
return request.get<ILyric>('/lyric', { params: { id } });
|
||||
};
|
||||
|
||||
export const getParsingMusicUrl = (id: number) => {
|
||||
return requestMusic.get<any>('/music', { params: { id } });
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { IData } from '@/type';
|
||||
import { IMvItem, IMvUrlData } from '@/type/mv';
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 获取 mv 排行
|
||||
export const getTopMv = (limit: number) => {
|
||||
return request.get<IData<Array<IMvItem>>>('/top/mv', {
|
||||
params: {
|
||||
limit,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 获取 mv 数据
|
||||
export const getMvDetail = (mvid: string) => {
|
||||
return request.get('/mv/detail', {
|
||||
params: {
|
||||
mvid,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 获取 mv 地址
|
||||
export const getMvUrl = (id: Number) => {
|
||||
return request.get<IData<IMvUrlData>>('/mv/url', {
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<n-drawer
|
||||
:show="show"
|
||||
:height="isMobile ? '100vh' : '70vh'"
|
||||
placement="bottom"
|
||||
:drawer-style="{ backgroundColor: 'transparent' }"
|
||||
>
|
||||
<div class="music-page">
|
||||
<i class="iconfont icon-icon_error music-close" @click="close"></i>
|
||||
<div class="music-title text-el">{{ name }}</div>
|
||||
<!-- 歌单歌曲列表 -->
|
||||
<div class="music-list">
|
||||
<n-scrollbar>
|
||||
<div
|
||||
v-for="(item, index) in songList"
|
||||
:key="item.id"
|
||||
:class="setAnimationClass('animate__bounceInUp')"
|
||||
:style="setAnimationDelay(index, 50)"
|
||||
>
|
||||
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
||||
</div>
|
||||
<play-bottom />
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
import PlayBottom from './common/PlayBottom.vue';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
name: string;
|
||||
songList: any[];
|
||||
}>();
|
||||
const emit = defineEmits(['update:show']);
|
||||
|
||||
const formatDetail = computed(() => (detail: any) => {
|
||||
const song = {
|
||||
artists: detail.ar,
|
||||
name: detail.al.name,
|
||||
id: detail.al.id,
|
||||
};
|
||||
|
||||
detail.song = song;
|
||||
detail.picUrl = detail.al.picUrl;
|
||||
return detail;
|
||||
});
|
||||
|
||||
const handlePlay = () => {
|
||||
const tracks = props.songList || [];
|
||||
store.commit('setPlayList', tracks);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('update:show', false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.music {
|
||||
&-page {
|
||||
@apply px-8 w-full h-full bg-black bg-opacity-75 rounded-t-2xl;
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
&-title {
|
||||
@apply text-lg font-bold text-white p-4;
|
||||
}
|
||||
|
||||
&-close {
|
||||
@apply absolute top-4 right-8 cursor-pointer text-white text-3xl;
|
||||
}
|
||||
|
||||
&-list {
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.music-page {
|
||||
@apply px-4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<!-- 歌单分类列表 -->
|
||||
<div class="play-list-type">
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">歌单分类</div>
|
||||
<div>
|
||||
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
|
||||
<span
|
||||
v-show="isShowAllPlaylistCategory || index <= 19"
|
||||
class="play-list-type-item"
|
||||
:class="setAnimationClass('animate__bounceIn')"
|
||||
:style="setAnimationDelay(index <= 19 ? index : index - 19)"
|
||||
@click="handleClickPlaylistType(item.name)"
|
||||
>{{ item.name }}</span
|
||||
>
|
||||
</template>
|
||||
<div
|
||||
class="play-list-type-showall"
|
||||
:class="setAnimationClass('animate__bounceIn')"
|
||||
:style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
|
||||
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
|
||||
>
|
||||
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { getPlaylistCategory } from '@/api/home';
|
||||
import type { IPlayListSort } from '@/type/playlist';
|
||||
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
// 歌单分类
|
||||
const playlistCategory = ref<IPlayListSort>();
|
||||
// 是否显示全部歌单分类
|
||||
const isShowAllPlaylistCategory = ref<boolean>(false);
|
||||
|
||||
// 加载歌单分类
|
||||
const loadPlaylistCategory = async () => {
|
||||
const { data } = await getPlaylistCategory();
|
||||
playlistCategory.value = data;
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const handleClickPlaylistType = (type: string) => {
|
||||
router.push({
|
||||
path: '/list',
|
||||
query: {
|
||||
type,
|
||||
},
|
||||
});
|
||||
};
|
||||
// 页面初始化
|
||||
onMounted(() => {
|
||||
loadPlaylistCategory();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
@apply text-lg font-bold mb-4;
|
||||
}
|
||||
.play-list-type {
|
||||
width: 250px;
|
||||
@apply mx-6;
|
||||
&-item,
|
||||
&-showall {
|
||||
@apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 transition;
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
&-showall {
|
||||
@apply block text-center;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.play-list-type {
|
||||
@apply mx-0 w-full;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { setAnimationClass } from '@/utils';
|
||||
|
||||
const props = defineProps({
|
||||
showPop: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const musicFullClass = computed(() => {
|
||||
if (props.showPop) {
|
||||
return setAnimationClass('animate__fadeInUp');
|
||||
}
|
||||
return setAnimationClass('animate__fadeOutDown');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-show="props.showPop" class="pop-page" :class="musicFullClass">
|
||||
<i v-if="props.showClose" class="iconfont icon-icon_error close"></i>
|
||||
<img src="http://code.myalger.top/2000*2000.jpg,f054f0,0f2255" />
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pop-page {
|
||||
height: 800px;
|
||||
@apply absolute top-4 left-0 w-full;
|
||||
background-color: #000000f0;
|
||||
.close {
|
||||
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<n-drawer :show="show" height="100vh" placement="bottom" :z-index="999999999">
|
||||
<div class="mv-detail">
|
||||
<video :src="url" controls autoplay></video>
|
||||
<div class="mv-detail-title">
|
||||
<div class="title">{{ title }}</div>
|
||||
<button @click="close">
|
||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
title: string;
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(val) => {
|
||||
if (val) {
|
||||
store.commit('setIsPlay', false);
|
||||
store.commit('setPlayMusic', false);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits(['update:show']);
|
||||
|
||||
const close = () => {
|
||||
emit('update:show', false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mv-detail {
|
||||
@apply w-full h-full bg-black relative;
|
||||
|
||||
&-title {
|
||||
@apply absolute w-full left-0 flex justify-between h-16 px-6 py-2 text-xl font-bold items-center z-50 transition-all duration-300 ease-in-out -top-24;
|
||||
background: linear-gradient(0, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%);
|
||||
button .icon-xiasanjiaoxing {
|
||||
@apply text-3xl;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
@apply text-green-400;
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
@apply w-full h-full;
|
||||
}
|
||||
video:hover + .mv-detail-title {
|
||||
@apply top-0;
|
||||
}
|
||||
|
||||
.mv-detail-title:hover {
|
||||
@apply top-0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<div class="search-item" @click="handleClick">
|
||||
<div class="search-item-img">
|
||||
<n-image :src="getImgUrl(item.picUrl, '200y200')" lazy preview-disabled />
|
||||
</div>
|
||||
<div class="search-item-info">
|
||||
<div class="search-item-name">{{ item.name }}</div>
|
||||
<div class="search-item-artist">{{ item.desc }}</div>
|
||||
</div>
|
||||
|
||||
<MusicList
|
||||
v-if="['专辑', 'playlist'].includes(item.type)"
|
||||
v-model:show="showPop"
|
||||
:name="item.name"
|
||||
:song-list="songList"
|
||||
/>
|
||||
|
||||
<PlayVideo v-if="item.type === 'mv'" v-model:show="showPop" :title="item.name" :url="url" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getAlbum, getListDetail } from '@/api/list';
|
||||
import { getMvUrl } from '@/api/mv';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
item: {
|
||||
picUrl: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}>();
|
||||
|
||||
const url = ref('');
|
||||
|
||||
const songList = ref<any[]>([]);
|
||||
|
||||
const showPop = ref(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
if (props.item.type === '专辑') {
|
||||
showPop.value = true;
|
||||
const res = await getAlbum(props.item.id);
|
||||
songList.value = res.data.songs.map((song: any) => {
|
||||
song.al.picUrl = song.al.picUrl || props.item.picUrl;
|
||||
return song;
|
||||
});
|
||||
}
|
||||
|
||||
if (props.item.type === 'playlist') {
|
||||
showPop.value = true;
|
||||
const res = await getListDetail(props.item.id);
|
||||
songList.value = res.data.playlist.tracks;
|
||||
}
|
||||
|
||||
if (props.item.type === 'mv') {
|
||||
const res = await getMvUrl(props.item.id);
|
||||
url.value = res.data.data.url;
|
||||
showPop.value = true;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-item {
|
||||
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition cursor-pointer;
|
||||
margin: 0 10px;
|
||||
.search-item-img {
|
||||
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
|
||||
}
|
||||
.search-item-info {
|
||||
&-name {
|
||||
@apply text-white text-sm text-center;
|
||||
}
|
||||
&-artist {
|
||||
@apply text-gray-400 text-xs text-center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<div class="song-item" :class="{ 'song-mini': mini }">
|
||||
<n-image v-if="item.picUrl" :src="getImgUrl(item.picUrl, '40y40')" class="song-item-img" lazy preview-disabled />
|
||||
<div class="song-item-content">
|
||||
<div class="song-item-content-title">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
|
||||
</div>
|
||||
<div class="song-item-content-name">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||
<span v-for="(artists, artistsindex) in item.song.artists" :key="artistsindex"
|
||||
>{{ artists.name }}{{ artistsindex < item.song.artists.length - 1 ? ' / ' : '' }}</span
|
||||
>
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
<div class="song-item-operating">
|
||||
<div class="song-item-operating-like">
|
||||
<i class="iconfont icon-likefill"></i>
|
||||
</div>
|
||||
<div
|
||||
class="song-item-operating-play bg-black animate__animated"
|
||||
:class="{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }"
|
||||
@click="playMusicEvent(item)"
|
||||
>
|
||||
<i v-if="isPlaying && play" class="iconfont icon-stop"></i>
|
||||
<i v-else class="iconfont icon-playfill"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: SongResult;
|
||||
mini?: boolean;
|
||||
}>(),
|
||||
{
|
||||
mini: false,
|
||||
},
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
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 playMusicEvent = async (item: SongResult) => {
|
||||
if (playMusic.value.id === item.id) {
|
||||
return;
|
||||
}
|
||||
await store.commit('setPlay', item);
|
||||
store.commit('setIsPlay', true);
|
||||
emits('play', item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 配置文字不可选中
|
||||
.text-ellipsis {
|
||||
width: 100%;
|
||||
}
|
||||
.song-item {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition;
|
||||
&-img {
|
||||
@apply w-12 h-12 rounded-2xl mr-4;
|
||||
}
|
||||
&-content {
|
||||
@apply flex-1;
|
||||
&-title {
|
||||
@apply text-base text-white;
|
||||
}
|
||||
&-name {
|
||||
@apply text-xs;
|
||||
@apply text-gray-400;
|
||||
}
|
||||
}
|
||||
&-operating {
|
||||
@apply flex items-center pl-4 rounded-full border border-gray-700 ml-4;
|
||||
background-color: #0d0d0d;
|
||||
.iconfont {
|
||||
@apply text-xl;
|
||||
}
|
||||
.icon-likefill {
|
||||
color: #868686;
|
||||
@apply text-xl hover:text-red-600 transition;
|
||||
}
|
||||
&-like {
|
||||
@apply mr-2 cursor-pointer;
|
||||
}
|
||||
&-play {
|
||||
@apply cursor-pointer border border-gray-500 rounded-full w-10 h-10 flex justify-center items-center hover:bg-green-600 transition;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.song-mini {
|
||||
@apply p-2 rounded-2xl;
|
||||
.song-item {
|
||||
@apply p-0;
|
||||
&-img {
|
||||
@apply w-10 h-10 mr-2;
|
||||
}
|
||||
&-content {
|
||||
@apply flex-1;
|
||||
&-title {
|
||||
@apply text-sm;
|
||||
}
|
||||
&-name {
|
||||
@apply text-xs;
|
||||
}
|
||||
}
|
||||
&-operating {
|
||||
@apply pl-2;
|
||||
.iconfont {
|
||||
@apply text-base;
|
||||
}
|
||||
&-like {
|
||||
@apply mr-1;
|
||||
}
|
||||
&-play {
|
||||
@apply w-8 h-8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
10
src/electron.d.ts
vendored
@@ -1,10 +0,0 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
minimize: () => void;
|
||||
maximize: () => void;
|
||||
close: () => void;
|
||||
dragStart: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import { getMusicLrc } from '@/api/music';
|
||||
import { ILyric } from '@/type/lyric';
|
||||
|
||||
interface ILrcData {
|
||||
text: string;
|
||||
trText: string;
|
||||
}
|
||||
|
||||
export const lrcData = ref<ILyric>();
|
||||
export const newLrcIndex = ref<number>(0);
|
||||
export const lrcArray = ref<Array<ILrcData>>([]);
|
||||
export const lrcTimeArray = ref<Array<Number>>([]);
|
||||
|
||||
export const parseTime = (timeString: string) => {
|
||||
const [minutes, seconds] = timeString.split(':');
|
||||
return Number(minutes) * 60 + Number(seconds);
|
||||
};
|
||||
|
||||
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g;
|
||||
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g;
|
||||
|
||||
function parseLyricLine(lyricLine: string) {
|
||||
const timeText = lyricLine.match(TIME_REGEX)?.[0] || '';
|
||||
const time = parseTime(timeText);
|
||||
const text = lyricLine.replace(LRC_REGEX, '').trim();
|
||||
return { time, text };
|
||||
}
|
||||
|
||||
interface ILyricText {
|
||||
text: string;
|
||||
trText: string;
|
||||
}
|
||||
|
||||
function parseLyrics(lyricsString: string) {
|
||||
const lines = lyricsString.split('\n');
|
||||
const lyrics: Array<ILyricText> = [];
|
||||
const times: number[] = [];
|
||||
lines.forEach((line) => {
|
||||
const { time, text } = parseLyricLine(line);
|
||||
times.push(time);
|
||||
lyrics.push({ text, trText: '' });
|
||||
});
|
||||
return { lyrics, times };
|
||||
}
|
||||
|
||||
export const loadLrc = async (playMusicId: number): Promise<void> => {
|
||||
try {
|
||||
const { data } = await getMusicLrc(playMusicId);
|
||||
const { lyrics, times } = parseLyrics(data.lrc.lyric);
|
||||
let tlyric: {
|
||||
[key: string]: string;
|
||||
} = {};
|
||||
if (data.tlyric.lyric) {
|
||||
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
|
||||
tlyric = tLyrics.reduce((acc: any, cur, index) => {
|
||||
acc[tTimes[index]] = cur.text;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
if (Object.keys(tlyric).length) {
|
||||
lyrics.forEach((item, index) => {
|
||||
item.trText = item.text ? tlyric[times[index].toString()] : '';
|
||||
});
|
||||
}
|
||||
lrcTimeArray.value = times;
|
||||
lrcArray.value = lyrics;
|
||||
} catch (err) {
|
||||
console.error('err', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 歌词矫正时间Correction time
|
||||
const correctionTime = ref(0.4);
|
||||
|
||||
// 增加矫正时间
|
||||
export const addCorrectionTime = (time: number) => {
|
||||
correctionTime.value += time;
|
||||
};
|
||||
|
||||
// 减少矫正时间
|
||||
export const reduceCorrectionTime = (time: number) => {
|
||||
correctionTime.value -= time;
|
||||
};
|
||||
|
||||
export const isCurrentLrc = (index: number, time: number) => {
|
||||
const currentTime = Number(lrcTimeArray.value[index]);
|
||||
const nextTime = Number(lrcTimeArray.value[index + 1]);
|
||||
const nowTime = time + correctionTime.value;
|
||||
const isTrue = nowTime > currentTime && nowTime < nextTime;
|
||||
if (isTrue) {
|
||||
newLrcIndex.value = index;
|
||||
}
|
||||
return isTrue;
|
||||
};
|
||||
|
||||
export const nowTime = ref(0);
|
||||
export const allTime = ref(0);
|
||||
export const nowIndex = ref(0);
|
||||
|
||||
export const getLrcIndex = (time: number) => {
|
||||
for (let i = 0; i < lrcTimeArray.value.length; i++) {
|
||||
if (isCurrentLrc(i, time)) {
|
||||
nowIndex.value = i || nowIndex.value;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return nowIndex.value;
|
||||
};
|
||||
|
||||
// 设置当前播放时间
|
||||
export const setAudioTime = (index: number, audio: HTMLAudioElement) => {
|
||||
audio.currentTime = lrcTimeArray.value[index] as number;
|
||||
audio.play();
|
||||
};
|
||||
|
||||
// 计算这个歌词的播放时间
|
||||
const getLrcTime = (index: number) => {
|
||||
return Number(lrcTimeArray.value[index]);
|
||||
};
|
||||
|
||||
// 获取当前播放的歌词
|
||||
export const getCurrentLrc = () => {
|
||||
const index = getLrcIndex(nowTime.value);
|
||||
const currentLrc = lrcArray.value[index];
|
||||
const nextLrc = lrcArray.value[index + 1];
|
||||
return { currentLrc, nextLrc };
|
||||
};
|
||||
|
||||
// 获取一句歌词播放时间是 几秒到几秒
|
||||
export const getLrcTimeRange = (index: number) => {
|
||||
const currentTime = Number(lrcTimeArray.value[index]);
|
||||
const nextTime = Number(lrcTimeArray.value[index + 1]);
|
||||
return { currentTime, nextTime };
|
||||
};
|
||||
|
||||
export const sendLyricToWin = (isPlay: boolean = true) => {
|
||||
try {
|
||||
// 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间
|
||||
let lyricWinData = null;
|
||||
if (lrcArray.value.length > 0) {
|
||||
const nowIndex = getLrcIndex(nowTime.value);
|
||||
const { currentLrc, nextLrc } = getCurrentLrc();
|
||||
const { currentTime, nextTime } = getLrcTimeRange(nowIndex);
|
||||
lyricWinData = {
|
||||
currentLrc,
|
||||
nextLrc,
|
||||
currentTime,
|
||||
nextTime,
|
||||
nowIndex,
|
||||
lrcTimeArray: lrcTimeArray.value,
|
||||
lrcArray: lrcArray.value,
|
||||
nowTime: nowTime.value,
|
||||
allTime: allTime.value,
|
||||
startCurrentTime: getLrcTime(nowIndex),
|
||||
isPlay,
|
||||
};
|
||||
|
||||
const windowData = window as any;
|
||||
windowData.electronAPI.sendLyric(JSON.stringify(lyricWinData));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const openLyric = () => {
|
||||
const windowData = window as any;
|
||||
windowData.electronAPI.openLyric();
|
||||
sendLyricToWin();
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
/* ./src/index.css */
|
||||
|
||||
/*! @import */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.n-image img {
|
||||
background-color: #111111;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-el {
|
||||
@apply overflow-ellipsis overflow-hidden whitespace-nowrap;
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
<template>
|
||||
<div class="layout-page">
|
||||
<div class="layout-main">
|
||||
<title-bar v-if="isElectron" />
|
||||
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
|
||||
<!-- 侧边菜单栏 -->
|
||||
<app-menu v-if="!isMobile" class="menu" :menus="menus" />
|
||||
<div class="main">
|
||||
<!-- 搜索栏 -->
|
||||
<search-bar />
|
||||
<!-- 主页面路由 -->
|
||||
<div class="main-content bg-black" :native-scrollbar="false">
|
||||
<n-message-provider>
|
||||
<router-view
|
||||
v-slot="{ Component }"
|
||||
class="main-page"
|
||||
:class="route.meta.noScroll && !isMobile ? 'pr-3' : ''"
|
||||
>
|
||||
<keep-alive :include="keepAliveInclude">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</n-message-provider>
|
||||
</div>
|
||||
<play-bottom height="5rem" />
|
||||
<app-menu v-if="isMobile" class="menu" :menus="menus" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部音乐播放 -->
|
||||
<play-bar v-if="isPlay" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||
import homeRouter from '@/router/home';
|
||||
import { isMobile } from '@/utils';
|
||||
|
||||
const keepAliveInclude = computed(() =>
|
||||
homeRouter
|
||||
.filter((item) => {
|
||||
return item.meta.keepAlive;
|
||||
})
|
||||
.map((item) => {
|
||||
// return item.name;
|
||||
// 首字母大写
|
||||
return item.name.charAt(0).toUpperCase() + item.name.slice(1);
|
||||
}),
|
||||
);
|
||||
|
||||
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
|
||||
const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue'));
|
||||
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
|
||||
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const isPlay = computed(() => store.state.isPlay as boolean);
|
||||
const { menus } = store.state;
|
||||
const play = computed(() => store.state.play as boolean);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const audio = {
|
||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
|
||||
};
|
||||
|
||||
const windowData = window as any;
|
||||
const isElectron = computed(() => {
|
||||
return !!windowData.electronAPI;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 监听音乐是否播放
|
||||
watch(
|
||||
() => play.value,
|
||||
(value) => {
|
||||
if (value && audio.value) {
|
||||
audioPlay();
|
||||
} else {
|
||||
audioPause();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
document.onkeyup = (e) => {
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
playMusicEvent();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
// 按下键盘按钮监听
|
||||
document.onkeydown = (e) => {
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
return false;
|
||||
default:
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const audioPlay = () => {
|
||||
if (audio.value) {
|
||||
audio.value.play();
|
||||
}
|
||||
};
|
||||
|
||||
const audioPause = () => {
|
||||
if (audio.value) {
|
||||
audio.value.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const playMusicEvent = async () => {
|
||||
if (play.value) {
|
||||
store.commit('setPlayMusic', false);
|
||||
} else {
|
||||
store.commit('setPlayMusic', true);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-page {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@apply flex justify-center items-center overflow-hidden;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
@apply bg-black text-white shadow-xl flex flex-col relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
&-page {
|
||||
@apply flex flex-1 overflow-hidden;
|
||||
}
|
||||
.main {
|
||||
@apply flex-1 box-border flex flex-col overflow-hidden;
|
||||
height: 100%;
|
||||
&-content {
|
||||
@apply box-border flex-1 overflow-hidden;
|
||||
}
|
||||
}
|
||||
// :deep(.n-scrollbar-content) {
|
||||
// @apply pr-3;
|
||||
// }
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.layout-main {
|
||||
&-page {
|
||||
@apply pt-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- menu -->
|
||||
<div class="app-menu">
|
||||
<div class="app-menu-header">
|
||||
<div class="app-menu-logo">
|
||||
<img src="/icon.png" class="w-9 h-9" alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-menu-list">
|
||||
<div v-for="(item, index) in menus" :key="item.path" class="app-menu-item">
|
||||
<router-link class="app-menu-item-link" :to="item.path">
|
||||
<i class="iconfont app-menu-item-icon" :style="iconStyle(index)" :class="item.meta.icon"></i>
|
||||
<span v-if="isText" class="app-menu-item-text ml-3">{{ item.meta.title }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
isText: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '26px',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#aaa',
|
||||
},
|
||||
selectColor: {
|
||||
type: String,
|
||||
default: '#10B981',
|
||||
},
|
||||
menus: {
|
||||
type: Array as any,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const path = ref(route.path);
|
||||
watch(
|
||||
() => route.path,
|
||||
async (newParams) => {
|
||||
path.value = newParams;
|
||||
},
|
||||
);
|
||||
|
||||
const iconStyle = (index: number) => {
|
||||
const style = {
|
||||
fontSize: props.size,
|
||||
color: path.value === props.menus[index].path ? props.selectColor : props.color,
|
||||
};
|
||||
return style;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-menu {
|
||||
@apply flex-col items-center justify-center px-6;
|
||||
max-width: 100px;
|
||||
}
|
||||
.app-menu-item-link,
|
||||
.app-menu-header {
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
|
||||
.app-menu-item-link {
|
||||
@apply mb-6 mt-6;
|
||||
}
|
||||
|
||||
.app-menu-item-icon:hover {
|
||||
color: #10b981 !important;
|
||||
transform: scale(1.05);
|
||||
transition: 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.app-menu {
|
||||
max-width: 100%;
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
z-index: 999999;
|
||||
background-color: #000;
|
||||
&-header {
|
||||
display: none;
|
||||
}
|
||||
&-list {
|
||||
@apply flex justify-between;
|
||||
}
|
||||
&-item {
|
||||
&-link {
|
||||
@apply my-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,194 +0,0 @@
|
||||
<template>
|
||||
<n-drawer :show="musicFull" height="100vh" placement="bottom" :drawer-style="{ backgroundColor: 'transparent' }">
|
||||
<div id="drawer-target">
|
||||
<div
|
||||
class="drawer-back"
|
||||
:class="{ paused: !isPlaying }"
|
||||
:style="{ backgroundImage: `url(${getImgUrl(playMusic?.picUrl, '300y300')})` }"
|
||||
></div>
|
||||
<div class="music-img">
|
||||
<n-image ref="PicImgRef" :src="getImgUrl(playMusic?.picUrl, '300y300')" class="img" lazy preview-disabled />
|
||||
</div>
|
||||
<div class="music-content">
|
||||
<div class="music-content-name">{{ playMusic.song.name }}</div>
|
||||
<div class="music-content-singer">
|
||||
<span v-for="(item, index) in playMusic.song.artists" :key="index">
|
||||
{{ item.name }}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
<n-layout
|
||||
ref="lrcSider"
|
||||
class="music-lrc"
|
||||
style="height: 55vh"
|
||||
:native-scrollbar="false"
|
||||
@mouseover="mouseOverLayout"
|
||||
@mouseleave="mouseLeaveLayout"
|
||||
>
|
||||
<template v-for="(item, index) in lrcArray" :key="index">
|
||||
<div
|
||||
class="music-lrc-text"
|
||||
:class="{ 'now-text': isCurrentLrc(index, nowTime) }"
|
||||
@click="setAudioTime(index, audio)"
|
||||
>
|
||||
<div>{{ item.text }}</div>
|
||||
<div class="music-lrc-text-tr">{{ item.trText }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-layout>
|
||||
<!-- 时间矫正 -->
|
||||
<div class="music-content-time">
|
||||
<n-button @click="reduceCorrectionTime(0.2)">-</n-button>
|
||||
<n-button @click="addCorrectionTime(0.2)">+</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import {
|
||||
addCorrectionTime,
|
||||
isCurrentLrc,
|
||||
lrcArray,
|
||||
newLrcIndex,
|
||||
nowTime,
|
||||
reduceCorrectionTime,
|
||||
setAudioTime,
|
||||
} from '@/hooks/MusicHook';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const props = defineProps({
|
||||
musicFull: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
audio: {
|
||||
type: HTMLAudioElement,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 播放的音乐信息
|
||||
const playMusic = computed(() => store.state.playMusic as SongResult);
|
||||
const isPlaying = computed(() => store.state.play as boolean);
|
||||
// 获取歌词滚动dom
|
||||
const lrcSider = ref<any>(null);
|
||||
const isMouse = ref(false);
|
||||
// 歌词滚动方法
|
||||
const lrcScroll = () => {
|
||||
if (props.musicFull && !isMouse.value) {
|
||||
const top = newLrcIndex.value * 60 - 225;
|
||||
lrcSider.value.scrollTo({ top, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
const mouseOverLayout = () => {
|
||||
isMouse.value = true;
|
||||
};
|
||||
const mouseLeaveLayout = () => {
|
||||
setTimeout(() => {
|
||||
isMouse.value = false;
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
lrcScroll,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@keyframes round {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.drawer-back {
|
||||
@apply absolute bg-cover bg-center opacity-70;
|
||||
filter: blur(80px) brightness(80%);
|
||||
z-index: -1;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
animation: round 20s linear infinite;
|
||||
}
|
||||
|
||||
.drawer-back.paused {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
#drawer-target {
|
||||
@apply top-0 left-0 absolute w-full h-full overflow-hidden rounded px-24 pt-24 pb-48 flex items-center;
|
||||
backdrop-filter: blur(20px);
|
||||
background-color: rgba(0, 0, 0, 0.747);
|
||||
animation-duration: 300ms;
|
||||
|
||||
.music-img {
|
||||
@apply flex-1 flex justify-center mr-24;
|
||||
|
||||
.img {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
@apply rounded-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.music-content {
|
||||
@apply flex flex-col justify-center items-center;
|
||||
|
||||
&-name {
|
||||
@apply font-bold text-3xl py-2;
|
||||
}
|
||||
|
||||
&-singer {
|
||||
@apply text-base py-2;
|
||||
}
|
||||
}
|
||||
|
||||
.music-content-time {
|
||||
display: none;
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
|
||||
.music-lrc {
|
||||
background-color: inherit;
|
||||
width: 500px;
|
||||
height: 550px;
|
||||
.now-text {
|
||||
@apply text-red-500;
|
||||
}
|
||||
&-text {
|
||||
@apply text-white text-lg flex flex-col justify-center items-center cursor-pointer font-bold;
|
||||
height: 60px;
|
||||
transition: all 0.2s ease-out;
|
||||
|
||||
&:hover {
|
||||
@apply font-bold text-red-500;
|
||||
}
|
||||
|
||||
&-tr {
|
||||
@apply text-sm font-normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
#drawer-target {
|
||||
@apply flex-col p-4 pt-8;
|
||||
.music-img {
|
||||
display: none;
|
||||
}
|
||||
.music-lrc {
|
||||
height: calc(100vh - 260px) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,340 +0,0 @@
|
||||
<template>
|
||||
<!-- 展开全屏 -->
|
||||
<music-full ref="MusicFullRef" v-model:musicFull="musicFull" :audio="audio.value as HTMLAudioElement" />
|
||||
<!-- 底部播放栏 -->
|
||||
<div class="music-play-bar" :class="setAnimationClass('animate__bounceInUp')">
|
||||
<n-image
|
||||
:src="getImgUrl(playMusic?.picUrl, '300y300')"
|
||||
class="play-bar-img"
|
||||
lazy
|
||||
preview-disabled
|
||||
@click="setMusicFull"
|
||||
/>
|
||||
<div class="music-content">
|
||||
<div class="music-content-title">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||
{{ playMusic.name }}
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
<div class="music-content-name">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||
<span v-for="(item, index) in playMusic.song.artists" :key="index">
|
||||
{{ item.name }}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
|
||||
</span>
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-buttons">
|
||||
<div class="music-buttons-prev" @click="handlePrev">
|
||||
<i class="iconfont icon-prev"></i>
|
||||
</div>
|
||||
<div class="music-buttons-play" @click="playMusicEvent">
|
||||
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
|
||||
</div>
|
||||
<div class="music-buttons-next" @click="handleEnded">
|
||||
<i class="iconfont icon-next"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-time">
|
||||
<div class="time">{{ getNowTime }}</div>
|
||||
<n-slider v-model:value="timeSlider" :step="0.05" :tooltip="false"></n-slider>
|
||||
<div class="time">{{ getAllTime }}</div>
|
||||
</div>
|
||||
<div class="audio-volume">
|
||||
<div>
|
||||
<i class="iconfont icon-notificationfill"></i>
|
||||
</div>
|
||||
<n-slider v-model:value="volumeSlider" :step="0.01" :tooltip="false"></n-slider>
|
||||
</div>
|
||||
<div class="audio-button">
|
||||
<!-- <n-tooltip trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont icon-likefill"></i>
|
||||
</template>
|
||||
喜欢
|
||||
</n-tooltip> -->
|
||||
<!-- <n-tooltip trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont icon-Play" @click="parsingMusic"></i>
|
||||
</template>
|
||||
解析播放
|
||||
</n-tooltip> -->
|
||||
<n-tooltip class="music-lyric" trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont ri-netease-cloud-music-line" @click="openLyric"></i>
|
||||
</template>
|
||||
歌词
|
||||
</n-tooltip>
|
||||
<n-popover trigger="click" :z-index="99999999" content-class="music-play" raw :show-arrow="false" :delay="200">
|
||||
<template #trigger>
|
||||
<n-tooltip trigger="manual" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont icon-list"></i>
|
||||
</template>
|
||||
播放列表
|
||||
</n-tooltip>
|
||||
</template>
|
||||
<div class="music-play-list">
|
||||
<div class="music-play-list-back"></div>
|
||||
<n-scrollbar>
|
||||
<div class="music-play-list-content">
|
||||
<song-item v-for="item in playList" :key="item.id" :item="item" mini></song-item>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
<!-- 播放音乐 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
import { allTime, loadLrc, nowTime, openLyric, sendLyricToWin } from '@/hooks/MusicHook';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils';
|
||||
|
||||
import MusicFull from './MusicFull.vue';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
// 播放的音乐信息
|
||||
const playMusic = computed(() => store.state.playMusic as SongResult);
|
||||
// 是否播放
|
||||
const play = computed(() => store.state.play as boolean);
|
||||
|
||||
const playList = computed(() => store.state.playList as SongResult[]);
|
||||
|
||||
const audio = {
|
||||
value: document.querySelector('#MusicAudio') as HTMLAudioElement,
|
||||
};
|
||||
|
||||
watch(
|
||||
() => store.state.playMusicUrl,
|
||||
() => {
|
||||
loadLrc(playMusic.value.id);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const audioPlay = () => {
|
||||
if (audio.value) {
|
||||
audio.value.play();
|
||||
}
|
||||
};
|
||||
|
||||
// 计算属性 获取当前播放时间的进度
|
||||
const timeSlider = computed({
|
||||
get: () => (nowTime.value / allTime.value) * 100,
|
||||
set: (value) => {
|
||||
if (!audio.value) return;
|
||||
audio.value.currentTime = (value * allTime.value) / 100;
|
||||
audioPlay();
|
||||
store.commit('setPlayMusic', true);
|
||||
},
|
||||
});
|
||||
|
||||
// 音量条
|
||||
const audioVolume = ref(1);
|
||||
const volumeSlider = computed({
|
||||
get: () => audioVolume.value * 100,
|
||||
set: (value) => {
|
||||
if (!audio.value) return;
|
||||
audio.value.volume = value / 100;
|
||||
},
|
||||
});
|
||||
// 获取当前播放时间
|
||||
const getNowTime = computed(() => {
|
||||
return secondToMinute(nowTime.value);
|
||||
});
|
||||
|
||||
// 获取总时间
|
||||
const getAllTime = computed(() => {
|
||||
return secondToMinute(allTime.value);
|
||||
});
|
||||
|
||||
// 监听音乐播放 获取时间
|
||||
const onAudio = () => {
|
||||
if (audio.value) {
|
||||
audio.value.removeEventListener('timeupdate', handleGetAudioTime);
|
||||
audio.value.removeEventListener('ended', handleEnded);
|
||||
audio.value.addEventListener('timeupdate', handleGetAudioTime);
|
||||
audio.value.addEventListener('ended', handleEnded);
|
||||
// 监听音乐播放暂停
|
||||
audio.value.addEventListener('pause', () => {
|
||||
store.commit('setPlayMusic', false);
|
||||
});
|
||||
audio.value.addEventListener('play', () => {
|
||||
store.commit('setPlayMusic', true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onAudio();
|
||||
|
||||
function handleEnded() {
|
||||
store.commit('nextPlay');
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
store.commit('prevPlay');
|
||||
}
|
||||
|
||||
const MusicFullRef = ref<any>(null);
|
||||
|
||||
function handleGetAudioTime(this: HTMLAudioElement) {
|
||||
// 监听音频播放的实时时间事件
|
||||
const audio = this as HTMLAudioElement;
|
||||
// 获取当前播放时间
|
||||
nowTime.value = Math.floor(audio.currentTime);
|
||||
// 获取总时间
|
||||
allTime.value = audio.duration;
|
||||
// 获取音量
|
||||
audioVolume.value = audio.volume;
|
||||
sendLyricToWin(store.state.isPlay);
|
||||
MusicFullRef.value?.lrcScroll();
|
||||
}
|
||||
|
||||
// 播放暂停按钮事件
|
||||
const playMusicEvent = async () => {
|
||||
if (play.value) {
|
||||
store.commit('setPlayMusic', false);
|
||||
} else {
|
||||
store.commit('setPlayMusic', true);
|
||||
}
|
||||
};
|
||||
|
||||
const musicFull = ref(false);
|
||||
|
||||
// 设置musicFull
|
||||
const setMusicFull = () => {
|
||||
musicFull.value = !musicFull.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text-ellipsis {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.music-play-bar {
|
||||
@apply h-20 w-full absolute bottom-0 left-0 flex items-center rounded-t-2xl overflow-hidden box-border px-6 py-2;
|
||||
z-index: 9999;
|
||||
box-shadow: 0px 0px 10px 2px rgba(203, 203, 203, 0.034);
|
||||
background-color: rgba(0, 0, 0, 0.747);
|
||||
animation-duration: 0.5s !important;
|
||||
.music-content {
|
||||
width: 140px;
|
||||
@apply ml-4;
|
||||
|
||||
&-title {
|
||||
@apply text-base text-white;
|
||||
}
|
||||
|
||||
&-name {
|
||||
@apply text-xs mt-1;
|
||||
@apply text-gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-bar-img {
|
||||
@apply w-14 h-14 rounded-2xl;
|
||||
}
|
||||
|
||||
.music-buttons {
|
||||
@apply mx-6;
|
||||
|
||||
.iconfont {
|
||||
@apply text-2xl hover:text-green-500 transition;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply text-xl hover:text-white;
|
||||
}
|
||||
|
||||
@apply flex items-center;
|
||||
|
||||
> div {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
&-play {
|
||||
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition;
|
||||
background: #383838;
|
||||
}
|
||||
}
|
||||
|
||||
.music-time {
|
||||
@apply flex flex-1 items-center;
|
||||
|
||||
.time {
|
||||
@apply mx-4 mt-1;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-volume {
|
||||
width: 140px;
|
||||
@apply flex items-center mx-4;
|
||||
|
||||
.iconfont {
|
||||
@apply text-2xl hover:text-green-500 transition cursor-pointer mr-4;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-button {
|
||||
@apply flex items-center mx-4;
|
||||
|
||||
.iconfont {
|
||||
@apply text-2xl hover:text-green-500 transition cursor-pointer m-4;
|
||||
}
|
||||
}
|
||||
|
||||
.music-play {
|
||||
&-list {
|
||||
height: 50vh;
|
||||
@apply relative rounded-3xl overflow-hidden;
|
||||
&-back {
|
||||
backdrop-filter: blur(20px);
|
||||
@apply absolute top-0 left-0 w-full h-full bg-gray-800 bg-opacity-75;
|
||||
}
|
||||
&-content {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.music-play-bar {
|
||||
@apply px-4;
|
||||
bottom: 70px;
|
||||
}
|
||||
.music-time {
|
||||
display: none;
|
||||
}
|
||||
.ri-netease-cloud-music-line {
|
||||
display: none;
|
||||
}
|
||||
.audio-volume {
|
||||
display: none;
|
||||
}
|
||||
.audio-button {
|
||||
@apply mx-0;
|
||||
}
|
||||
.music-buttons {
|
||||
@apply m-0;
|
||||
&-prev,
|
||||
&-next {
|
||||
display: none;
|
||||
}
|
||||
&-play {
|
||||
@apply m-0;
|
||||
}
|
||||
}
|
||||
.music-content {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,154 +0,0 @@
|
||||
<template>
|
||||
<div class="search-box flex">
|
||||
<div class="search-box-input flex-1">
|
||||
<n-input
|
||||
v-model:value="searchValue"
|
||||
size="medium"
|
||||
round
|
||||
:placeholder="hotSearchKeyword"
|
||||
class="border border-gray-600"
|
||||
@keydown.enter="search"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="iconfont icon-search"></i>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<div class="w-20 px-3 flex justify-between items-center">
|
||||
<div>{{ searchTypeOptions.find((item) => item.key === searchType)?.label }}</div>
|
||||
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
|
||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
<div class="user-box">
|
||||
<n-dropdown trigger="hover" :options="userSetOptions" @select="selectItem">
|
||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
||||
</n-dropdown>
|
||||
<n-avatar
|
||||
v-if="store.state.user"
|
||||
class="ml-2 cursor-pointer"
|
||||
circle
|
||||
size="medium"
|
||||
:src="getImgUrl(store.state.user.avatarUrl)"
|
||||
/>
|
||||
<div v-else class="mx-2 rounded-full cursor-pointer text-sm" @click="toLogin">登录</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getSearchKeyword } from '@/api/home';
|
||||
import { getUserDetail, logout } from '@/api/login';
|
||||
import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const userSetOptions = ref(USER_SET_OPTIONS);
|
||||
|
||||
// 推荐热搜词
|
||||
const hotSearchKeyword = ref('搜索点什么吧...');
|
||||
const hotSearchValue = ref('');
|
||||
const loadHotSearchKeyword = async () => {
|
||||
const { data } = await getSearchKeyword();
|
||||
hotSearchKeyword.value = data.data.showKeyword;
|
||||
hotSearchValue.value = data.data.realkeyword;
|
||||
};
|
||||
|
||||
const loadPage = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const { data } = await getUserDetail();
|
||||
store.state.user = data.profile;
|
||||
localStorage.setItem('user', JSON.stringify(data.profile));
|
||||
};
|
||||
|
||||
loadPage();
|
||||
|
||||
watchEffect(() => {
|
||||
if (store.state.user) {
|
||||
userSetOptions.value = USER_SET_OPTIONS;
|
||||
} else {
|
||||
userSetOptions.value = USER_SET_OPTIONS.filter((item) => item.key !== 'logout');
|
||||
}
|
||||
});
|
||||
|
||||
const toLogin = () => {
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
// 页面初始化
|
||||
onMounted(() => {
|
||||
loadHotSearchKeyword();
|
||||
loadPage();
|
||||
});
|
||||
|
||||
// 搜索词
|
||||
const searchValue = ref('');
|
||||
const searchType = ref(1);
|
||||
const search = () => {
|
||||
const { value } = searchValue;
|
||||
if (value === '') {
|
||||
searchValue.value = hotSearchValue.value;
|
||||
return;
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: '/search',
|
||||
query: {
|
||||
keyword: value,
|
||||
type: searchType.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const selectSearchType = (key: number) => {
|
||||
searchType.value = key;
|
||||
};
|
||||
|
||||
const searchTypeOptions = ref(SEARCH_TYPES);
|
||||
|
||||
const selectItem = async (key: string) => {
|
||||
// switch 判断
|
||||
switch (key) {
|
||||
case 'logout':
|
||||
logout().then(() => {
|
||||
store.state.user = null;
|
||||
localStorage.clear();
|
||||
router.push('/login');
|
||||
});
|
||||
break;
|
||||
case 'login':
|
||||
router.push('/login');
|
||||
break;
|
||||
case 'set':
|
||||
router.push('/set');
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-box {
|
||||
@apply ml-4 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
.search-box {
|
||||
@apply pb-4 pr-4;
|
||||
}
|
||||
.search-box-input {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.search-box {
|
||||
@apply pl-4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<div id="title-bar" @mousedown="drag">
|
||||
<div id="title">Alger Music</div>
|
||||
<div id="buttons">
|
||||
<button @click="minimize">
|
||||
<i class="iconfont icon-minisize"></i>
|
||||
</button>
|
||||
<button @click="close">
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDialog } from 'naive-ui';
|
||||
|
||||
const dialog = useDialog();
|
||||
const windowData = window as any;
|
||||
|
||||
const minimize = () => {
|
||||
windowData.electronAPI.minimize();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要退出吗?',
|
||||
positiveText: '最小化',
|
||||
negativeText: '关闭',
|
||||
onPositiveClick: () => {
|
||||
windowData.electronAPI.miniTray();
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
windowData.electronAPI.close();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const drag = (event: MouseEvent) => {
|
||||
windowData.electronAPI.dragStart(event);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#title-bar {
|
||||
-webkit-app-region: drag;
|
||||
@apply flex justify-between text-white px-6 py-2 select-none relative;
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
#buttons {
|
||||
@apply flex gap-4;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply hover:text-green-500;
|
||||
}
|
||||
</style>
|
||||
93
src/main/index.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils';
|
||||
import { app, ipcMain, nativeImage } from 'electron';
|
||||
import { join } from 'path';
|
||||
|
||||
import { loadLyricWindow } from './lyric';
|
||||
import { initializeCacheManager } from './modules/cache';
|
||||
import { initializeConfig } from './modules/config';
|
||||
import { initializeFileManager } from './modules/fileManager';
|
||||
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
||||
import { initializeTray } from './modules/tray';
|
||||
import { createMainWindow, initializeWindowManager } from './modules/window';
|
||||
import { startMusicApi } from './server';
|
||||
|
||||
// 导入所有图标
|
||||
const iconPath = join(__dirname, '../../resources');
|
||||
const icon = nativeImage.createFromPath(
|
||||
process.platform === 'darwin'
|
||||
? join(iconPath, 'icon.icns')
|
||||
: process.platform === 'win32'
|
||||
? join(iconPath, 'favicon.ico')
|
||||
: join(iconPath, 'icon.png')
|
||||
);
|
||||
|
||||
let mainWindow: Electron.BrowserWindow;
|
||||
|
||||
// 初始化应用
|
||||
function initialize() {
|
||||
// 初始化配置管理
|
||||
initializeConfig();
|
||||
// 初始化缓存管理
|
||||
initializeCacheManager();
|
||||
// 初始化文件管理
|
||||
initializeFileManager();
|
||||
// 初始化窗口管理
|
||||
initializeWindowManager();
|
||||
|
||||
// 创建主窗口
|
||||
mainWindow = createMainWindow(icon);
|
||||
|
||||
// 初始化托盘
|
||||
initializeTray(iconPath, mainWindow);
|
||||
|
||||
// 启动音乐API
|
||||
startMusicApi();
|
||||
|
||||
// 加载歌词窗口
|
||||
loadLyricWindow(ipcMain, mainWindow);
|
||||
|
||||
// 初始化快捷键
|
||||
initializeShortcuts(mainWindow);
|
||||
}
|
||||
|
||||
// 应用程序准备就绪时的处理
|
||||
app.whenReady().then(() => {
|
||||
// 设置应用ID
|
||||
electronApp.setAppUserModelId('com.alger.music');
|
||||
|
||||
// 监听窗口创建事件
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
// 初始化应用
|
||||
initialize();
|
||||
|
||||
// macOS 激活应用时的处理
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null) initialize();
|
||||
});
|
||||
});
|
||||
|
||||
// 监听快捷键更新
|
||||
ipcMain.on('update-shortcuts', () => {
|
||||
registerShortcuts(mainWindow);
|
||||
});
|
||||
|
||||
// 所有窗口关闭时的处理
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// 重启应用
|
||||
ipcMain.on('restart', () => {
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
});
|
||||
|
||||
// 获取系统架构信息
|
||||
ipcMain.on('get-arch', (event) => {
|
||||
event.returnValue = process.arch;
|
||||
});
|
||||
176
src/main/lyric.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { BrowserWindow, IpcMain, screen } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import path, { join } from 'path';
|
||||
|
||||
const store = new Store();
|
||||
let lyricWindow: BrowserWindow | null = null;
|
||||
|
||||
const createWin = () => {
|
||||
console.log('Creating lyric window');
|
||||
|
||||
// 获取保存的窗口位置
|
||||
const windowBounds =
|
||||
(store.get('lyricWindowBounds') as {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}) || {};
|
||||
const { x, y, width, height } = windowBounds;
|
||||
|
||||
// 获取屏幕尺寸
|
||||
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
||||
|
||||
// 验证保存的位置是否有效
|
||||
const validPosition =
|
||||
x !== undefined && y !== undefined && x >= 0 && y >= 0 && x < screenWidth && y < screenHeight;
|
||||
|
||||
lyricWindow = new BrowserWindow({
|
||||
width: width || 800,
|
||||
height: height || 200,
|
||||
x: validPosition ? x : undefined,
|
||||
y: validPosition ? y : undefined,
|
||||
frame: false,
|
||||
show: false,
|
||||
transparent: true,
|
||||
hasShadow: false,
|
||||
alwaysOnTop: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
contextIsolation: true
|
||||
}
|
||||
});
|
||||
|
||||
// 监听窗口关闭事件
|
||||
lyricWindow.on('closed', () => {
|
||||
if (lyricWindow) {
|
||||
lyricWindow.destroy();
|
||||
lyricWindow = null;
|
||||
}
|
||||
});
|
||||
|
||||
return lyricWindow;
|
||||
};
|
||||
|
||||
export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void => {
|
||||
const showLyricWindow = () => {
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
if (lyricWindow.isMinimized()) {
|
||||
lyricWindow.restore();
|
||||
}
|
||||
lyricWindow.focus();
|
||||
lyricWindow.show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
ipcMain.on('open-lyric', () => {
|
||||
console.log('Received open-lyric request');
|
||||
|
||||
if (showLyricWindow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Creating new lyric window');
|
||||
const win = createWin();
|
||||
|
||||
if (!win) {
|
||||
console.error('Failed to create lyric window');
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
win.webContents.openDevTools({ mode: 'detach' });
|
||||
win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/#/lyric`);
|
||||
} else {
|
||||
const distPath = path.resolve(__dirname, '../renderer');
|
||||
win.loadURL(`file://${distPath}/index.html#/lyric`);
|
||||
}
|
||||
|
||||
win.setMinimumSize(600, 200);
|
||||
win.setSkipTaskbar(true);
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
console.log('Lyric window ready to show');
|
||||
win.show();
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('send-lyric', (_, data) => {
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
try {
|
||||
lyricWindow.webContents.send('receive-lyric', data);
|
||||
} catch (error) {
|
||||
console.error('Error processing lyric data:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('top-lyric', (_, data) => {
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
lyricWindow.setAlwaysOnTop(data);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('close-lyric', () => {
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
lyricWindow.webContents.send('lyric-window-close');
|
||||
mainWin.webContents.send('lyric-control-back', 'close');
|
||||
lyricWindow.destroy();
|
||||
lyricWindow = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理鼠标事件
|
||||
ipcMain.on('mouseenter-lyric', () => {
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
lyricWindow.setIgnoreMouseEvents(true);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('mouseleave-lyric', () => {
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
lyricWindow.setIgnoreMouseEvents(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理拖动移动
|
||||
ipcMain.on('lyric-drag-move', (_, { deltaX, deltaY }) => {
|
||||
if (!lyricWindow || lyricWindow.isDestroyed()) return;
|
||||
|
||||
const [currentX, currentY] = lyricWindow.getPosition();
|
||||
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
||||
const [windowWidth, windowHeight] = lyricWindow.getSize();
|
||||
|
||||
// 计算新位置,确保窗口不会移出屏幕
|
||||
const newX = Math.max(0, Math.min(currentX + deltaX, screenWidth - windowWidth));
|
||||
const newY = Math.max(0, Math.min(currentY + deltaY, screenHeight - windowHeight));
|
||||
|
||||
lyricWindow.setPosition(newX, newY);
|
||||
|
||||
// 保存新位置
|
||||
store.set('lyricWindowBounds', {
|
||||
...lyricWindow.getBounds(),
|
||||
x: newX,
|
||||
y: newY
|
||||
});
|
||||
});
|
||||
|
||||
// 添加鼠标穿透事件处理
|
||||
ipcMain.on('set-ignore-mouse', (_, shouldIgnore) => {
|
||||
if (!lyricWindow || lyricWindow.isDestroyed()) return;
|
||||
|
||||
lyricWindow.setIgnoreMouseEvents(shouldIgnore, { forward: true });
|
||||
});
|
||||
|
||||
// 添加播放控制处理
|
||||
ipcMain.on('control-back', (_, command) => {
|
||||
console.log('command', command);
|
||||
if (mainWin && !mainWin.isDestroyed()) {
|
||||
console.log('Sending control-back command:', command);
|
||||
mainWin.webContents.send('lyric-control-back', command);
|
||||
}
|
||||
});
|
||||
};
|
||||
89
src/main/modules/cache.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
|
||||
interface LyricData {
|
||||
id: number;
|
||||
data: any;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface StoreSchema {
|
||||
lyrics: Record<number, LyricData>;
|
||||
}
|
||||
|
||||
class CacheManager {
|
||||
private store: Store<StoreSchema>;
|
||||
|
||||
constructor() {
|
||||
this.store = new Store<StoreSchema>({
|
||||
name: 'lyrics',
|
||||
defaults: {
|
||||
lyrics: {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async cacheLyric(id: number, data: any) {
|
||||
try {
|
||||
const lyrics = this.store.get('lyrics');
|
||||
lyrics[id] = {
|
||||
id,
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
this.store.set('lyrics', lyrics);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error caching lyric:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getCachedLyric(id: number) {
|
||||
try {
|
||||
const lyrics = this.store.get('lyrics');
|
||||
const result = lyrics[id];
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
// 检查缓存是否过期(24小时)
|
||||
if (Date.now() - result.timestamp > 24 * 60 * 60 * 1000) {
|
||||
delete lyrics[id];
|
||||
this.store.set('lyrics', lyrics);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Error getting cached lyric:', error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async clearLyricCache() {
|
||||
try {
|
||||
this.store.set('lyrics', {});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error clearing lyric cache:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const cacheManager = new CacheManager();
|
||||
|
||||
export function initializeCacheManager() {
|
||||
// 添加歌词缓存相关的 IPC 处理
|
||||
ipcMain.handle('cache-lyric', async (_, id: number, lyricData: any) => {
|
||||
return await cacheManager.cacheLyric(id, lyricData);
|
||||
});
|
||||
|
||||
ipcMain.handle('get-cached-lyric', async (_, id: number) => {
|
||||
return await cacheManager.getCachedLyric(id);
|
||||
});
|
||||
|
||||
ipcMain.handle('clear-lyric-cache', async () => {
|
||||
return await cacheManager.clearLyricCache();
|
||||
});
|
||||
}
|
||||
50
src/main/modules/config.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { app, ipcMain } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
|
||||
import set from '../set.json';
|
||||
import { defaultShortcuts } from './shortcuts';
|
||||
|
||||
interface StoreType {
|
||||
set: {
|
||||
isProxy: boolean;
|
||||
noAnimate: boolean;
|
||||
animationSpeed: number;
|
||||
author: string;
|
||||
authorUrl: string;
|
||||
musicApiPort: number;
|
||||
};
|
||||
shortcuts: typeof defaultShortcuts;
|
||||
}
|
||||
|
||||
let store: Store<StoreType>;
|
||||
|
||||
/**
|
||||
* 初始化配置管理
|
||||
*/
|
||||
export function initializeConfig() {
|
||||
store = new Store<StoreType>({
|
||||
name: 'config',
|
||||
defaults: {
|
||||
set,
|
||||
shortcuts: defaultShortcuts
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function getStore() {
|
||||
return store;
|
||||
}
|
||||
395
src/main/modules/fileManager.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
import axios from 'axios';
|
||||
import { app, dialog, ipcMain, protocol, shell } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 3;
|
||||
const downloadQueue: { url: string; filename: string; songInfo: any; type?: string }[] = [];
|
||||
let activeDownloads = 0;
|
||||
|
||||
// 创建一个store实例用于存储下载历史
|
||||
const downloadStore = new Store({
|
||||
name: 'downloads',
|
||||
defaults: {
|
||||
history: []
|
||||
}
|
||||
});
|
||||
|
||||
// 创建一个store实例用于存储音频缓存
|
||||
const audioCacheStore = new Store({
|
||||
name: 'audioCache',
|
||||
defaults: {
|
||||
cache: {}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化文件管理相关的IPC监听
|
||||
*/
|
||||
export function initializeFileManager() {
|
||||
// 注册本地文件协议
|
||||
protocol.registerFileProtocol('local', (request, callback) => {
|
||||
try {
|
||||
const decodedUrl = decodeURIComponent(request.url);
|
||||
const filePath = decodedUrl.replace('local://', '');
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error('File not found:', filePath);
|
||||
callback({ error: -6 }); // net::ERR_FILE_NOT_FOUND
|
||||
return;
|
||||
}
|
||||
|
||||
callback({ path: filePath });
|
||||
} catch (error) {
|
||||
console.error('Error handling local protocol:', error);
|
||||
callback({ error: -2 }); // net::FAILED
|
||||
}
|
||||
});
|
||||
|
||||
// 通用的选择目录处理
|
||||
ipcMain.handle('select-directory', async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory'],
|
||||
title: '选择目录'
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
// 通用的打开目录处理
|
||||
ipcMain.on('open-directory', (_, filePath) => {
|
||||
try {
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
shell.openPath(filePath);
|
||||
} else {
|
||||
shell.showItemInFolder(filePath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening path:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 下载音乐处理
|
||||
ipcMain.on('download-music', handleDownloadRequest);
|
||||
|
||||
// 检查文件是否已下载
|
||||
ipcMain.handle('check-music-downloaded', (_, filename: string) => {
|
||||
const store = new Store();
|
||||
const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');
|
||||
const filePath = path.join(downloadPath, `${filename}.mp3`);
|
||||
return fs.existsSync(filePath);
|
||||
});
|
||||
|
||||
// 删除已下载的音乐
|
||||
ipcMain.handle('delete-downloaded-music', async (_, filePath: string) => {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
// 先删除文件
|
||||
try {
|
||||
await fs.promises.unlink(filePath);
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error);
|
||||
}
|
||||
|
||||
// 删除对应的歌曲信息
|
||||
const store = new Store();
|
||||
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||
delete songInfos[filePath];
|
||||
store.set('downloadedSongs', songInfos);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取已下载音乐列表
|
||||
ipcMain.handle('get-downloaded-music', () => {
|
||||
try {
|
||||
const store = new Store();
|
||||
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||
|
||||
// 过滤出实际存在的文件
|
||||
const validSongs = Object.entries(songInfos)
|
||||
.filter(([path]) => fs.existsSync(path))
|
||||
.map(([_, info]) => info)
|
||||
.sort((a, b) => (b.downloadTime || 0) - (a.downloadTime || 0));
|
||||
|
||||
// 更新存储,移除不存在的文件记录
|
||||
const newSongInfos = validSongs.reduce((acc, song) => {
|
||||
acc[song.path] = song;
|
||||
return acc;
|
||||
}, {});
|
||||
store.set('downloadedSongs', newSongInfos);
|
||||
|
||||
return validSongs;
|
||||
} catch (error) {
|
||||
console.error('Error getting downloaded music:', error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// 检查歌曲是否已下载并返回本地路径
|
||||
ipcMain.handle('check-song-downloaded', (_, songId: number) => {
|
||||
const store = new Store();
|
||||
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||
|
||||
// 通过ID查找已下载的歌曲
|
||||
for (const [path, info] of Object.entries(songInfos)) {
|
||||
if (info.id === songId && fs.existsSync(path)) {
|
||||
return {
|
||||
isDownloaded: true,
|
||||
localPath: `local://${path}`,
|
||||
songInfo: info
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isDownloaded: false,
|
||||
localPath: '',
|
||||
songInfo: null
|
||||
};
|
||||
});
|
||||
|
||||
// 添加清除下载历史的处理函数
|
||||
ipcMain.on('clear-downloads-history', () => {
|
||||
downloadStore.set('history', []);
|
||||
});
|
||||
|
||||
// 添加清除音频缓存的处理函数
|
||||
ipcMain.on('clear-audio-cache', () => {
|
||||
audioCacheStore.set('cache', {});
|
||||
// 清除临时音频文件目录
|
||||
const tempDir = path.join(app.getPath('userData'), 'AudioCache');
|
||||
if (fs.existsSync(tempDir)) {
|
||||
try {
|
||||
fs.readdirSync(tempDir).forEach((file) => {
|
||||
const filePath = path.join(tempDir, file);
|
||||
if (file.endsWith('.mp3') || file.endsWith('.m4a')) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('清除音频缓存文件失败:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下载请求
|
||||
*/
|
||||
function handleDownloadRequest(
|
||||
event: Electron.IpcMainEvent,
|
||||
{
|
||||
url,
|
||||
filename,
|
||||
songInfo,
|
||||
type
|
||||
}: { url: string; filename: string; songInfo?: any; type?: string }
|
||||
) {
|
||||
// 检查是否已经在队列中或正在下载
|
||||
if (downloadQueue.some((item) => item.filename === filename)) {
|
||||
event.reply('music-download-error', {
|
||||
filename,
|
||||
error: '该歌曲已在下载队列中'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已下载
|
||||
const store = new Store();
|
||||
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||
|
||||
// 检查是否已下载(通过ID)
|
||||
const isDownloaded =
|
||||
songInfo?.id && Object.values(songInfos).some((info: any) => info.id === songInfo.id);
|
||||
|
||||
if (isDownloaded) {
|
||||
event.reply('music-download-error', {
|
||||
filename,
|
||||
error: '该歌曲已下载'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加到下载队列
|
||||
downloadQueue.push({ url, filename, songInfo, type });
|
||||
event.reply('music-download-queued', {
|
||||
filename,
|
||||
songInfo
|
||||
});
|
||||
|
||||
// 尝试开始下载
|
||||
processDownloadQueue(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下载队列
|
||||
*/
|
||||
async function processDownloadQueue(event: Electron.IpcMainEvent) {
|
||||
if (activeDownloads >= MAX_CONCURRENT_DOWNLOADS || downloadQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { url, filename, songInfo, type } = downloadQueue.shift()!;
|
||||
activeDownloads++;
|
||||
|
||||
try {
|
||||
await downloadMusic(event, { url, filename, songInfo, type });
|
||||
} finally {
|
||||
activeDownloads--;
|
||||
processDownloadQueue(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载音乐功能
|
||||
*/
|
||||
async function downloadMusic(
|
||||
event: Electron.IpcMainEvent,
|
||||
{
|
||||
url,
|
||||
filename,
|
||||
songInfo,
|
||||
type = 'mp3'
|
||||
}: { url: string; filename: string; songInfo: any; type?: string }
|
||||
) {
|
||||
let finalFilePath = '';
|
||||
let writer: fs.WriteStream | null = null;
|
||||
|
||||
try {
|
||||
const store = new Store();
|
||||
const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');
|
||||
|
||||
// 从URL中获取文件扩展名,如果没有则使用传入的type或默认mp3
|
||||
const urlExt = type ? `.${type}` : '.mp3';
|
||||
const filePath = path.join(downloadPath, `${filename}${urlExt}`);
|
||||
|
||||
// 检查文件是否已存在,如果存在则添加序号
|
||||
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 headResponse = await axios.head(url);
|
||||
const totalSize = parseInt(headResponse.headers['content-length'] || '0', 10);
|
||||
|
||||
// 开始下载
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
timeout: 30000 // 30秒超时
|
||||
});
|
||||
|
||||
writer = fs.createWriteStream(finalFilePath);
|
||||
let downloadedSize = 0;
|
||||
|
||||
// 使用 data 事件来跟踪下载进度
|
||||
response.data.on('data', (chunk: Buffer) => {
|
||||
downloadedSize += chunk.length;
|
||||
const progress = Math.round((downloadedSize / totalSize) * 100);
|
||||
event.reply('music-download-progress', {
|
||||
filename,
|
||||
progress,
|
||||
loaded: downloadedSize,
|
||||
total: totalSize,
|
||||
path: finalFilePath,
|
||||
status: progress === 100 ? 'completed' : 'downloading',
|
||||
songInfo: songInfo || {
|
||||
name: filename,
|
||||
ar: [{ name: '本地音乐' }],
|
||||
picUrl: '/images/default_cover.png'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 等待下载完成
|
||||
await new Promise((resolve, reject) => {
|
||||
writer!.on('finish', resolve);
|
||||
writer!.on('error', reject);
|
||||
response.data.pipe(writer!);
|
||||
});
|
||||
|
||||
// 验证文件是否完整下载
|
||||
const stats = fs.statSync(finalFilePath);
|
||||
if (stats.size !== totalSize) {
|
||||
throw new Error('文件下载不完整');
|
||||
}
|
||||
|
||||
// 保存下载信息
|
||||
try {
|
||||
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
|
||||
const defaultInfo = {
|
||||
name: filename,
|
||||
ar: [{ name: '本地音乐' }],
|
||||
picUrl: '/images/default_cover.png'
|
||||
};
|
||||
|
||||
const newSongInfo = {
|
||||
id: songInfo?.id || 0,
|
||||
name: songInfo?.name || filename,
|
||||
filename,
|
||||
picUrl: songInfo?.picUrl || defaultInfo.picUrl,
|
||||
ar: songInfo?.ar || defaultInfo.ar,
|
||||
size: totalSize,
|
||||
path: finalFilePath,
|
||||
downloadTime: Date.now(),
|
||||
al: songInfo?.al || { picUrl: songInfo?.picUrl || defaultInfo.picUrl },
|
||||
type: type || 'mp3'
|
||||
};
|
||||
|
||||
// 保存到下载记录
|
||||
songInfos[finalFilePath] = newSongInfo;
|
||||
store.set('downloadedSongs', songInfos);
|
||||
|
||||
// 添加到下载历史
|
||||
const history = downloadStore.get('history', []) as any[];
|
||||
history.unshift(newSongInfo);
|
||||
downloadStore.set('history', history);
|
||||
|
||||
// 发送下载完成事件
|
||||
event.reply('music-download-complete', {
|
||||
success: true,
|
||||
path: finalFilePath,
|
||||
filename,
|
||||
size: totalSize,
|
||||
songInfo: newSongInfo
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving download info:', error);
|
||||
throw new Error('保存下载信息失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Download error:', error);
|
||||
|
||||
// 清理未完成的下载
|
||||
if (writer) {
|
||||
writer.end();
|
||||
}
|
||||
if (finalFilePath && fs.existsSync(finalFilePath)) {
|
||||
try {
|
||||
fs.unlinkSync(finalFilePath);
|
||||
} catch (e) {
|
||||
console.error('Failed to delete incomplete download:', e);
|
||||
}
|
||||
}
|
||||
|
||||
event.reply('music-download-complete', {
|
||||
success: false,
|
||||
error: error.message || '下载失败',
|
||||
filename
|
||||
});
|
||||
}
|
||||
}
|
||||
88
src/main/modules/shortcuts.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { globalShortcut, ipcMain } from 'electron';
|
||||
|
||||
import { getStore } from './config';
|
||||
|
||||
// 添加获取平台信息的 IPC 处理程序
|
||||
ipcMain.on('get-platform', (event) => {
|
||||
event.returnValue = process.platform;
|
||||
});
|
||||
|
||||
// 定义默认快捷键
|
||||
export const defaultShortcuts = {
|
||||
togglePlay: 'CommandOrControl+Alt+P',
|
||||
prevPlay: 'CommandOrControl+Alt+Left',
|
||||
nextPlay: 'CommandOrControl+Alt+Right',
|
||||
volumeUp: 'CommandOrControl+Alt+Up',
|
||||
volumeDown: 'CommandOrControl+Alt+Down',
|
||||
toggleFavorite: 'CommandOrControl+Alt+L',
|
||||
toggleWindow: 'CommandOrControl+Alt+Shift+M'
|
||||
};
|
||||
|
||||
let mainWindowRef: Electron.BrowserWindow | null = null;
|
||||
|
||||
// 注册快捷键
|
||||
export function registerShortcuts(mainWindow: Electron.BrowserWindow) {
|
||||
mainWindowRef = mainWindow;
|
||||
const store = getStore();
|
||||
const shortcuts = store.get('shortcuts');
|
||||
|
||||
// 注销所有已注册的快捷键
|
||||
globalShortcut.unregisterAll();
|
||||
|
||||
// 显示/隐藏主窗口
|
||||
globalShortcut.register(shortcuts.toggleWindow, () => {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
// 播放/暂停
|
||||
globalShortcut.register(shortcuts.togglePlay, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'togglePlay');
|
||||
});
|
||||
|
||||
// 上一首
|
||||
globalShortcut.register(shortcuts.prevPlay, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'prevPlay');
|
||||
});
|
||||
|
||||
// 下一首
|
||||
globalShortcut.register(shortcuts.nextPlay, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'nextPlay');
|
||||
});
|
||||
|
||||
// 音量增加
|
||||
globalShortcut.register(shortcuts.volumeUp, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'volumeUp');
|
||||
});
|
||||
|
||||
// 音量减少
|
||||
globalShortcut.register(shortcuts.volumeDown, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'volumeDown');
|
||||
});
|
||||
|
||||
// 收藏当前歌曲
|
||||
globalShortcut.register(shortcuts.toggleFavorite, () => {
|
||||
mainWindow.webContents.send('global-shortcut', 'toggleFavorite');
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化快捷键
|
||||
export function initializeShortcuts(mainWindow: Electron.BrowserWindow) {
|
||||
mainWindowRef = mainWindow;
|
||||
registerShortcuts(mainWindow);
|
||||
|
||||
// 监听禁用快捷键事件
|
||||
ipcMain.on('disable-shortcuts', () => {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// 监听启用快捷键事件
|
||||
ipcMain.on('enable-shortcuts', () => {
|
||||
if (mainWindowRef) {
|
||||
registerShortcuts(mainWindowRef);
|
||||
}
|
||||
});
|
||||
}
|
||||
45
src/main/modules/tray.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { app, BrowserWindow, Menu, nativeImage, Tray } 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
@@ -0,0 +1,119 @@
|
||||
import { is } from '@electron-toolkit/utils';
|
||||
import { app, BrowserWindow, ipcMain, session, shell } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import { join } from 'path';
|
||||
|
||||
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;
|
||||
}
|
||||
30
src/main/server.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import fs from 'fs';
|
||||
import server from 'netease-cloud-music-api-alger/server';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { unblockMusic } from './unblockMusic';
|
||||
|
||||
const store = new Store();
|
||||
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
|
||||
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
|
||||
}
|
||||
|
||||
// 处理解锁音乐请求
|
||||
ipcMain.handle('unblock-music', async (_, id, data) => {
|
||||
return unblockMusic(id, data);
|
||||
});
|
||||
|
||||
async function startMusicApi(): Promise<void> {
|
||||
console.log('MUSIC API STARTED');
|
||||
|
||||
const port = (store.get('set') as any).musicApiPort || 30488;
|
||||
|
||||
await server.serveNcmApi({
|
||||
port
|
||||
});
|
||||
}
|
||||
|
||||
export { startMusicApi };
|
||||
18
src/main/set.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"isProxy": false,
|
||||
"proxyConfig": {
|
||||
"enable": false,
|
||||
"protocol": "http",
|
||||
"host": "127.0.0.1",
|
||||
"port": 7890
|
||||
},
|
||||
"enableRealIP": false,
|
||||
"realIP": "",
|
||||
"noAnimate": false,
|
||||
"animationSpeed": 1,
|
||||
"author": "Alger",
|
||||
"authorUrl": "https://github.com/algerkong",
|
||||
"musicApiPort": 30488,
|
||||
"closeAction": "ask",
|
||||
"musicQuality": "higher"
|
||||
}
|
||||
162
src/main/unblockMusic.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import match from '@unblockneteasemusic/server';
|
||||
import Store from 'electron-store';
|
||||
|
||||
type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili' | 'youtube';
|
||||
|
||||
interface SongData {
|
||||
name: string;
|
||||
artists: Array<{ name: string }>;
|
||||
album?: { name: string };
|
||||
}
|
||||
|
||||
interface ResponseData {
|
||||
url: string;
|
||||
br: number;
|
||||
size: number;
|
||||
md5?: string;
|
||||
platform?: Platform;
|
||||
gain?: number;
|
||||
}
|
||||
|
||||
interface UnblockResult {
|
||||
data: {
|
||||
data: ResponseData;
|
||||
params: {
|
||||
id: number;
|
||||
type: 'song';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface CacheData extends UnblockResult {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface CacheStore {
|
||||
[key: string]: CacheData;
|
||||
}
|
||||
|
||||
// 初始化缓存存储
|
||||
const store = new Store<CacheStore>({
|
||||
name: 'unblock-cache'
|
||||
});
|
||||
|
||||
// 缓存过期时间(24小时)
|
||||
const CACHE_EXPIRY = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* 检查缓存是否有效
|
||||
* @param cacheData 缓存数据
|
||||
* @returns boolean
|
||||
*/
|
||||
const isCacheValid = (cacheData: CacheData | null): boolean => {
|
||||
if (!cacheData) return false;
|
||||
const now = Date.now();
|
||||
return now - cacheData.timestamp < CACHE_EXPIRY;
|
||||
};
|
||||
|
||||
/**
|
||||
* 从缓存中获取数据
|
||||
* @param id 歌曲ID
|
||||
* @returns CacheData | null
|
||||
*/
|
||||
const getFromCache = (id: string | number): CacheData | null => {
|
||||
const cacheData = store.get(String(id)) as CacheData | null;
|
||||
if (isCacheValid(cacheData)) {
|
||||
return cacheData;
|
||||
}
|
||||
// 清除过期缓存
|
||||
store.delete(String(id));
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 将数据存入缓存
|
||||
* @param id 歌曲ID
|
||||
* @param data 解析结果
|
||||
*/
|
||||
const saveToCache = (id: string | number, data: UnblockResult): void => {
|
||||
const cacheData: CacheData = {
|
||||
...data,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
store.set(String(id), cacheData);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理过期缓存
|
||||
*/
|
||||
const cleanExpiredCache = (): void => {
|
||||
const allData = store.store;
|
||||
Object.entries(allData).forEach(([id, data]) => {
|
||||
if (!isCacheValid(data)) {
|
||||
store.delete(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 音乐解析函数
|
||||
* @param id 歌曲ID
|
||||
* @param songData 歌曲信息
|
||||
* @param retryCount 重试次数
|
||||
* @returns Promise<UnblockResult>
|
||||
*/
|
||||
const unblockMusic = async (
|
||||
id: number | string,
|
||||
songData: SongData,
|
||||
retryCount = 3
|
||||
): Promise<UnblockResult> => {
|
||||
// 检查缓存
|
||||
const cachedData = getFromCache(id);
|
||||
if (cachedData) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
// 所有可用平台
|
||||
const platforms: Platform[] = ['migu', 'kugou', 'pyncmd', 'joox', 'kuwo', 'bilibili', 'youtube'];
|
||||
|
||||
const retry = async (attempt: number): Promise<UnblockResult> => {
|
||||
try {
|
||||
const data = await match(parseInt(String(id), 10), platforms, songData);
|
||||
const result: UnblockResult = {
|
||||
data: {
|
||||
data,
|
||||
params: {
|
||||
id: parseInt(String(id), 10),
|
||||
type: 'song'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 保存到缓存
|
||||
saveToCache(id, result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (attempt < retryCount) {
|
||||
// 延迟重试,每次重试增加延迟时间
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
||||
return retry(attempt + 1);
|
||||
}
|
||||
|
||||
// 所有重试都失败后,抛出详细错误
|
||||
throw new Error(
|
||||
`音乐解析失败 (ID: ${id}): ${err instanceof Error ? err.message : '未知错误'}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return retry(1);
|
||||
};
|
||||
|
||||
// 定期清理过期缓存(每小时执行一次)
|
||||
setInterval(cleanExpiredCache, 60 * 60 * 1000);
|
||||
|
||||
export {
|
||||
cleanExpiredCache, // 导出清理缓存函数,以便手动调用
|
||||
type Platform,
|
||||
type ResponseData,
|
||||
type SongData,
|
||||
unblockMusic,
|
||||
type UnblockResult
|
||||
};
|
||||
20
src/preload/index.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI;
|
||||
api: {
|
||||
sendLyric: (data: string) => void;
|
||||
openLyric: () => void;
|
||||
minimize: () => void;
|
||||
maximize: () => void;
|
||||
close: () => void;
|
||||
dragStart: (data: string) => void;
|
||||
miniTray: () => void;
|
||||
restart: () => void;
|
||||
unblockMusic: (id: number, data: any) => Promise<any>;
|
||||
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||
};
|
||||
$message: any;
|
||||
}
|
||||
}
|
||||
40
src/preload/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload';
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
minimize: () => ipcRenderer.send('minimize-window'),
|
||||
maximize: () => ipcRenderer.send('maximize-window'),
|
||||
close: () => ipcRenderer.send('close-window'),
|
||||
dragStart: (data) => ipcRenderer.send('drag-start', data),
|
||||
miniTray: () => ipcRenderer.send('mini-tray'),
|
||||
restart: () => ipcRenderer.send('restart'),
|
||||
openLyric: () => ipcRenderer.send('open-lyric'),
|
||||
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
||||
unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id),
|
||||
// 歌词缓存相关
|
||||
invoke: (channel: string, ...args: any[]) => {
|
||||
const validChannels = ['get-cached-lyric', 'cache-lyric', 'clear-lyric-cache'];
|
||||
if (validChannels.includes(channel)) {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
return Promise.reject(new Error(`Invalid channel: ${channel}`));
|
||||
}
|
||||
};
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||
contextBridge.exposeInMainWorld('api', api);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore (define in dts)
|
||||
window.electron = electronAPI;
|
||||
// @ts-ignore (define in dts)
|
||||
window.api = api;
|
||||
}
|
||||
54
src/renderer/App.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="app-container" :class="{ mobile: isMobile, noElectron: !isElectron }">
|
||||
<n-config-provider :theme="theme === 'dark' ? darkTheme : lightTheme">
|
||||
<n-dialog-provider>
|
||||
<n-message-provider>
|
||||
<router-view></router-view>
|
||||
</n-message-provider>
|
||||
</n-dialog-provider>
|
||||
</n-config-provider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { darkTheme, lightTheme } from 'naive-ui';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import homeRouter from '@/router/home';
|
||||
import store from '@/store';
|
||||
import { isElectron } from '@/utils';
|
||||
|
||||
import { isMobile } from './utils';
|
||||
|
||||
const theme = computed(() => {
|
||||
return store.state.theme;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('initializeSettings');
|
||||
store.dispatch('initializeTheme');
|
||||
if (isMobile.value) {
|
||||
store.commit(
|
||||
'setMenus',
|
||||
homeRouter.filter((item) => item.meta.isMobile)
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
@apply h-full w-full;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.text-base {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.html:has(.mobile) {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
21
src/renderer/api/artist.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 获取歌手详情
|
||||
export const getArtistDetail = (id) => {
|
||||
return request.get('/artist/detail', { params: { id } });
|
||||
};
|
||||
|
||||
// 获取歌手热门歌曲
|
||||
export const getArtistTopSongs = (params) => {
|
||||
return request.get('/artist/songs', {
|
||||
params: {
|
||||
...params,
|
||||
order: 'hot'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 获取歌手专辑
|
||||
export const getArtistAlbums = (params) => {
|
||||
return request.get('/artist/album', { params });
|
||||
};
|
||||
@@ -43,7 +43,7 @@ export const getRecommendMusic = (params: IRecommendMusicParams) => {
|
||||
|
||||
// 获取每日推荐
|
||||
export const getDayRecommend = () => {
|
||||
return request.get<IData<IDayRecommend>>('/recommend/songs');
|
||||
return request.get<IData<IData<IDayRecommend>>>('/recommend/songs');
|
||||
};
|
||||
|
||||
// 获取最新专辑推荐
|
||||
@@ -22,7 +22,7 @@ export function getListByTag(params: IListByTagParams) {
|
||||
// 根据cat 获取歌单列表
|
||||
export function getListByCat(params: IListByCatParams) {
|
||||
return request.get('/top/playlist', {
|
||||
params,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,6 +41,6 @@ export function logout() {
|
||||
export function loginByCellphone(phone: string, password: string) {
|
||||
return request.post('/login/cellphone', {
|
||||
phone,
|
||||
password,
|
||||
password
|
||||
});
|
||||
}
|
||||
75
src/renderer/api/music.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import store from '@/store';
|
||||
import type { ILyric } from '@/type/lyric';
|
||||
import { isElectron } from '@/utils';
|
||||
import request from '@/utils/request';
|
||||
import requestMusic from '@/utils/request_music';
|
||||
|
||||
// 获取音乐音质详情
|
||||
export const getMusicQualityDetail = (id: number) => {
|
||||
return request.get('/song/music/detail', { params: { id } });
|
||||
};
|
||||
|
||||
// 根据音乐Id获取音乐播放URl
|
||||
export const getMusicUrl = async (id: number) => {
|
||||
const res = await request.get('/song/download/url/v1', {
|
||||
params: {
|
||||
id,
|
||||
level: store.state.setData.musicQuality || 'higher'
|
||||
}
|
||||
});
|
||||
|
||||
if (res.data.data.url) {
|
||||
return { data: { data: [{ ...res.data.data }] } };
|
||||
}
|
||||
|
||||
return await request.get('/song/url/v1', {
|
||||
params: {
|
||||
id,
|
||||
level: store.state.setData.musicQuality || 'higher'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 获取歌曲详情
|
||||
export const getMusicDetail = (ids: Array<number>) => {
|
||||
return request.get('/song/detail', { params: { ids: ids.join(',') } });
|
||||
};
|
||||
|
||||
// 根据音乐Id获取音乐歌词
|
||||
export const getMusicLrc = async (id: number) => {
|
||||
if (isElectron) {
|
||||
// 先尝试从缓存获取
|
||||
const cachedLyric = await window.api.invoke('get-cached-lyric', id);
|
||||
console.log('cachedLyric', cachedLyric);
|
||||
if (cachedLyric) {
|
||||
return { data: cachedLyric };
|
||||
}
|
||||
}
|
||||
|
||||
// 如果缓存中没有,则从服务器获取
|
||||
const res = await request.get<ILyric>('/lyric', { params: { id } });
|
||||
|
||||
// 缓存完整的响应数据
|
||||
if (isElectron && res) {
|
||||
await window.api.invoke('cache-lyric', id, res.data);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getParsingMusicUrl = (id: number, data: any) => {
|
||||
if (isElectron) {
|
||||
return window.api.unblockMusic(id, data);
|
||||
}
|
||||
return requestMusic.get<any>('/music', { params: { id } });
|
||||
};
|
||||
|
||||
// 收藏歌曲
|
||||
export const likeSong = (id: number, like: boolean = true) => {
|
||||
return request.get('/like', { params: { id, like } });
|
||||
};
|
||||
|
||||
// 获取用户喜欢的音乐列表
|
||||
export const getLikedList = () => {
|
||||
return request.get('/likelist');
|
||||
};
|
||||
45
src/renderer/api/mv.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { IData } from '@/type';
|
||||
import { IMvUrlData } from '@/type/mv';
|
||||
import request from '@/utils/request';
|
||||
|
||||
interface MvParams {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
area?: string;
|
||||
}
|
||||
|
||||
// 获取 mv 排行
|
||||
export const getTopMv = (params: MvParams) => {
|
||||
return request({
|
||||
url: '/mv/all',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
// 获取所有mv
|
||||
export const getAllMv = (params: MvParams) => {
|
||||
return request({
|
||||
url: '/mv/all',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
// 获取 mv 数据
|
||||
export const getMvDetail = (mvid: string) => {
|
||||
return request.get('/mv/detail', {
|
||||
params: {
|
||||
mvid
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 获取 mv 地址
|
||||
export const getMvUrl = (id: Number) => {
|
||||
return request.get<IData<IMvUrlData>>('/mv/url', {
|
||||
params: {
|
||||
id
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -3,10 +3,12 @@ import request from '@/utils/request';
|
||||
interface IParams {
|
||||
keywords: string;
|
||||
type: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
// 搜索内容
|
||||
export const getSearch = (params: IParams) => {
|
||||
return request.get<any>('/cloudsearch', {
|
||||
params,
|
||||
params
|
||||
});
|
||||
};
|
||||
BIN
src/renderer/assets/alipay.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
3687
src/renderer/assets/css/animate.css
vendored
Normal file
11
src/renderer/assets/css/base.css
Normal file
@@ -0,0 +1,11 @@
|
||||
body {
|
||||
/* background-color: #000; */
|
||||
}
|
||||
|
||||
.n-popover:has(.music-play) {
|
||||
border-radius: 1.5rem !important;
|
||||
}
|
||||
.n-popover {
|
||||
border-radius: 0.5rem !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@@ -1,12 +1,13 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 2685283 */
|
||||
src: url('iconfont.woff2?t=1703643214551') format('woff2'),
|
||||
url('iconfont.woff?t=1703643214551') format('woff'),
|
||||
url('iconfont.ttf?t=1703643214551') format('truetype');
|
||||
font-family: 'iconfont'; /* Project id 2685283 */
|
||||
src:
|
||||
url('iconfont.woff2?t=1703643214551') format('woff2'),
|
||||
url('iconfont.woff?t=1703643214551') format('woff'),
|
||||
url('iconfont.ttf?t=1703643214551') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -14,270 +15,269 @@
|
||||
}
|
||||
|
||||
.icon-list:before {
|
||||
content: "\e603";
|
||||
content: '\e603';
|
||||
}
|
||||
|
||||
.icon-maxsize:before {
|
||||
content: "\e692";
|
||||
content: '\e692';
|
||||
}
|
||||
|
||||
.icon-close:before {
|
||||
content: "\e616";
|
||||
content: '\e616';
|
||||
}
|
||||
|
||||
.icon-minisize:before {
|
||||
content: "\e602";
|
||||
content: '\e602';
|
||||
}
|
||||
|
||||
.icon-shuaxin:before {
|
||||
content: "\e627";
|
||||
content: '\e627';
|
||||
}
|
||||
|
||||
.icon-icon_error:before {
|
||||
content: "\e615";
|
||||
content: '\e615';
|
||||
}
|
||||
|
||||
.icon-a-3User:before {
|
||||
content: "\e601";
|
||||
content: '\e601';
|
||||
}
|
||||
|
||||
.icon-Chat:before {
|
||||
content: "\e605";
|
||||
content: '\e605';
|
||||
}
|
||||
|
||||
.icon-Category:before {
|
||||
content: "\e606";
|
||||
content: '\e606';
|
||||
}
|
||||
|
||||
.icon-Document:before {
|
||||
content: "\e607";
|
||||
content: '\e607';
|
||||
}
|
||||
|
||||
.icon-Heart:before {
|
||||
content: "\e608";
|
||||
content: '\e608';
|
||||
}
|
||||
|
||||
.icon-Hide:before {
|
||||
content: "\e609";
|
||||
content: '\e609';
|
||||
}
|
||||
|
||||
.icon-Home:before {
|
||||
content: "\e60a";
|
||||
content: '\e60a';
|
||||
}
|
||||
|
||||
.icon-a-Image2:before {
|
||||
content: "\e60b";
|
||||
content: '\e60b';
|
||||
}
|
||||
|
||||
.icon-Profile:before {
|
||||
content: "\e60c";
|
||||
content: '\e60c';
|
||||
}
|
||||
|
||||
.icon-Search:before {
|
||||
content: "\e60d";
|
||||
content: '\e60d';
|
||||
}
|
||||
|
||||
.icon-Paper:before {
|
||||
content: "\e60e";
|
||||
content: '\e60e';
|
||||
}
|
||||
|
||||
.icon-Play:before {
|
||||
content: "\e60f";
|
||||
content: '\e60f';
|
||||
}
|
||||
|
||||
.icon-Setting:before {
|
||||
content: "\e610";
|
||||
content: '\e610';
|
||||
}
|
||||
|
||||
.icon-a-TicketStar:before {
|
||||
content: "\e611";
|
||||
content: '\e611';
|
||||
}
|
||||
|
||||
.icon-a-VolumeOff:before {
|
||||
content: "\e612";
|
||||
content: '\e612';
|
||||
}
|
||||
|
||||
.icon-a-VolumeUp:before {
|
||||
content: "\e613";
|
||||
content: '\e613';
|
||||
}
|
||||
|
||||
.icon-a-VolumeDown:before {
|
||||
content: "\e614";
|
||||
content: '\e614';
|
||||
}
|
||||
|
||||
.icon-stop:before {
|
||||
content: "\e600";
|
||||
content: '\e600';
|
||||
}
|
||||
|
||||
.icon-next:before {
|
||||
content: "\e6a9";
|
||||
content: '\e6a9';
|
||||
}
|
||||
|
||||
.icon-prev:before {
|
||||
content: "\e6ac";
|
||||
content: '\e6ac';
|
||||
}
|
||||
|
||||
.icon-play:before {
|
||||
content: "\e6aa";
|
||||
content: '\e6aa';
|
||||
}
|
||||
|
||||
.icon-xiasanjiaoxing:before {
|
||||
content: "\e642";
|
||||
content: '\e642';
|
||||
}
|
||||
|
||||
.icon-videofill:before {
|
||||
content: "\e7c7";
|
||||
content: '\e7c7';
|
||||
}
|
||||
|
||||
.icon-favorfill:before {
|
||||
content: "\e64b";
|
||||
content: '\e64b';
|
||||
}
|
||||
|
||||
.icon-favor:before {
|
||||
content: "\e64c";
|
||||
content: '\e64c';
|
||||
}
|
||||
|
||||
.icon-loading:before {
|
||||
content: "\e64f";
|
||||
content: '\e64f';
|
||||
}
|
||||
|
||||
.icon-search:before {
|
||||
content: "\e65c";
|
||||
content: '\e65c';
|
||||
}
|
||||
|
||||
.icon-likefill:before {
|
||||
content: "\e668";
|
||||
content: '\e668';
|
||||
}
|
||||
|
||||
.icon-like:before {
|
||||
content: "\e669";
|
||||
content: '\e669';
|
||||
}
|
||||
|
||||
.icon-notificationfill:before {
|
||||
content: "\e66a";
|
||||
content: '\e66a';
|
||||
}
|
||||
|
||||
.icon-notification:before {
|
||||
content: "\e66b";
|
||||
content: '\e66b';
|
||||
}
|
||||
|
||||
.icon-evaluate:before {
|
||||
content: "\e672";
|
||||
content: '\e672';
|
||||
}
|
||||
|
||||
.icon-homefill:before {
|
||||
content: "\e6bb";
|
||||
content: '\e6bb';
|
||||
}
|
||||
|
||||
.icon-link:before {
|
||||
content: "\e6bf";
|
||||
content: '\e6bf';
|
||||
}
|
||||
|
||||
.icon-roundaddfill:before {
|
||||
content: "\e6d8";
|
||||
content: '\e6d8';
|
||||
}
|
||||
|
||||
.icon-roundadd:before {
|
||||
content: "\e6d9";
|
||||
content: '\e6d9';
|
||||
}
|
||||
|
||||
.icon-add:before {
|
||||
content: "\e6da";
|
||||
content: '\e6da';
|
||||
}
|
||||
|
||||
.icon-appreciatefill:before {
|
||||
content: "\e6e3";
|
||||
content: '\e6e3';
|
||||
}
|
||||
|
||||
.icon-forwardfill:before {
|
||||
content: "\e6ea";
|
||||
content: '\e6ea';
|
||||
}
|
||||
|
||||
.icon-voicefill:before {
|
||||
content: "\e6f0";
|
||||
content: '\e6f0';
|
||||
}
|
||||
|
||||
.icon-wefill:before {
|
||||
content: "\e6f4";
|
||||
content: '\e6f4';
|
||||
}
|
||||
|
||||
.icon-keyboard:before {
|
||||
content: "\e71b";
|
||||
content: '\e71b';
|
||||
}
|
||||
|
||||
.icon-picfill:before {
|
||||
content: "\e72c";
|
||||
content: '\e72c';
|
||||
}
|
||||
|
||||
.icon-markfill:before {
|
||||
content: "\e730";
|
||||
content: '\e730';
|
||||
}
|
||||
|
||||
.icon-presentfill:before {
|
||||
content: "\e732";
|
||||
content: '\e732';
|
||||
}
|
||||
|
||||
.icon-peoplefill:before {
|
||||
content: "\e735";
|
||||
content: '\e735';
|
||||
}
|
||||
|
||||
.icon-read:before {
|
||||
content: "\e742";
|
||||
content: '\e742';
|
||||
}
|
||||
|
||||
.icon-backwardfill:before {
|
||||
content: "\e74d";
|
||||
content: '\e74d';
|
||||
}
|
||||
|
||||
.icon-playfill:before {
|
||||
content: "\e74f";
|
||||
content: '\e74f';
|
||||
}
|
||||
|
||||
.icon-all:before {
|
||||
content: "\e755";
|
||||
content: '\e755';
|
||||
}
|
||||
|
||||
.icon-hotfill:before {
|
||||
content: "\e757";
|
||||
content: '\e757';
|
||||
}
|
||||
|
||||
.icon-recordfill:before {
|
||||
content: "\e7a4";
|
||||
content: '\e7a4';
|
||||
}
|
||||
|
||||
.icon-full:before {
|
||||
content: "\e7bc";
|
||||
content: '\e7bc';
|
||||
}
|
||||
|
||||
.icon-favor_fill_light:before {
|
||||
content: "\e7ec";
|
||||
content: '\e7ec';
|
||||
}
|
||||
|
||||
.icon-round_favor_fill:before {
|
||||
content: "\e80a";
|
||||
content: '\e80a';
|
||||
}
|
||||
|
||||
.icon-round_location_fill:before {
|
||||
content: "\e80b";
|
||||
content: '\e80b';
|
||||
}
|
||||
|
||||
.icon-round_like_fill:before {
|
||||
content: "\e80c";
|
||||
content: '\e80c';
|
||||
}
|
||||
|
||||
.icon-round_people_fill:before {
|
||||
content: "\e80d";
|
||||
content: '\e80d';
|
||||
}
|
||||
|
||||
.icon-round_skin_fill:before {
|
||||
content: "\e80e";
|
||||
content: '\e80e';
|
||||
}
|
||||
|
||||
.icon-broadcast_fill:before {
|
||||
content: "\e81d";
|
||||
content: '\e81d';
|
||||
}
|
||||
|
||||
.icon-card_fill:before {
|
||||
content: "\e81f";
|
||||
content: '\e81f';
|
||||
}
|
||||
|
||||