Compare commits

...

46 Commits

Author SHA1 Message Date
alger
ba992b7c33 📃 docs: v3.4.0 2025-01-11 19:03:06 +08:00
alger
24d7c839c7 🌈 style: 添加 "animate.css" 2025-01-11 18:51:40 +08:00
alger
a4f3df80c9 📃 docs: v3.4.0 2025-01-11 18:45:42 +08:00
alger
866fec6ee3 feat: 优化收藏逻辑 本地和线上同步 添加批量下载 2025-01-11 18:38:34 +08:00
alger
8f7d6fbb8d feat: 设置页 添加捐赠支持列表 2025-01-11 18:22:14 +08:00
alger
62e26cae7d 🌈 style: 优化代码格式化 2025-01-10 22:49:55 +08:00
alger
ddb814da10 feat: v3.3.0 2025-01-06 22:33:13 +08:00
alger
e266ea8ef8 🐞 fix: 修复类型校验问题 2025-01-06 22:24:37 +08:00
alger
a894954641 🐞 fix: 修复类型校验问题 2025-01-06 22:15:25 +08:00
alger
f640ab9969 feat: v3.3.0 2025-01-06 22:10:20 +08:00
alger
9eb17fd978 feat: 优化登录失效 2025-01-06 22:03:50 +08:00
alger
020aca7384 feat: 添加音质选择 优化灰色歌曲解析 2025-01-06 20:54:42 +08:00
alger
fcc47dc0ff feat: 添加退出登录 2025-01-05 15:58:48 +08:00
alger
17ce268da6 feat: 修复未登录 收藏问题 2025-01-05 15:01:55 +08:00
alger
43c64b1b43 feat: 收藏功能改为接口对接 2025-01-04 16:58:08 +08:00
alger
11ced6b418 feat: 优化更新检查 下载 功能 2025-01-04 16:13:37 +08:00
alger
3d3992154a 🐞 fix: 修复歌词滚动问题 2025-01-04 00:26:30 +08:00
alger
81e7b67c7f 📃 docs: 3.2.0 2025-01-03 23:59:07 +08:00
alger
d7e94a342b feat: 添加代理功能和 realIP配置功能 2025-01-03 23:53:07 +08:00
alger
46f8067577 feat: 关闭应用的提示修改 可存储配置最小化 还是 关闭 2025-01-03 22:24:13 +08:00
alger
1dc7d0ceca 🐞 fix: 修复歌词页面与底栏冲突问题(#26) 修复搜索歌曲列表页面显示错误问题 (#33)
closed #26   #33
2025-01-03 22:03:26 +08:00
alger
ba64631a17 🐞 fix: 修复搜索类型切换 没有重新加载搜索的问题(#25)
closed #25
2025-01-03 21:28:48 +08:00
alger
cdb9524f04 feat: 解决检查更新请求失败问题 2025-01-02 00:45:01 +08:00
alger
5213aa13c5 🌈 style: 修复格式问题 2025-01-02 00:27:31 +08:00
alger
d870d0198f 🌈 style: 修复格式问题 2025-01-02 00:25:54 +08:00
alger
976a9afd2f 📃 docs: v3.1.0 2025-01-02 00:18:41 +08:00
alger
018218a5bf feat: 优化主入口代码 添加歌曲下载功能 2025-01-02 00:14:05 +08:00
alger
38a9d6ed31 feat: 完善网页版 安装应用功能 2025-01-01 22:42:25 +08:00
alger
8dab799939 feat: 修改更新检查功能 2025-01-01 15:05:49 +08:00
alger
1ddbe6f24e feat: 修改 github action 2025-01-01 14:48:31 +08:00
alger
4d5bcba6c7 fix: update macOS build config 2025-01-01 14:42:19 +08:00
alger
f833306b60 feat: 修改 github action 2025-01-01 14:31:27 +08:00
alger
4d92ed9963 🐞 fix: 修复mac 安装包损坏问题 2025-01-01 14:19:44 +08:00
alger
a22285156a feat: 修改 github action 添加更新日志 2025-01-01 14:01:55 +08:00
alger
d1029f16d6 feat: 修改 github action 2025-01-01 13:42:08 +08:00
alger
4908555635 feat: 修改 github action 2025-01-01 13:34:36 +08:00
alger
750cf7a484 🐞 fix: 去除无用导入 2025-01-01 13:26:06 +08:00
alger
a334743f6f feat: 添加 github action 自动 打包 发布 2025-01-01 13:14:56 +08:00
alger
14747cac10 feat: 优化打包和版本更新功能 2025-01-01 13:12:46 +08:00
alger
cc239aeaba 📃 docs: 修改文档 2025-01-01 02:44:39 +08:00
alger
eeda296589 📃 docs: 修改文档 2025-01-01 02:43:00 +08:00
alger
edb7ea201c 🌈 style: 去除无用提交 2025-01-01 02:30:37 +08:00
alger
17d20fa299 🦄 refactor: 重构整个项目 优化打包 修改后台服务为本地运行 添加更新版本检测功能 2025-01-01 02:25:18 +08:00
alger
f8d421c9b1 🐞 fix: 修复web移动端 页面空白问题 (#24)
closed #24
2024-12-30 11:20:23 +08:00
alger
dfdf02a17f feat: 优化主题效果 添加展开 menu功能 优化图片清晰度 添加随机播放功能(#20) 2024-12-29 00:43:39 +08:00
alger
abdb2bcd50 feat: 新增主题色切换功能 默认为日间主题可切换夜间 (#19、#21)
fixes #19  #21
2024-12-28 16:43:52 +08:00
177 changed files with 9461 additions and 3323 deletions

View File

@@ -1,13 +1,4 @@
snapshot*
dist
lib
es
esm
node_modules
static
cypress
script/test/cypress
_site
temp*
static/
!.prettierrc.js
dist
out
.gitignore

140
.eslintrc
View File

@@ -1,140 +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-nested-ternary": "off",
"no-console": "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", // 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)
"consistent-return": "off",
"no-promise-executor-return": "off",
"prefer-promise-reject-errors": "off"
}
}
]
}

135
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,135 @@
/* 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-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: {
'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
View 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 }}

4
.gitignore vendored
View File

@@ -17,4 +17,6 @@ dist.zip
bun.lockb
.env.*.local
.env.*.local
out

2
.npmrc
View File

@@ -1,2 +0,0 @@
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json

View File

@@ -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
View File

@@ -0,0 +1,5 @@
singleQuote: true
semi: true
printWidth: 100
trailingComma: none
endOfLine: auto

8
CHANGELOG.md Normal file
View File

@@ -0,0 +1,8 @@
# 更新日志
## [v3.4.0] - 2025-01-11
### ✨ 新功能
- 添加捐赠支持列表
- 添加收藏列表 批量下载功能
- 优化搜藏列表 可本地和线上收藏合并 如果收藏失败 会自动收藏到本地

201
LICENSE
View File

@@ -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.

View File

@@ -2,12 +2,13 @@
主要功能如下
- 音乐推荐
- 音乐播放
- 网易云登录
- 播放历史
- 桌面歌词
- 歌单 mv 搜索 专辑等功能
- 识别无法播放歌曲 并代理播放
- 识别无法播放歌曲 并解析播放
- 主题切换 更新检测
- 本地服务 不依赖线上服务
- 可听周杰伦(搜索专辑)
## 项目简介
@@ -19,9 +20,10 @@
QQ群:789288579
## 软件截图
![首页](./docs/img/image-7.png)
![歌词](./docs/img/image-6.png)
![搜索](./docs/img/image-8.png)
![首页](./docs/image.png)
![首页黑](./docs/image3.png)
![歌词](./docs/image1.png)
![桌面歌词](./docs/image2.png)
## 技术栈
@@ -30,16 +32,8 @@ QQ群:789288579
- TypeScript - JavaScript 的超集,添加了类型系统
- Electron - 跨平台桌面应用开发框架
- Vite - 下一代前端构建工具
### UI 框架
- Naive UI - 基于 Vue 3 的组件库
### 项目特点
- 完整的类型支持TypeScript
- 模块化设计
- 自动化组件和 API 导入
- 多平台支持Web、Desktop、Mobile Web
- 构建优化(代码分割、压缩)
## 咖啡☕️
| 微信 | 支付宝 |

148
app.js
View File

@@ -1,148 +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');
const config = require('./electron/config');
let mainWin = null;
function createWindow() {
mainWin = new BrowserWindow({
width: 1200,
height: 780,
frame: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
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:${config.development.mainPort}/`);
} else {
win.loadURL(`file://${__dirname}/dist/index.html`);
}
const image = nativeImage
.createFromPath(path.join(__dirname, 'public/icon_16x16.png'))
.resize({ width: 16, height: 16 });
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, mainWin);
}
// 限制只能启动一个应用
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 || '';
});

139
auto-imports.d.ts vendored
View File

@@ -6,70 +6,85 @@
// 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 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']
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, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, 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');
}

View 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

Binary file not shown.

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

13
build/installer.nsh Normal file
View 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

View File

@@ -1,49 +0,0 @@
{
"appId": "com.alger.music",
"productName": "AlgerMusic",
"artifactName": "${productName}_${version}_${arch}.${ext}",
"directories": {
"output": "dist_electron/mac"
},
"files": [
"dist/**/*",
"package.json",
"app.js",
"electron/**/*",
"**/*",
"public/**/*",
"node_modules/**/*"
],
"mac": {
"icon": "public/icon.icns",
"target": [
{
"target": "dmg",
"arch": ["x64", "arm64"]
}
],
"category": "public.app-category.music",
"darkModeSupport": true
},
"dmg": {
"title": "${productName} ${version}",
"icon": "public/icon.icns",
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 150,
"type": "file"
}
],
"window": {
"width": 540,
"height": 380
}
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

21
components.d.ts vendored
View File

@@ -2,19 +2,13 @@
// @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 {
Coffee: typeof import('./src/components/Coffee.vue')['default']
InstallAppModal: typeof import('./src/components/common/InstallAppModal.vue')['default']
MPop: typeof import('./src/components/common/MPop.vue')['default']
MusicList: typeof import('./src/components/MusicList.vue')['default']
MvPlayer: typeof import('./src/components/MvPlayer.vue')['default']
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
@@ -24,27 +18,16 @@ declare module 'vue' {
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']
NPagination: typeof import('naive-ui')['NPagination']
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']
NVirtualList: typeof import('naive-ui')['NVirtualList']
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
View File

@@ -0,0 +1,3 @@
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: electron-lan-file-updater

BIN
docs/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
docs/image1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
docs/image2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
docs/image3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

45
electron-builder.yml Normal file
View 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
View 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}`), '')
}
}
}
}
});

View File

@@ -1,11 +0,0 @@
module.exports = {
// 开发环境配置
development: {
mainPort: 4488,
lyricPort: 4488,
},
// 生产环境配置
production: {
distPath: '../dist',
},
};

View File

@@ -1,30 +0,0 @@
const { contextBridge, ipcRenderer } = 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),
});
// 存储相关
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
setStoreValue: (key, value) => ipcRenderer.send('setStore', key, value),
getStoreValue: (key) => ipcRenderer.sendSync('getStore', key),
on: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
},
once: (channel, func) => {
ipcRenderer.once(channel, (event, ...args) => func(...args));
},
send: (channel, data) => {
ipcRenderer.send(channel, data);
},
},
});

View File

@@ -1,7 +0,0 @@
{
"isProxy": false,
"noAnimate": false,
"animationSpeed": 1,
"author": "Alger",
"authorUrl": "https://github.com/algerkong"
}

View File

@@ -1,69 +0,0 @@
const { app, BrowserWindow } = require('electron');
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const AdmZip = require('adm-zip');
class Updater {
constructor(mainWindow) {
this.mainWindow = mainWindow;
this.updateUrl = 'http://your-server.com/update'; // 更新服务器地址
this.version = app.getVersion();
}
// 检查更新
async checkForUpdates() {
try {
const response = await axios.get(`${this.updateUrl}/check`, {
params: {
version: this.version,
},
});
if (response.data.hasUpdate) {
await this.downloadUpdate(response.data.downloadUrl);
}
} catch (error) {
console.error('检查更新失败:', error);
}
}
// 下载更新
async downloadUpdate(downloadUrl) {
try {
const response = await axios({
url: downloadUrl,
method: 'GET',
responseType: 'arraybuffer',
});
const tempPath = path.join(app.getPath('temp'), 'update.zip');
fs.writeFileSync(tempPath, response.data);
await this.extractUpdate(tempPath);
} catch (error) {
console.error('下载更新失败:', error);
}
}
// 解压更新
async extractUpdate(zipPath) {
try {
const zip = new AdmZip(zipPath);
const targetPath = path.join(__dirname, '../dist'); // 前端文件目录
// 解压文件
zip.extractAllTo(targetPath, true);
// 删除临时文件
fs.unlinkSync(zipPath);
// 刷新页面
this.mainWindow.webContents.reload();
} catch (error) {
console.error('解压更新失败:', error);
}
}
}
module.exports = Updater;

View File

@@ -1,57 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<!-- SEO 元数据 -->
<title>网抑云音乐 | AlgerKong AlgerMusicPlayer</title>
<meta name="description"
content="AlgerMusicPlayer 网抑云音乐 基于 网易云音乐API 的一款免费的在线音乐播放器,支持在线播放、歌词显示、音乐下载等功能。提供海量音乐资源,让您随时随地享受音乐。" />
<meta name="keywords" content="AlgerMusic, AlgerMusicPlayer, 网抑云, 音乐播放器, 在线音乐, 免费音乐, 歌词显示, 音乐下载, AlgerKong, 网易云音乐" />
<!-- 作者信息 -->
<meta name="author" content="AlgerKong" />
<meta name="author-url" content="https://github.com/algerkong" />
<!-- PWA 相关 -->
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#000000" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="网抑云音乐" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<!-- 资源预加载 -->
<link rel="preload" href="/icon/iconfont.css" as="style" />
<link rel="preload" href="/css/animate.css" as="style" />
<link rel="preload" href="/css/base.css" as="style" />
<!-- 样式表 -->
<link rel="stylesheet" href="/icon/iconfont.css" />
<link rel="stylesheet" href="/css/animate.css" />
<link rel="stylesheet" href="/css/base.css" />
<script defer src="https://cn.vercount.one/js"></script>
<!-- 动画配置 -->
<style>
:root {
--animate-delay: 0.5s;
}
</style>
</head>
<body>
<div id="app"></div>
<div style="display: none;">
Total Page View <span id="vercount_value_page_pv">Loading</span>
Total Visits <span id="vercount_value_site_pv">Loading</span>
Site Total Visitors <span id="vercount_value_site_uv">Loading</span>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,32 +1,46 @@
{
"name": "alger-music",
"version": "2.4.0",
"description": "这是一个用于音乐播放的应用程序。",
"name": "AlgerMusicPlayer",
"version": "3.4.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": "cross-env NODE_ENV=production vite build",
"serve": "vite preview",
"start": "cross-env NODE_ENV=development electron .",
"lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0",
"b:win:x64": "cross-env NODE_ENV=production electron-builder --config ./build/win64.json",
"b:win:x86": "cross-env NODE_ENV=production electron-builder --config ./build/win32.json",
"b:win:arm": "cross-env NODE_ENV=production electron-builder --config ./build/winarm64.json",
"b:mac": "cross-env NODE_ENV=production npm run build && electron-builder --config ./build/mac.json",
"b:win": "cross-env NODE_ENV=production npm run build && npm run b:win:x64 && npm run b:win:x86 && npm run b:win:arm"
"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": {
"@types/howler": "^2.2.12",
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@unblockneteasemusic/server": "^0.27.8-patch.1",
"electron-store": "^8.1.0",
"howler": "^2.2.4"
"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": "^5.1.3",
"@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",
@@ -34,33 +48,115 @@
"autoprefixer": "^10.4.20",
"axios": "^1.7.7",
"cross-env": "^7.0.3",
"electron": "^32.2.7",
"electron-builder": "^25.0.5",
"eslint": "^8.56.0",
"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",
"marked": "^15.0.4",
"naive-ui": "^2.39.0",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"prettier": "^3.3.2",
"remixicon": "^4.2.0",
"sass": "^1.82.0",
"tailwindcss": "^3.4.15",
"typescript": "^5.5.4",
"typescript": "^5.5.2",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.4",
"vfonts": "^0.1.0",
"vite": "^5.4.3",
"vite": "^5.3.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "7.4.0",
"vue": "^3.5.0",
"vue": "^3.4.30",
"vue-router": "^4.4.3",
"vue-tsc": "^2.1.4",
"vuex": "^4.1.0"
"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"
}
}
}

View File

@@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
autoprefixer: {}
}
};

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
body{
background-color: #000;
}
.n-popover:has(.music-play){
border-radius: 1.5rem !important;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

View File

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 626 B

View File

@@ -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 } });
};

View File

@@ -1,73 +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';
import { audioService } from '@/services/audioService';
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);
audioService.getCurrentSound()?.pause();
}
},
);
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>

View File

@@ -1,251 +0,0 @@
<template>
<div class="song-item" :class="{ 'song-mini': mini, 'song-list': list }">
<n-image
v-if="item.picUrl"
ref="songImg"
:src="getImgUrl(item.picUrl, '40y40')"
class="song-item-img"
preview-disabled
:img-props="{
crossorigin: 'anonymous',
}"
@load="imageLoad"
/>
<div class="song-item-content">
<div v-if="list" class="song-item-content-wrapper">
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
<div class="song-item-content-divider">-</div>
<n-ellipsis class="song-item-content-name text-ellipsis" line-clamp="1">
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
>
</n-ellipsis>
</div>
<template v-else>
<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.ar || item.song.artists" :key="artistsindex"
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
>
</n-ellipsis>
</div>
</template>
</div>
<div class="song-item-operating" :class="{ 'song-item-operating-list': list }">
<div v-if="favorite" class="song-item-operating-like">
<i class="iconfont icon-likefill" :class="{ 'like-active': isFavorite }" @click.stop="toggleFavorite"></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 { computed, useTemplateRef } from 'vue';
import { useStore } from 'vuex';
import { audioService } from '@/services/audioService';
import type { SongResult } from '@/type/music';
import { getImgUrl } from '@/utils';
import { getImageBackground } from '@/utils/linearColor';
const props = withDefaults(
defineProps<{
item: SongResult;
mini?: boolean;
list?: boolean;
favorite?: boolean;
}>(),
{
mini: false,
list: false,
favorite: true,
},
);
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 songImageRef = useTemplateRef('songImg');
const imageLoad = async () => {
if (!songImageRef.value) {
return;
}
const { backgroundColor } = await getImageBackground(
(songImageRef.value as any).imageRef as unknown as HTMLImageElement,
);
// eslint-disable-next-line vue/no-mutating-props
props.item.backgroundColor = backgroundColor;
};
// 播放音乐 设置音乐详情 打开音乐底栏
const playMusicEvent = async (item: SongResult) => {
if (playMusic.value.id === item.id) {
if (play.value) {
store.commit('setPlayMusic', false);
audioService.getCurrentSound()?.pause();
} else {
store.commit('setPlayMusic', true);
audioService.getCurrentSound()?.play();
}
return;
}
await store.commit('setPlay', item);
store.commit('setIsPlay', true);
emits('play', item);
};
// 判断是否已收藏
const isFavorite = computed(() => {
return store.state.favoriteList.includes(props.item.id);
});
// 切换收藏状态
const toggleFavorite = async (e: Event) => {
e.stopPropagation();
if (isFavorite.value) {
store.commit('removeFromFavorite', props.item.id);
} else {
store.commit('addToFavorite', props.item.id);
}
};
</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 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 ml-4;
}
.like-active {
@apply text-red-600;
}
&-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 ml-1;
}
&-play {
@apply w-8 h-8;
}
}
}
}
.song-list {
@apply p-2 rounded-lg hover:bg-gray-800/50 border border-gray-800/50 mb-2;
.song-item-img {
@apply w-10 h-10 rounded-lg mr-3;
}
.song-item-content {
@apply flex items-center flex-1;
&-wrapper {
@apply flex items-center flex-1 text-sm;
}
&-title {
@apply text-white flex-shrink-0 max-w-[45%];
}
&-divider {
@apply mx-2 text-gray-400;
}
&-name {
@apply text-gray-400 flex-1 min-w-0;
}
}
.song-item-operating {
@apply flex items-center gap-2;
&-like {
@apply cursor-pointer hover:scale-110 transition-transform;
.iconfont {
@apply text-base text-gray-400 hover:text-red-500;
}
}
&-play {
@apply w-7 h-7 cursor-pointer hover:scale-110 transition-transform;
.iconfont {
@apply text-base;
}
}
}
}
</style>

10
src/electron.d.ts vendored
View File

@@ -1,10 +0,0 @@
declare global {
interface Window {
electronAPI: {
minimize: () => void;
maximize: () => void;
close: () => void;
dragStart: () => void;
};
}
}

View File

@@ -1,19 +0,0 @@
/* ./src/index.css */
/*! @import */
@tailwind base;
@tailwind components;
@tailwind utilities;
.n-image img {
background-color: #111111;
width: 100%;
}
.n-slider-handle-indicator--top {
@apply bg-transparent text-[#ffffffdd] text-2xl px-2 py-1 shadow-none mb-0 !important;
}
.text-el {
@apply overflow-ellipsis overflow-hidden whitespace-nowrap;
}

View File

@@ -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>

View File

@@ -1,71 +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';
import { isElectron } from '@/hooks/MusicHook';
const dialog = useDialog();
const windowData = window as any;
const minimize = () => {
if (!isElectron.value) {
return;
}
windowData.electronAPI.minimize();
};
const close = () => {
if (!isElectron.value) {
return;
}
dialog.warning({
title: '提示',
content: '确定要退出吗?',
positiveText: '最小化',
negativeText: '关闭',
onPositiveClick: () => {
windowData.electronAPI.miniTray();
},
onNegativeClick: () => {
windowData.electronAPI.close();
},
});
};
const drag = (event: MouseEvent) => {
if (!isElectron.value) {
return;
}
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>

92
src/main/index.ts Normal file
View File

@@ -0,0 +1,92 @@
import { electronApp, optimizer } from '@electron-toolkit/utils';
import { app, globalShortcut, ipcMain, nativeImage } from 'electron';
import { join } from 'path';
import { loadLyricWindow } from './lyric';
import { initializeConfig } from './modules/config';
import { initializeFileManager } from './modules/fileManager';
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();
initializeFileManager();
// 创建主窗口
mainWindow = createMainWindow(icon);
// 初始化窗口管理
initializeWindowManager();
// 初始化托盘
initializeTray(iconPath, mainWindow);
// 启动音乐API
startMusicApi();
// 加载歌词窗口
loadLyricWindow(ipcMain, mainWindow);
}
// 应用程序准备就绪时的处理
app.whenReady().then(() => {
// 设置应用ID
electronApp.setAppUserModelId('com.alger.music');
// 监听窗口创建事件
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window);
});
// 初始化应用
initialize();
// macOS 激活应用时的处理
app.on('activate', function () {
if (mainWindow === null) initialize();
});
});
// 应用程序准备就绪后的快捷键设置
app.on('ready', () => {
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
});
// 所有窗口关闭时的处理
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;
});

View File

@@ -1,23 +1,29 @@
const { BrowserWindow, screen } = require('electron');
const path = require('path');
const Store = require('electron-store');
const config = require('./config');
import { BrowserWindow, IpcMain, screen } from 'electron';
import Store from 'electron-store';
import path, { join } from 'path';
const store = new Store();
let lyricWindow = null;
let lyricWindow: BrowserWindow | null = null;
const createWin = () => {
console.log('Creating lyric window');
// 获取保存的窗口位置
const windowBounds = store.get('lyricWindowBounds') || {};
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;
const validPosition =
x !== undefined && y !== undefined && x >= 0 && y >= 0 && x < screenWidth && y < screenHeight;
lyricWindow = new BrowserWindow({
width: width || 800,
@@ -30,63 +36,79 @@ const createWin = () => {
hasShadow: false,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: `${__dirname}/preload.js`,
webSecurity: false,
},
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
contextIsolation: true
}
});
// 监听窗口关闭事件
lyricWindow.on('closed', () => {
console.log('Lyric window closed');
lyricWindow = null;
if (lyricWindow) {
lyricWindow.destroy();
lyricWindow = null;
}
});
return lyricWindow;
};
const loadLyricWindow = (ipcMain, mainWin) => {
ipcMain.on('open-lyric', () => {
console.log('Received open-lyric request');
if (lyricWindow) {
console.log('Lyric window exists, focusing');
if (lyricWindow.isMinimized()) lyricWindow.restore();
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');
createWin();
if (process.env.NODE_ENV === 'development') {
lyricWindow.webContents.openDevTools({ mode: 'detach' });
lyricWindow.loadURL(`http://localhost:${config.development.lyricPort}/#/lyric`);
} else {
const distPath = path.resolve(__dirname, config.production.distPath);
lyricWindow.loadURL(`file://${distPath}/index.html#/lyric`);
const win = createWin();
if (!win) {
console.error('Failed to create lyric window');
return;
}
lyricWindow.setMinimumSize(600, 200);
lyricWindow.setSkipTaskbar(true);
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`);
}
lyricWindow.once('ready-to-show', () => {
win.setMinimumSize(600, 200);
win.setSkipTaskbar(true);
win.once('ready-to-show', () => {
console.log('Lyric window ready to show');
lyricWindow.show();
win.show();
});
});
ipcMain.on('send-lyric', (e, data) => {
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);
}
} else {
console.log('Cannot send lyric: window not available or destroyed');
}
});
ipcMain.on('top-lyric', (e, data) => {
ipcMain.on('top-lyric', (_, data) => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setAlwaysOnTop(data);
}
@@ -96,22 +118,27 @@ const loadLyricWindow = (ipcMain, mainWin) => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.webContents.send('lyric-window-close');
mainWin.webContents.send('lyric-control-back', 'close');
lyricWindow.close();
lyricWindow.destroy();
lyricWindow = null;
}
});
// 处理鼠标事件
ipcMain.on('mouseenter-lyric', () => {
lyricWindow.setIgnoreMouseEvents(true);
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setIgnoreMouseEvents(true);
}
});
ipcMain.on('mouseleave-lyric', () => {
lyricWindow.setIgnoreMouseEvents(false);
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setIgnoreMouseEvents(false);
}
});
// 处理拖动移动
ipcMain.on('lyric-drag-move', (e, { deltaX, deltaY }) => {
if (!lyricWindow) return;
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;
@@ -127,30 +154,23 @@ const loadLyricWindow = (ipcMain, mainWin) => {
store.set('lyricWindowBounds', {
...lyricWindow.getBounds(),
x: newX,
y: newY,
y: newY
});
});
// 添加鼠标穿透事件处理
ipcMain.on('set-ignore-mouse', (e, shouldIgnore) => {
if (!lyricWindow) return;
ipcMain.on('set-ignore-mouse', (_, shouldIgnore) => {
if (!lyricWindow || lyricWindow.isDestroyed()) return;
if (shouldIgnore) {
// 设置鼠标穿透,但保留拖动区域可交互
lyricWindow.setIgnoreMouseEvents(true, { forward: true });
} else {
// 取消鼠标穿透
lyricWindow.setIgnoreMouseEvents(false);
}
lyricWindow.setIgnoreMouseEvents(shouldIgnore, { forward: true });
});
// 添加播放控制处理
ipcMain.on('control-back', (e, command) => {
console.log('Received control-back request:', command);
mainWin.webContents.send('lyric-control-back', command);
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);
}
});
};
module.exports = {
loadLyricWindow,
};

View File

@@ -0,0 +1,43 @@
import { app, ipcMain } from 'electron';
import Store from 'electron-store';
import set from '../set.json';
interface StoreType {
set: {
isProxy: boolean;
noAnimate: boolean;
animationSpeed: number;
author: string;
authorUrl: string;
musicApiPort: number;
};
}
let store: Store<StoreType>;
/**
* 初始化配置管理
*/
export function initializeConfig() {
store = new Store<StoreType>({
name: 'config',
defaults: {
set
}
});
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;
}

View File

@@ -0,0 +1,72 @@
import axios from 'axios';
import { app, dialog, ipcMain, shell } from 'electron';
import Store from 'electron-store';
import * as fs from 'fs';
import * as path from 'path';
/**
* 初始化文件管理相关的IPC监听
*/
export function initializeFileManager() {
// 通用的选择目录处理
ipcMain.handle('select-directory', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory'],
title: '选择目录'
});
return result;
});
// 通用的打开目录处理
ipcMain.on('open-directory', (_, path) => {
shell.openPath(path);
});
// 下载音乐处理
ipcMain.on('download-music', downloadMusic);
}
/**
* 下载音乐功能
*/
async function downloadMusic(
event: Electron.IpcMainEvent,
{ url, filename }: { url: string; filename: string }
) {
try {
const store = new Store();
const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');
// 直接使用配置的下载路径
const filePath = path.join(downloadPath, `${filename}.mp3`);
// 检查文件是否已存在,如果存在则添加序号
let finalFilePath = filePath;
let counter = 1;
while (fs.existsSync(finalFilePath)) {
const ext = path.extname(filePath);
const nameWithoutExt = filePath.slice(0, -ext.length);
finalFilePath = `${nameWithoutExt} (${counter})${ext}`;
counter++;
}
const response = await axios({
url,
method: 'GET',
responseType: 'stream'
});
const writer = fs.createWriteStream(finalFilePath);
response.data.pipe(writer);
writer.on('finish', () => {
event.reply('music-download-complete', { success: true, path: finalFilePath });
});
writer.on('error', (err) => {
event.reply('music-download-complete', { success: false, error: err.message });
});
} catch (error: any) {
event.reply('music-download-complete', { success: false, error: error.message });
}
}

45
src/main/modules/tray.ts Normal file
View 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
View 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
View 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
View 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"
}

23
src/main/unblockMusic.ts Normal file
View File

@@ -0,0 +1,23 @@
import match from '@unblockneteasemusic/server';
const unblockMusic = async (id: any, songData: any): Promise<any> => {
return new Promise((resolve, reject) => {
match(parseInt(id, 10), ['qq', 'migu', 'kugou', 'joox'], songData)
.then((data) => {
resolve({
data: {
data,
params: {
id,
type: 'song'
}
}
});
})
.catch((err) => {
reject(err);
});
});
};
export { unblockMusic };

19
src/preload/index.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
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>;
};
$message: any;
}
}

32
src/preload/index.ts Normal file
View File

@@ -0,0 +1,32 @@
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)
};
// 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;
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="app-container" :class="{ mobile: isMobile, noElectron: !isElectron }">
<n-config-provider :theme="darkTheme">
<n-config-provider :theme="theme === 'dark' ? darkTheme : lightTheme">
<n-dialog-provider>
<n-message-provider>
<router-view></router-view>
@@ -11,21 +11,26 @@
</template>
<script setup lang="ts">
import { darkTheme } from 'naive-ui';
import { darkTheme, lightTheme } from 'naive-ui';
import { onMounted } from 'vue';
import { isElectron } from '@/hooks/MusicHook';
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),
homeRouter.filter((item) => item.meta.isMobile)
);
}
});

View File

@@ -22,7 +22,7 @@ export function getListByTag(params: IListByTagParams) {
// 根据cat 获取歌单列表
export function getListByCat(params: IListByCatParams) {
return request.get('/top/playlist', {
params,
params
});
}

View File

@@ -41,6 +41,6 @@ export function logout() {
export function loginByCellphone(phone: string, password: string) {
return request.post('/login/cellphone', {
phone,
password,
password
});
}

58
src/renderer/api/music.ts Normal file
View File

@@ -0,0 +1,58 @@
import store from '@/store';
import { 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: [{ url: res.data.data.url }] } };
}
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 = (id: number) => {
return request.get<ILyric>('/lyric', { params: { id } });
};
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');
};

View File

@@ -1,5 +1,5 @@
import { IData } from '@/type';
import { IMvItem, IMvUrlData } from '@/type/mv';
import { IMvUrlData } from '@/type/mv';
import request from '@/utils/request';
interface MvParams {
@@ -13,7 +13,7 @@ export const getTopMv = (params: MvParams) => {
return request({
url: '/mv/all',
method: 'get',
params,
params
});
};
@@ -22,7 +22,7 @@ export const getAllMv = (params: MvParams) => {
return request({
url: '/mv/all',
method: 'get',
params,
params
});
};
@@ -30,8 +30,8 @@ export const getAllMv = (params: MvParams) => {
export const getMvDetail = (mvid: string) => {
return request.get('/mv/detail', {
params: {
mvid,
},
mvid
}
});
};
@@ -39,7 +39,7 @@ export const getMvDetail = (mvid: string) => {
export const getMvUrl = (id: Number) => {
return request.get<IData<IMvUrlData>>('/mv/url', {
params: {
id,
},
id
}
});
};

View File

@@ -7,6 +7,6 @@ interface IParams {
// 搜索内容
export const getSearch = (params: IParams) => {
return request.get<any>('/cloudsearch', {
params,
params
});
};

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

3687
src/renderer/assets/css/animate.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
body {
/* background-color: #000; */
}
.n-popover:has(.music-play) {
border-radius: 1.5rem !important;
}

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -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';
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

75
src/renderer/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,75 @@
/* eslint-disable */
/* prettier-ignore */
// @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 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, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

41
src/renderer/components.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
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']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
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']
NSelect: typeof import('naive-ui')['NSelect']
NSlider: typeof import('naive-ui')['NSlider']
NSpin: typeof import('naive-ui')['NSpin']
NSwitch: typeof import('naive-ui')['NSwitch']
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
NVirtualList: typeof import('naive-ui')['NVirtualList']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

View File

@@ -12,20 +12,33 @@
</slot>
</template>
<div class="p-6 bg-black rounded-lg shadow-lg">
<div class="p-6 rounded-lg shadow-lg bg-light dark:bg-gray-800">
<div class="flex gap-10">
<div class="flex flex-col items-center gap-2">
<n-image :src="alipayQR" alt="支付宝收款码" class="w-32 h-32 rounded-lg cursor-none" preview-disabled />
<span class="text-sm text-gray-100">支付宝</span>
<n-image
:src="alipayQR"
alt="支付宝收款码"
class="w-32 h-32 rounded-lg cursor-none"
preview-disabled
/>
<span class="text-sm text-gray-700 dark:text-gray-200">支付宝</span>
</div>
<div class="flex flex-col items-center gap-2">
<n-image :src="wechatQR" alt="微信收款码" class="w-32 h-32 rounded-lg cursor-none" preview-disabled />
<span class="text-sm text-gray-100">微信支付</span>
<n-image
:src="wechatQR"
alt="微信收款码"
class="w-32 h-32 rounded-lg cursor-none"
preview-disabled
/>
<span class="text-sm text-gray-700 dark:text-gray-200">微信支付</span>
</div>
</div>
<div class="mt-4">
<p class="text-sm text-gray-100 text-center cursor-pointer hover:text-green-500" @click="copyQQ">
<p
class="text-sm text-gray-700 dark:text-gray-200 text-center cursor-pointer hover:text-green-500"
@click="copyQQ"
>
QQ群789288579
</p>
</div>
@@ -37,6 +50,9 @@
<script setup>
import { NButton, NImage, NPopover } from 'naive-ui';
import alipay from '@/assets/alipay.png';
import wechat from '@/assets/wechat.png';
const message = useMessage();
const copyQQ = () => {
navigator.clipboard.writeText('789288579');
@@ -46,11 +62,11 @@ const copyQQ = () => {
defineProps({
alipayQR: {
type: String,
required: true,
default: alipay
},
wechatQR: {
type: String,
required: true,
},
default: wechat
}
});
</script>

View File

@@ -25,24 +25,24 @@
<div class="music-info">
<div class="music-cover">
<n-image
:src="getImgUrl(cover ? listInfo?.coverImgUrl : displayedSongs[0]?.picUrl, '300y300')"
:src="getImgUrl(cover ? listInfo?.coverImgUrl : displayedSongs[0]?.picUrl, '500y500')"
class="cover-img"
preview-disabled
:class="setAnimationClass('animate__fadeIn')"
object-fit="cover"
/>
</div>
<div class="music-detail">
<div v-if="listInfo?.creator" class="creator-info">
<n-avatar round :size="24" :src="getImgUrl(listInfo.creator.avatarUrl, '50y50')" />
<span class="creator-name">{{ listInfo.creator.nickname }}</span>
</div>
<div v-if="listInfo?.description" class="music-desc">
<n-ellipsis :line-clamp="isMobile ? 3 : 10">
{{ listInfo.description }}
</n-ellipsis>
</div>
<div v-if="listInfo?.creator" class="creator-info">
<n-avatar round :size="24" :src="getImgUrl(listInfo.creator.avatarUrl, '50y50')" />
<span class="creator-name">{{ listInfo.creator.nickname }}</span>
</div>
<n-scrollbar style="max-height: 200">
<div v-if="listInfo?.description" class="music-desc">
{{ listInfo.description }}
</div>
<play-bottom />
</n-scrollbar>
</div>
<!-- 右侧歌曲列表 -->
@@ -98,8 +98,8 @@ const props = withDefaults(
}>(),
{
loading: false,
cover: true,
},
cover: true
}
);
const emit = defineEmits(['update:show', 'update:loading']);
@@ -122,7 +122,7 @@ const formatDetail = computed(() => (detail: any) => {
const song = {
artists: detail.ar,
name: detail.al.name,
id: detail.al.id,
id: detail.al.id
};
detail.song = song;
@@ -138,9 +138,9 @@ const handlePlay = () => {
...item,
picUrl: item.al.picUrl,
song: {
artists: item.ar,
},
})),
artists: item.ar
}
}))
);
};
@@ -204,7 +204,7 @@ watch(
if (!props.cover) {
loadingList.value = false;
}
},
}
);
// songList
@@ -218,23 +218,23 @@ watch(
}
loadingList.value = false;
},
{ immediate: true },
{ immediate: true }
);
</script>
<style scoped lang="scss">
.music {
&-title {
@apply text-xl font-bold text-white;
@apply text-xl font-bold text-gray-900 dark:text-white;
}
&-page {
@apply px-8 w-full h-full bg-black bg-opacity-75 rounded-t-2xl;
@apply px-8 w-full h-full bg-light dark:bg-black bg-opacity-75 dark:bg-opacity-75 rounded-t-2xl;
backdrop-filter: blur(20px);
}
&-close {
@apply cursor-pointer text-white flex gap-2 items-center;
@apply cursor-pointer text-gray-900 dark:text-white flex gap-2 items-center;
.icon {
@apply text-3xl;
}
@@ -248,34 +248,29 @@ watch(
@apply w-[25%] flex-shrink-0 pr-8 flex flex-col;
.music-cover {
@apply w-full aspect-square rounded-lg overflow-hidden mb-4;
@apply w-full aspect-square rounded-2xl overflow-hidden mb-4 min-h-[250px];
.cover-img {
@apply w-full h-full object-cover;
}
}
.music-detail {
@apply flex flex-col flex-grow;
.creator-info {
@apply flex items-center mb-4;
.creator-name {
@apply ml-2 text-sm text-gray-300;
}
}
.music-desc {
@apply text-sm text-gray-400;
.creator-info {
@apply flex items-center mb-4;
.creator-name {
@apply ml-2 text-gray-700 dark:text-gray-300;
}
}
}
&-list-container {
@apply flex-grow min-h-0 flex flex-col relative;
.music-desc {
@apply text-sm text-gray-600 dark:text-gray-400 leading-relaxed pr-4;
}
}
&-list {
@apply flex-grow min-h-0;
&-container {
@apply flex-grow min-h-0 flex flex-col relative;
}
&-content {
@apply min-h-[calc(80vh-60px)];
@@ -312,16 +307,19 @@ watch(
}
.loading-more {
@apply text-center text-white py-10;
@apply text-center py-4 text-gray-500 dark:text-gray-400;
}
.double-list {
.double-item {
width: 100%;
}
.double-item {
@apply mb-2 bg-light-100 bg-opacity-20 dark:bg-dark-100 dark:bg-opacity-20 rounded-3xl;
}
.song-item {
background-color: #191919;
.mobile {
.music-info {
@apply hidden;
}
.music-list-content {
@apply pb-[100px];
}
}
</style>

View File

@@ -1,7 +1,11 @@
<template>
<n-drawer :show="show" height="100%" placement="bottom" :z-index="999999999" :to="`#layout-main`">
<div class="mv-detail">
<div ref="videoContainerRef" class="video-container" :class="{ 'cursor-hidden': !showCursor }">
<div
ref="videoContainerRef"
class="video-container"
:class="{ 'cursor-hidden': !showCursor }"
>
<video
ref="videoRef"
:src="mvUrl"
@@ -34,11 +38,6 @@
:step="0.1"
@update:value="handleProgressChange"
>
<template #rail>
<div class="progress-rail">
<div class="progress-buffer" :style="{ width: `${bufferedProgress}%` }"></div>
</div>
</template>
</n-slider>
</div>
@@ -86,7 +85,9 @@
下一个
</n-tooltip>
<div class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</div>
<div class="time-display">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</div>
</div>
<div class="right-controls">
@@ -96,14 +97,22 @@
<n-button quaternary circle @click="toggleMute">
<template #icon>
<n-icon size="24">
<i :class="volume === 0 ? 'ri-volume-mute-line' : 'ri-volume-up-line'"></i>
<i
:class="volume === 0 ? 'ri-volume-mute-line' : 'ri-volume-up-line'"
></i>
</n-icon>
</template>
</n-button>
</template>
{{ volume === 0 ? '取消静音' : '静音' }}
</n-tooltip>
<n-slider v-model:value="volume" :min="0" :max="100" :tooltip="false" class="volume-slider" />
<n-slider
v-model:value="volume"
:min="0"
:max="100"
:tooltip="false"
class="volume-slider"
/>
</div>
<n-tooltip v-if="!props.noList" placement="top">
@@ -111,7 +120,11 @@
<n-button quaternary circle class="play-mode-btn" @click="togglePlayMode">
<template #icon>
<n-icon size="24">
<i :class="playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'"></i>
<i
:class="
playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'
"
></i>
</n-icon>
</template>
</n-button>
@@ -124,7 +137,9 @@
<n-button quaternary circle @click="toggleFullscreen">
<template #icon>
<n-icon size="24">
<i :class="isFullscreen ? 'ri-fullscreen-exit-line' : 'ri-fullscreen-line'"></i>
<i
:class="isFullscreen ? 'ri-fullscreen-exit-line' : 'ri-fullscreen-line'"
></i>
</n-icon>
</template>
</n-button>
@@ -181,7 +196,7 @@ import { IMvItem } from '@/type/mv';
type PlayMode = 'single' | 'auto';
const PLAY_MODE = {
Single: 'single' as PlayMode,
Auto: 'auto' as PlayMode,
Auto: 'auto' as PlayMode
} as const;
const props = withDefaults(
@@ -193,8 +208,8 @@ const props = withDefaults(
{
show: false,
currentMv: undefined,
noList: false,
},
noList: false
}
);
const emit = defineEmits<{
@@ -310,7 +325,7 @@ watch(
if (newMv) {
await loadMvUrl(newMv);
}
},
}
);
const autoPlayBlocked = ref(false);
@@ -383,11 +398,21 @@ const checkFullscreenAPI = () => {
(videoContainerRef.value as any)?.webkitRequestFullscreen ||
(videoContainerRef.value as any)?.mozRequestFullScreen ||
(videoContainerRef.value as any)?.msRequestFullscreen,
exitFullscreen: doc.exitFullscreen || doc.webkitExitFullscreen || doc.mozCancelFullScreen || doc.msExitFullscreen,
exitFullscreen:
doc.exitFullscreen ||
doc.webkitExitFullscreen ||
doc.mozCancelFullScreen ||
doc.msExitFullscreen,
fullscreenElement:
doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement,
doc.fullscreenElement ||
doc.webkitFullscreenElement ||
doc.mozFullScreenElement ||
doc.msFullscreenElement,
fullscreenEnabled:
doc.fullscreenEnabled || doc.webkitFullscreenEnabled || doc.mozFullScreenEnabled || doc.msFullscreenEnabled,
doc.fullscreenEnabled ||
doc.webkitFullscreenEnabled ||
doc.mozFullScreenEnabled ||
doc.msFullscreenEnabled
};
};
@@ -549,226 +574,57 @@ const isMobile = computed(() => store.state.isMobile);
<style scoped lang="scss">
.mv-detail {
@apply w-full h-full bg-black relative;
@apply h-full bg-light dark:bg-black;
//
@media screen and (orientation: landscape) {
height: 100% !important;
width: 100% !important;
&-title {
@apply fixed top-0 left-0 right-0 p-4 z-10 transition-opacity duration-300;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), transparent);
.title {
@apply text-white text-lg font-bold;
}
}
}
.video-container {
@apply h-full w-full relative;
.video-player {
@apply h-full w-full object-contain bg-black;
}
.video-container {
@apply w-full h-full relative;
transition: cursor 0.3s ease;
//
@media (max-width: 768px) {
.custom-controls {
.controls-main {
@apply flex-wrap gap-2 justify-center;
.left-controls,
.right-controls {
@apply w-full justify-center;
}
.time-display {
@apply order-first w-full text-center mb-2;
}
}
}
//
.mv-detail-title {
.title {
@apply text-base max-w-full;
}
}
//
.progress-bar {
@apply mb-2;
}
.play-hint {
@apply absolute inset-0 flex items-center justify-center bg-black bg-opacity-50;
.n-button {
@apply text-white hover:text-green-500 transition-colors;
}
}
&.cursor-hidden {
* {
cursor: none !important;
}
.custom-controls {
@apply absolute bottom-0 left-0 right-0 p-4 transition-opacity duration-300;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
//
.custom-controls {
* {
cursor: default !important;
}
.controls-main {
@apply flex justify-between items-center;
.left-controls,
.right-controls {
@apply flex items-center gap-2;
.n-button {
cursor: pointer !important;
}
.n-slider {
cursor: pointer !important;
}
}
}
&:fullscreen,
&:-webkit-full-screen,
&:-moz-full-screen,
&:-ms-fullscreen {
background: black;
width: 100%;
height: 100%;
//
.mv-detail-title {
@apply px-8 py-6;
.title {
@apply text-xl;
}
}
//
.custom-controls {
padding: 20px 24px;
}
}
&::after {
content: '';
@apply absolute inset-0 bg-black opacity-0 transition-opacity duration-200;
pointer-events: none;
}
&:active::after {
@apply opacity-10;
}
video {
@apply w-full h-full;
}
.custom-controls {
@apply absolute bottom-0 left-0 w-full transition-opacity duration-300 ease-in-out;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);
padding: 16px 20px;
&.controls-hidden {
opacity: 0;
pointer-events: none;
}
.progress-bar {
@apply mb-4;
.progress-rail {
@apply relative w-full h-full;
.progress-buffer {
@apply absolute h-full bg-gray-600 rounded-full;
transition: width 0.2s ease;
}
}
}
.controls-main {
@apply flex justify-between items-center;
.left-controls,
.right-controls {
@apply flex items-center gap-4;
@apply text-white hover:text-green-500 transition-colors;
}
.time-display {
@apply text-sm text-white ml-2;
}
.volume-control {
@apply flex items-center gap-2;
.volume-slider {
width: 80px;
}
}
.n-button {
@apply text-white;
&:hover {
@apply text-green-400;
}
}
}
}
.play-hint {
@apply absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 cursor-pointer;
z-index: 10;
.n-button {
@apply text-white opacity-80 transform transition-all duration-300;
&:hover {
@apply opacity-100 scale-110;
@apply text-white text-sm ml-4;
}
}
}
}
.mv-detail-title {
@apply absolute w-full left-0 top-0 px-6 py-4 transition-opacity duration-300 z-50;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);
&.title-hidden {
opacity: 0;
}
.title {
@apply text-white text-lg font-medium;
max-width: 80%;
}
}
}
.custom-slider {
:deep(.n-slider) {
--n-rail-height: 4px;
--n-rail-color: rgba(255, 255, 255, 0.2);
--n-fill-color: var(--primary-color);
--n-handle-size: 12px;
--n-handle-color: var(--primary-color);
&:hover {
--n-rail-height: 6px;
--n-handle-size: 14px;
}
.n-slider-rail {
@apply overflow-hidden;
}
.n-slider-handle {
@apply transition-opacity duration-200;
opacity: 0;
}
&:hover .n-slider-handle {
opacity: 1;
}
}
}
:root {
--primary-color: #18a058;
}
//
.mode-hint {
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2;
@apply flex flex-col items-center justify-center;
@apply bg-black bg-opacity-70 rounded-lg p-4;
z-index: 20;
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center;
.mode-icon {
@apply text-white mb-2;
@@ -779,7 +635,83 @@ const isMobile = computed(() => store.state.isMobile);
}
}
//
.custom-slider {
:deep(.n-slider) {
--n-rail-height: 4px;
--n-rail-color: theme('colors.gray.200');
--n-rail-color-dark: theme('colors.gray.700');
--n-fill-color: theme('colors.green.500');
--n-handle-size: 12px;
--n-handle-color: theme('colors.green.500');
&.n-slider--vertical {
height: 100%;
.n-slider-rail {
width: 4px;
}
&:hover {
.n-slider-rail {
width: 6px;
}
.n-slider-handle {
width: 14px;
height: 14px;
}
}
}
.n-slider-rail {
@apply overflow-hidden transition-all duration-200;
@apply bg-gray-500 dark:bg-dark-300 bg-opacity-10 !important;
}
.n-slider-handle {
@apply transition-all duration-200;
opacity: 0;
}
&:hover .n-slider-handle {
opacity: 1;
}
}
}
.progress-bar {
@apply mb-4;
.progress-rail {
@apply relative w-full h-1 bg-gray-600;
.progress-buffer {
@apply absolute top-0 left-0 h-full bg-gray-400;
}
}
}
.volume-control {
@apply flex items-center gap-2;
.volume-slider {
width: 100px;
}
}
.controls-hidden {
opacity: 0;
pointer-events: none;
}
.cursor-hidden {
cursor: none;
}
.title-hidden {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
@@ -789,79 +721,4 @@ const isMobile = computed(() => store.state.isMobile);
.fade-leave-to {
opacity: 0;
}
// tooltip
:deep(.n-tooltip) {
padding: 4px 8px;
font-size: 12px;
}
//
.left-controls {
@apply flex items-center gap-2;
.time-display {
@apply text-sm text-white ml-4; //
}
}
//
:deep(.n-button--disabled) {
opacity: 0.5;
cursor: not-allowed;
}
//
:deep(.n-spin) {
.n-spin-body {
@apply text-white;
width: 20px;
height: 20px;
}
}
//
.video-player {
@apply w-full h-full cursor-pointer;
}
//
.video-container {
&::after {
content: '';
@apply absolute inset-0 bg-black opacity-0 transition-opacity duration-200;
pointer-events: none;
}
&:active::after {
@apply opacity-10;
}
}
//
.video-container {
@apply w-full h-full relative;
transition: cursor 0.3s ease;
&.cursor-hidden {
* {
cursor: none !important;
}
//
.custom-controls {
* {
cursor: default !important;
}
.n-button {
cursor: pointer !important;
}
.n-slider {
cursor: pointer !important;
}
}
}
}
</style>

View File

@@ -13,7 +13,7 @@
? 'animate__bounceIn'
: !isShowAllPlaylistCategory
? 'animate__backOutLeft'
: 'animate__bounceIn',
: 'animate__bounceIn'
) +
' ' +
'type-item-' +
@@ -27,7 +27,11 @@
<div
class="play-list-type-showall"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
:style="
setAnimationDelay(
!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30
)
"
@click="handleToggleShowAllPlaylistCategory"
>
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
@@ -63,8 +67,8 @@ const getAnimationDelay = computed(() => {
watch(isShowAllPlaylistCategory, (newVal) => {
if (!newVal) {
const elements = playlistCategory.value?.sub.map((item, index) =>
document.querySelector(`.type-item-${index}`),
const elements = playlistCategory.value?.sub.map((_, index) =>
document.querySelector(`.type-item-${index}`)
) as HTMLElement[];
elements
.slice(20)
@@ -75,7 +79,7 @@ watch(isShowAllPlaylistCategory, (newVal) => {
() => {
(element as HTMLElement).style.position = 'absolute';
},
index * DELAY_TIME + 400,
index * DELAY_TIME + 400
);
}
});
@@ -90,7 +94,7 @@ watch(isShowAllPlaylistCategory, (newVal) => {
}
});
},
(playlistCategory.value?.sub.length || 0 - 19) * DELAY_TIME,
(playlistCategory.value?.sub.length || 0 - 19) * DELAY_TIME
);
} else {
document.querySelectorAll('.play-list-type-item').forEach((element) => {
@@ -112,8 +116,8 @@ const handleClickPlaylistType = (type: string) => {
router.push({
path: '/list',
query: {
type,
},
type
}
});
};
@@ -132,15 +136,15 @@ onMounted(() => {
<style lang="scss" scoped>
.title {
@apply text-lg font-bold mb-4;
@apply text-lg font-bold mb-4 text-gray-900 dark:text-white;
}
.play-list-type {
width: 250px;
@apply mx-6;
@apply mr-4;
&-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;
@apply bg-light dark:bg-black text-gray-900 dark:text-white;
@apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-200 dark:border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 hover:text-white transition;
}
&-showall {
@apply block text-center;

Some files were not shown because too many files have changed in this diff Show More