Compare commits
77 Commits
v2.0.0-bet
...
v2.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d46c4c4285 | ||
|
|
0b871175b2 | ||
|
|
c34c4fd880 | ||
|
|
ff00f0c283 | ||
|
|
847c2e5810 | ||
|
|
e62c81bb33 | ||
|
|
984fdb3459 | ||
|
|
019b78bf38 | ||
|
|
cf88c7669f | ||
|
|
f4383ba848 | ||
|
|
adbda459ba | ||
|
|
984d747179 | ||
|
|
c012f84064 | ||
|
|
a57a18b9f5 | ||
|
|
309c323a14 | ||
|
|
6a1e606d6d | ||
|
|
af3931847e | ||
|
|
41eadb5843 | ||
|
|
8963d719d9 | ||
|
|
0a7761ffff | ||
|
|
1a63771f2d | ||
|
|
1f9141ba33 | ||
|
|
a341a69d48 | ||
|
|
0cedfe0af3 | ||
|
|
59f492ed8f | ||
|
|
8f416ff841 | ||
|
|
99ab194e4b | ||
|
|
43fb9b48dc | ||
|
|
c61e54d6a3 | ||
|
|
c8d195053f | ||
|
|
8cfe5d0481 | ||
|
|
fcc2f5015f | ||
|
|
9b98a45264 | ||
|
|
3c4e836fb8 | ||
|
|
a8111b9d3f | ||
|
|
8eaeffeda3 | ||
|
|
eed76966c4 | ||
|
|
b095e4eb36 | ||
|
|
3dbdf3e613 | ||
|
|
5ceca058a7 | ||
|
|
a8e867bbf9 | ||
|
|
4cb8eb0213 | ||
|
|
461f216cab | ||
|
|
2756313e4a | ||
|
|
e802a2f574 | ||
|
|
a45940b104 | ||
|
|
ac0ac5f4ea | ||
|
|
883b6d13a5 | ||
|
|
ee9bbf0687 | ||
|
|
693dc65b07 | ||
|
|
354d271582 | ||
|
|
eaaeb0f5d3 | ||
|
|
e4e8deec59 | ||
|
|
3f21704b82 | ||
|
|
7ad2bb8bde | ||
|
|
3123e4f5f8 | ||
|
|
60e43c9f40 | ||
|
|
298813a057 | ||
|
|
2db74f3a39 | ||
|
|
024ff1773e | ||
|
|
6046e5a153 | ||
|
|
3c39dbd87f | ||
|
|
c5747b6a3e | ||
|
|
750d570c3d | ||
|
|
b811b00b9f | ||
|
|
a372570038 | ||
|
|
6d5fa15098 | ||
|
|
b65369a8a6 | ||
|
|
0af0ac3cce | ||
|
|
f0ed78eed5 | ||
|
|
b1cda68c75 | ||
|
|
dd1081cfa2 | ||
|
|
046b8f3a92 | ||
|
|
72650a5419 | ||
|
|
d471e686b5 | ||
|
|
41c4342f76 | ||
|
|
16802aaac7 |
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
dist
|
||||
@@ -1,9 +1,18 @@
|
||||
# 根配置文件
|
||||
## 编辑器在查找配置时会停止查找更高层次的配置文件
|
||||
root = true
|
||||
|
||||
# 通配符,匹配所有文件
|
||||
[*]
|
||||
# 设置字符集为 UTF-8,确保文件中的文本使用 UTF-8 编码
|
||||
charset = utf-8
|
||||
# 使用空格作为缩进风格
|
||||
indent_style = space
|
||||
# 设置每个缩进级别的空格数量为 2
|
||||
indent_size = 2
|
||||
# 设置行尾换行符为LF(Line Feed)
|
||||
end_of_line = lf
|
||||
# 在文件的末尾插入一个新行
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
# 删除每一行末尾的尾随空格
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -21,9 +21,18 @@ RENDERER_VITE_SITE_ANTHOR = "無名"
|
||||
RENDERER_VITE_SITE_KEYWORDS = "SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器"
|
||||
RENDERER_VITE_SITE_DES = "一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能"
|
||||
RENDERER_VITE_SITE_URL = "imsyy.top"
|
||||
RENDERER_VITE_SITE_LOGO = "/images/logo/favicon.svg"
|
||||
RENDERER_VITE_SITE_APPLE_LOGO = "/images/logo/favicon-apple.png"
|
||||
|
||||
# Cookie
|
||||
## 咪咕音乐 Cookie
|
||||
MAIN_VITE_MIGU_COOKIE = ""
|
||||
|
||||
# 公告配置
|
||||
## 若无需公告,请将标题或内容任意一项设为空即可
|
||||
## 公告类型
|
||||
RENDERER_VITE_ANN_TYPE = "info"
|
||||
## 公告标题
|
||||
RENDERER_VITE_ANN_TITLE = ""
|
||||
## 公告内容
|
||||
RENDERER_VITE_ANN_CONTENT = ""
|
||||
## 公告时长(毫秒)不可超过 999999
|
||||
RENDERER_VITE_ANN_DURATION = 8000
|
||||
@@ -2,3 +2,5 @@ node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
||||
@@ -49,5 +49,8 @@ module.exports = {
|
||||
$notification: true,
|
||||
$changeThemeColor: true,
|
||||
$canNotConnect: true,
|
||||
$refreshCloudCatch: true,
|
||||
$cleanAll: true,
|
||||
$player: true,
|
||||
},
|
||||
};
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/add.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: 添加功能
|
||||
description: 请填写希望添加的功能的具体信息
|
||||
title: "添加功能"
|
||||
labels: [add]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "希望添加什么功能?"
|
||||
placeholder: "请填写功能名称"
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "请详细描述希望添加的功能的具体信息"
|
||||
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,5 +1,6 @@
|
||||
name: 遇到问题
|
||||
description: 关于使用过程中遇到的问题
|
||||
title: 请填写标题
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: input
|
||||
@@ -30,4 +31,5 @@ body:
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "有需要补充的信息吗?比如控制台的报错什么的"
|
||||
description: "请填写完整的复现步骤和遇到的问题,包括但不限于报错信息、控制台输出、网络请求等"
|
||||
placeholder: "请填写具体的复现步骤和遇到的问题"
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 添加功能
|
||||
url: https://github.com/imsyy/SPlayer/discussions/new?category=%E6%83%B3%E6%B3%95-ideas
|
||||
about: 新的功能建议和提问答疑请到讨论区发起
|
||||
- name: 转到讨论区
|
||||
url: https://github.com/imsyy/SPlayer/discussions
|
||||
about: Issues 用于反馈 Bug, 新的功能建议和提问答疑请到讨论区发起
|
||||
- name: 提问的艺术
|
||||
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||
8
.github/workflows/build.yml
vendored
@@ -21,6 +21,14 @@ jobs:
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.x"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
46
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
imsyy/splayer
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
55
.github/workflows/release.yml
vendored
@@ -12,6 +12,7 @@ jobs:
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
@@ -21,6 +22,14 @@ jobs:
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.x"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
@@ -36,24 +45,21 @@ jobs:
|
||||
with:
|
||||
name: SPlarer-Win
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.exe
|
||||
dist/*.msi
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.exe
|
||||
dist/*.msi
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Mac
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
@@ -63,7 +69,15 @@ jobs:
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.x"
|
||||
# 安装项目依赖
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
else
|
||||
echo ".env file already exists. Skipping the copy step."
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
@@ -78,24 +92,21 @@ jobs:
|
||||
with:
|
||||
name: SPlarer-Macos
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Linux
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
@@ -108,6 +119,14 @@ jobs:
|
||||
# 更新 Ubuntu 软件源
|
||||
- name: Ubuntu Update with sudo
|
||||
run: sudo apt-get update
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
else
|
||||
echo ".env file already exists. Skipping the copy step."
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
@@ -123,19 +142,13 @@ jobs:
|
||||
with:
|
||||
name: SPlarer-Linux
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
@@ -14,6 +14,7 @@ dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
out
|
||||
.env
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
5
.npmrc
@@ -1,2 +1,5 @@
|
||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
registry=https://registry.npmmirror.com
|
||||
disturl=https://registry.npmmirror.com/-/binary/node
|
||||
# ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
ELECTRON_MIRROR=https://registry.npmmirror.com/-/binary/electron/
|
||||
shamefully-hoist=true
|
||||
|
||||
@@ -4,3 +4,5 @@ pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# 是否使用单引号而不是双引号
|
||||
singleQuote: false
|
||||
# 是否在语句末尾使用分号
|
||||
semi: true
|
||||
# 每行的最大打印宽度
|
||||
printWidth: 100
|
||||
# 是否在对象和数组的末尾加上逗号
|
||||
trailingComma: all
|
||||
|
||||
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
# build
|
||||
FROM node:18-alpine as builder
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN [ ! -e ".env" ] && cp .env.example .env || true
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# nginx
|
||||
FROM nginx:1.25.3-alpine-slim as app
|
||||
|
||||
COPY --from=builder /app/out/renderer /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
RUN apk add --no-cache npm
|
||||
|
||||
RUN npm install -g NeteaseCloudMusicApi
|
||||
|
||||
CMD nginx && npx NeteaseCloudMusicApi
|
||||
149
LICENSE
@@ -1,23 +1,21 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
422
README.md
@@ -1,25 +1,28 @@
|
||||
> [!IMPORTANT]
|
||||
> ## 🎉 当前项目正在重构中 🎉
|
||||
>
|
||||
> - 目前版本进入维护模式,仅在遇到重大问题时会进行修复
|
||||
> - 支持客户端与网页端
|
||||
> - 支持现有版本所有功能
|
||||
> - 新增支持播放与管理本地歌曲
|
||||
# SPlayer
|
||||
|
||||
<div align="center">
|
||||
<img alt="logo" height="80" src="./public/images/logo/favicon.png" />
|
||||
<h2>SPlayer</h2>
|
||||
<p>一个简约的音乐播放器</p>
|
||||
<img alt="main" src="./screenshots/main.png" />
|
||||
</div>
|
||||
<br />
|
||||
> 一个简约的音乐播放器
|
||||
|
||||

|
||||
|
||||
## 说明
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> ### 严肃警告
|
||||
>
|
||||
> - 请务必遵守 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可协议
|
||||
> - 在您的修改、演绎、分发或派生项目中,必须同样采用 **AGPL-3.0** 许可协议,**并在适当的位置包含本项目的许可和版权信息**
|
||||
> - **禁止用于售卖或其他商业用途**,如若发现,作者保留追究法律责任的权利
|
||||
> - 若发现未遵守 **AGPL-3.0** 许可协议的行为,**本项目将永久停更**
|
||||
> - 感谢您的尊重与理解
|
||||
|
||||
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 [Electron](https://www.electronjs.org/zh/docs/latest/) 开发
|
||||
- 支持网页端与客户端,由于设备有限,目前仅适配 `Win`,其他平台可自行构建
|
||||
- ~~仅对移动端做了基础适配,**不保证功能全部可用**~~
|
||||
- 欢迎各位大佬指点和 `Star` 哦 😍
|
||||
- 支持网页端与客户端,由于设备有限,目前仅适配 `Win`,其他平台可自行解决兼容性后进行构建
|
||||
- 仅对移动端做了基础适配,**不保证功能全部可用**
|
||||
|
||||
> 请注意,本程序不打算开发移动端,也不会对移动端进行完美适配,仅保证基础可用性
|
||||
|
||||
- 欢迎各位大佬 `Star` 😍
|
||||
|
||||
## 👀 Demo
|
||||
|
||||
@@ -27,38 +30,31 @@
|
||||
|
||||
## 🎉 功能
|
||||
|
||||
- 支持扫码登录
|
||||
- 支持手机号登录
|
||||
- 自动进行每日签到及云贝签到
|
||||
- 封面主题色自适应
|
||||
- 本地歌曲管理及分类 ~~以及音乐标签编辑~~
|
||||
- **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
|
||||
- 下载歌曲(最高支持 Hi-Res)
|
||||
- 新建歌单及歌单编辑
|
||||
- 收藏 / 取消收藏歌单或歌手
|
||||
- 每日推荐歌曲
|
||||
- 私人 FM
|
||||
- 云盘音乐上传
|
||||
- 云盘内歌曲播放
|
||||
- 云盘内歌曲纠正
|
||||
- 云盘歌曲删除
|
||||
- 支持逐字歌词
|
||||
- 歌词滚动以及歌词翻译
|
||||
- MV 与视频播放
|
||||
- 音乐频谱显示( 暂时去除,还待完善 )
|
||||
- 音乐渐入渐出
|
||||
- 支持 PWA
|
||||
- 支持评论区及评论点赞
|
||||
- 明暗模式自动 / 手动切换
|
||||
- ~~移动端基础适配~~
|
||||
- ~~`i18n` 支持~~
|
||||
|
||||
#### 待办
|
||||
|
||||
- [ ] 完善音乐频谱
|
||||
- [ ] 添加桌面歌词
|
||||
- [ ] 多种布局方式
|
||||
- [ ] 发表评论
|
||||
- ✨ 支持扫码登录
|
||||
- 📱 支持手机号登录
|
||||
- 📅 自动进行每日签到及云贝签到
|
||||
- 🎨 封面主题色自适应
|
||||
- 📁 本地歌曲管理及分类(建议先使用 [音乐标签](https://www.cnblogs.com/vinlxc/p/11347744.html) 进行匹配后再使用)
|
||||
- 🎵 **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
|
||||
- ⬇️ 下载歌曲(最高支持 Hi-Res)
|
||||
- ➕ 新建歌单及歌单编辑
|
||||
- ❤️ 收藏 / 取消收藏歌单或歌手
|
||||
- 🎶 每日推荐歌曲
|
||||
- 📻 私人 FM
|
||||
- ☁️ 云盘音乐上传
|
||||
- 📂 云盘内歌曲播放
|
||||
- 🔄 云盘内歌曲纠正
|
||||
- 🗑️ 云盘歌曲删除
|
||||
- 📝 支持逐字歌词
|
||||
- 🔄 歌词滚动以及歌词翻译
|
||||
- 📹 MV 与视频播放
|
||||
- 🎶 音乐频谱显示
|
||||
- ⏭️ 音乐渐入渐出
|
||||
- 🔄 支持 PWA
|
||||
- 💬 支持评论区及评论点赞
|
||||
- 🌓 明暗模式自动 / 手动切换
|
||||
- 📱 移动端基础适配
|
||||
- ~~🌐 `i18n` 支持~~
|
||||
|
||||
## 🖼️ Screenshots
|
||||
|
||||
@@ -118,68 +114,109 @@
|
||||
|
||||
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
|
||||
|
||||
## ⚙️ 部署
|
||||
## ⚙️ Docker 部署
|
||||
|
||||
> Vercel 等托管平台可在 Fork 后一键导入并自动部署
|
||||
> 安装及配置 `Docker` 将不在此处说明,请自行解决
|
||||
|
||||
### API 服务(客户端无需理会,如果需要网页端,则必需部署)
|
||||
### 本地构建
|
||||
|
||||
> 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目
|
||||
|
||||
- 请在根目录下的 `.env` 文件中的 `RENDERER_VITE_SERVER_URL` 中填入 API 地址(必需)
|
||||
|
||||
```js
|
||||
RENDERER_VITE_SERVER_URL = "your api url";
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
> 请尽量拉取最新分支后使用本地构建方式,在线部署的仓库可能更新不及时
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或者
|
||||
yarn install
|
||||
# 或者
|
||||
npm install
|
||||
# 构建
|
||||
docker build -t splayer .
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 7899:7899 splayer
|
||||
# 或使用 Docker Compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 开发
|
||||
### 在线部署
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
# 或者
|
||||
yarn dev
|
||||
# 或者
|
||||
npm dev
|
||||
# 从 Docker Hub 拉取
|
||||
docker pull imsyy/splayer:latest
|
||||
# 从 GitHub ghcr 拉取
|
||||
docker pull ghcr.io/imsyy/splayer:latest
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
|
||||
```
|
||||
|
||||
### 构建网页端
|
||||
以上步骤成功后,将会在本地 [localhost:7899](http://localhost:7899/) 启动,如需更换端口,请自行修改命令行中的端口号
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
yarn build
|
||||
# 或者
|
||||
npm build
|
||||
```
|
||||
## ⚙️ Vercel 部署
|
||||
|
||||
构建完成后可将生成的 `out/renderer` 文件夹内的文件上传至服务器
|
||||
> 其他部署平台大致相同,在此不做说明
|
||||
|
||||
若使用的为第三方部署平台,比如 `Vercel`,请将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
1. 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目,并成功取得在线访问地址
|
||||
2. 点击本仓库右上角的 `Fork`,复制本仓库到你的 `GitHub` 账号
|
||||
3. 复制 `/.env.example` 文件并重命名为 `/.env`
|
||||
4. 将 `.env` 文件中的 `RENDERER_VITE_SERVER_URL` 改为第一步得到的 API 地址
|
||||
|
||||

|
||||
```js
|
||||
RENDERER_VITE_SERVER_URL = "https://example.com";
|
||||
```
|
||||
|
||||
### 构建客户端
|
||||
5. 将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
|
||||
```bash
|
||||
# win
|
||||
pnpm build:win
|
||||
# linux
|
||||
pnpm build:linux
|
||||
# mac
|
||||
pnpm build:mac
|
||||
```
|
||||

|
||||
|
||||
构建完成后可在 `dist` 文件夹中打开可执行文件来完成安装操作
|
||||
6. 点击 `Deploy`,即可成功部署
|
||||
|
||||
## ⚙️ 服务器部署
|
||||
|
||||
1. 重复 `⚙️ Vercel 部署` 中的 1 - 4 步骤
|
||||
2. 克隆仓库
|
||||
|
||||
> 将链接中的 example/repository.git 替换为你要克隆的实际仓库的地址
|
||||
|
||||
```bash
|
||||
git clone https://github.com/example/repository.git
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或者
|
||||
yarn install
|
||||
# 或者
|
||||
npm install
|
||||
```
|
||||
|
||||
4. 编译打包
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
yarn build
|
||||
# 或者
|
||||
npm build
|
||||
```
|
||||
|
||||
5. 将站点运行目录设置为 `out/renderer` 目录
|
||||
|
||||
## ⚙️ 本地部署
|
||||
|
||||
1. 本地部署需要用到 `Node.js`。可前往 [Node.js 官网](https://nodejs.org/zh-cn/) 下载安装包,请下载最新稳定版
|
||||
2. 安装 pnpm
|
||||
|
||||
```bash
|
||||
npm install pnpm -g
|
||||
```
|
||||
|
||||
3. 克隆仓库并拉取至本地,此处不再赘述
|
||||
4. 使用 `pnpm install` 安装项目依赖(若安装过程中遇到网络错误,请使用国内镜像源替代,此处不再赘述)
|
||||
5. 复制 `/.env.example` 文件并重命名为 `/.env` 并修改配置
|
||||
6. 打包客户端,请依据你的系统类型来选择,打包成功后,会输出安装包或可执行文件在 `/dist` 目录中,可自行安装
|
||||
|
||||
| 命令 | 系统类型 |
|
||||
| ------------------ | -------- |
|
||||
| `pnpm build:win` | Windows |
|
||||
| `pnpm build:linux` | Linux |
|
||||
| `pnpm build:mac` | MacOS |
|
||||
|
||||
## 😘 鸣谢
|
||||
|
||||
@@ -188,19 +225,8 @@ pnpm build:mac
|
||||
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
|
||||
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||
- [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)
|
||||
- [BlurLyric](https://github.com/Project-And-Factory/BlurLyric)
|
||||
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
|
||||
|
||||
## 📜 开源许可
|
||||
|
||||
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
- 本项目基于 [GNU General Public License version 3](https://opensource.org/license/gpl-3-0/) 许可进行开源
|
||||
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 GPL Version 3 进行,源代码必须一并提供
|
||||
2. **派生作品:** 任何派生作品必须同样采用 GPL Version 3,并在适当的地方注明原始项目的许可证
|
||||
3. **免责声明:** 根据 GPL Version 3,本项目不提供任何明示或暗示的担保。请详细阅读 GPL Version 3以了解完整的免责声明内容
|
||||
4. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
5. **许可证链接:** 请阅读 [GNU General Public License version 3](https://opensource.org/license/gpl-3-0/) 了解更多详情
|
||||
|
||||
## 📢 免责声明
|
||||
|
||||
本项目部分功能使用了网易云音乐的第三方 API 服务,**仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
@@ -210,3 +236,191 @@ pnpm build:mac
|
||||
请使用者在使用本项目时遵守相关法律法规,**不要将本项目用于任何商业及非法用途。如有违反,一切后果由使用者自负。** 同时,使用者应该自行承担因使用本项目而带来的风险和责任。本项目开发者不对本项目所提供的服务和内容做出任何保证
|
||||
|
||||
感谢您的理解
|
||||
|
||||
## 📜 开源许可
|
||||
|
||||
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
- 本项目基于 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可进行开源
|
||||
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 AGPL-3.0 进行,源代码必须一并提供
|
||||
2. **派生作品:** 任何派生作品必须同样采用 AGPL-3.0,并在适当的地方注明原始项目的许可证
|
||||
3. **注明原作者:** 在任何修改、派生作品或其他分发中,必须在适当的位置明确注明原作者及其贡献
|
||||
4. **免责声明:** 根据 AGPL-3.0,本项目不提供任何明示或暗示的担保。请详细阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 以了解完整的免责声明内容
|
||||
5. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
6. **许可证链接:** 请阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 了解更多详情
|
||||
|
||||
## 📂 目录结构
|
||||
|
||||
<details>
|
||||
<summary>查看目录结构详情</summary>
|
||||
|
||||
> ChatGPT 写的,如有错误,请见谅
|
||||
|
||||
```dir
|
||||
├── auto-imports.d.ts # 自动导入
|
||||
├── components.d.ts # 自动导入
|
||||
├── docker-compose.yml # Docker Compose
|
||||
├── Dockerfile # Docker
|
||||
├── electron # Electron
|
||||
│ ├── main # Electron 主进程
|
||||
│ │ ├── index.js # 主进程入口
|
||||
│ │ ├── mainIpcMain.js # 主进程与渲染进程通信
|
||||
│ │ ├── startMainServer.js # 启动主进程服务器
|
||||
│ │ ├── startNcmServer.js # 启动网易云音乐服务
|
||||
│ │ └── utils # 主进程工具函数
|
||||
│ │ ├── checkUpdates.js # 检查更新
|
||||
│ │ ├── createGlobalShortcut.js # 创建全局快捷键
|
||||
│ │ ├── createSystemTray.js # 创建系统托盘
|
||||
│ │ ├── getNeteaseMusicUrl.js # 解灰
|
||||
│ │ ├── kwDES.js # DES加密算法
|
||||
│ │ └── readDirAsync.js # 异步读取目录
|
||||
│ └── preload # Electron 预加载脚本
|
||||
│ └── index.js # 预加载脚本入口文件
|
||||
├── electron-builder.yml # Electron Builder
|
||||
├── electron.vite.config.js # Electron Vite
|
||||
├── index.html # 主页面 HTML
|
||||
├── LICENSE # 项目许可证
|
||||
├── nginx.conf # Nginx 配置
|
||||
├── src # 项目源代码
|
||||
│ ├── api # API 相关
|
||||
│ │ ├── ./..
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── assets # 静态资源
|
||||
│ │ ├── emoji.json # 表情数据
|
||||
│ │ ├── icon.json # 图标数据
|
||||
│ │ └── themeColor.json # 主题颜色数据
|
||||
│ ├── components # 组件目录
|
||||
│ │ ├── Cover # 封面相关组件目录
|
||||
│ │ │ ├── CoverDropdown.vue # 封面下拉组件
|
||||
│ │ │ ├── MainCover.vue # 主封面组件
|
||||
│ │ │ ├── SpecialCoverCard.vue # 特殊封面卡片组件
|
||||
│ │ │ └── SpecialCover.vue # 特殊封面组件
|
||||
│ │ ├── Global # 全局组件目录
|
||||
│ │ │ ├── MainLayout.vue # 主布局组件
|
||||
│ │ │ ├── Menu.vue # 菜单组件
|
||||
│ │ │ ├── Pagination.vue # 分页组件
|
||||
│ │ │ ├── Playlist.vue # 歌单组件
|
||||
│ │ │ ├── Provider.vue # 全局化配置组件
|
||||
│ │ │ └── SvgIcon.vue # SVG 图标组件
|
||||
│ │ ├── List # 列表组件目录
|
||||
│ │ │ ├── CommentList.vue # 评论列表组件
|
||||
│ │ │ ├── SongListDropdown.vue # 歌曲下拉组件
|
||||
│ │ │ └── SongList.vue # 歌曲列表组件
|
||||
│ │ ├── Modal # 弹窗相关组件目录
|
||||
│ │ │ ├── AddPlaylist.vue # 添加歌单组件
|
||||
│ │ │ ├── CloudSongMatch.vue # 云盘歌曲匹配组件
|
||||
│ │ │ ├── CreatePlaylist.vue # 创建歌单组件
|
||||
│ │ │ ├── DownloadSong.vue # 下载歌曲组件
|
||||
│ │ │ ├── LoginPhone.vue # 手机登录组件
|
||||
│ │ │ ├── LoginQRCode.vue # 二维码登录组件
|
||||
│ │ │ ├── Login.vue # 登录组件
|
||||
│ │ │ ├── PlaylistUpdate.vue # 歌单编辑组件
|
||||
│ │ │ └── UpCloudSong.vue # 上传云盘歌曲组件
|
||||
│ │ ├── Nav # 导航相关组件目录
|
||||
│ │ │ ├── MainNav.vue # 主导航组件
|
||||
│ │ │ └── UserData.vue # 用户数据组件
|
||||
│ │ ├── Player # 播放器相关组件目录
|
||||
│ │ │ ├── CountDown.vue # 倒计时组件
|
||||
│ │ │ ├── FullPlayer.vue # 全屏播放器组件
|
||||
│ │ │ ├── Lyric.vue # 歌词组件
|
||||
│ │ │ ├── MainControl.vue # 主控制组件
|
||||
│ │ │ ├── PlayerControl.vue # 播放器控制组件
|
||||
│ │ │ ├── PlayerCover.vue # 播放器封面组件
|
||||
│ │ │ └── PrivateFm.vue # 私人 FM 组件
|
||||
│ │ ├── Search # 搜索相关组件
|
||||
│ │ │ ├── SearchHot.vue # 热门搜索组件
|
||||
│ │ │ ├── SearchInp.vue # 搜索输入组件
|
||||
│ │ │ └── SearchSuggestions.vue # 搜索建议组件
|
||||
│ │ └── WinDom # 窗口 DOM 相关组件
|
||||
│ │ └── TitleBar.vue # 标题栏组件
|
||||
│ ├── main.js # Vue 应用的入口文件
|
||||
│ ├── router # Vue Router 相关文件夹
|
||||
│ │ ├── index.js # Vue Router 入口文件
|
||||
│ │ └── routes.js # 路由配置文件
|
||||
│ ├── stores # Vuex Store 相关文件夹
|
||||
│ │ ├── indexedDB.js # IndexedDB 数据库相关文件
|
||||
│ │ ├── index.js # Vuex Store 入口文件
|
||||
│ │ ├── musicData.js # 音乐数据相关文件
|
||||
│ │ ├── siteData.js # 网站数据相关文件
|
||||
│ │ ├── siteSettings.js # 网站设置相关文件
|
||||
│ │ └── siteStatus.js # 网站状态相关文件
|
||||
│ ├── style # 样式相关文件夹
|
||||
│ │ ├── animate.scss # 动画样式文件
|
||||
│ │ └── main.scss # 主样式文件
|
||||
│ ├── utils # 工具函数文件夹
|
||||
│ │ ├── auth.js # 认证相关函数
|
||||
│ │ ├── base64.js # Base64编码解码相关函数
|
||||
│ │ ├── color-utils.js # 颜色工具函数
|
||||
│ │ ├── cover-color.js # 封面颜色相关函数
|
||||
│ │ ├── debounce.js # 防抖函数
|
||||
│ │ ├── formatData.js # 数据格式化函数
|
||||
│ │ ├── formRules.js # 表单验证规则
|
||||
│ │ ├── globalEvents.js # 全局事件处理函数
|
||||
│ │ ├── globalShortcut.js # 全局快捷键相关函数
|
||||
│ │ ├── helper.js # 辅助函数
|
||||
│ │ ├── parseLyric.js # 解析歌词函数
|
||||
│ │ ├── Player.js # 播放器控制相关函数
|
||||
│ │ ├── request.js # 网络请求相关函数
|
||||
│ │ ├── throttle.js # 节流函数
|
||||
│ │ ├── timeTools.js # 时间工具函数
|
||||
│ │ └── userSignIn.js # 用户登录相关函数
|
||||
│ └── views # Vue组件文件夹
|
||||
│ ├── Artist # 艺术家相关组件
|
||||
│ │ ├── albums.vue # 艺术家专辑组件
|
||||
│ │ ├── hot.vue # 艺术家热门组件
|
||||
│ │ ├── index.vue # 艺术家主组件
|
||||
│ │ ├── songs.vue # 艺术家歌曲组件
|
||||
│ │ └── videos.vue # 艺术家视频组件
|
||||
│ ├── Cloud.vue # 云盘组件
|
||||
│ ├── Comment.vue # 评论组件
|
||||
│ ├── DailySongs.vue # 每日推荐组件
|
||||
│ ├── Discover # 发现音乐相关组件
|
||||
│ │ ├── artists.vue # 发现音乐艺术家组件
|
||||
│ │ ├── index.vue # 发现音乐主组件
|
||||
│ │ ├── new.vue # 发现音乐新歌组件
|
||||
│ │ ├── playlists.vue # 发现音乐歌单组件
|
||||
│ │ └── toplists.vue # 发现音乐排行榜组件
|
||||
│ ├── History.vue # 历史记录组件
|
||||
│ ├── Home.vue # 主页组件
|
||||
│ ├── Like # 我喜欢的相关组件
|
||||
│ │ ├── albums.vue # 我喜欢的专辑组件
|
||||
│ │ ├── artists.vue # 我喜欢的艺术家组件
|
||||
│ │ ├── index.vue # 我喜欢的主组件
|
||||
│ │ ├── playlists.vue # 我喜欢的歌单组件
|
||||
│ │ └── videos.vue # 我喜欢的视频组件
|
||||
│ ├── List # 列表相关组件
|
||||
│ │ ├── album.vue # 专辑组件
|
||||
│ │ └── playlist.vue # 歌单组件
|
||||
│ │ └── dj.vue # 电台组件
|
||||
│ ├── Local # 本地音乐相关组件
|
||||
│ │ ├── albums.vue # 本地音乐专辑组件
|
||||
│ │ ├── artists.vue # 本地音乐艺术家组件
|
||||
│ │ ├── index.vue # 本地音乐主组件
|
||||
│ │ └── songs.vue # 本地音乐歌曲组件
|
||||
│ ├── Player.vue # 视频播放器组件
|
||||
│ ├── Dj # 电台相关组件
|
||||
│ │ └── index.vue # 电台主组件
|
||||
│ │ └── type.vue # 电台分类组件
|
||||
│ ├── Search # 搜索相关组件
|
||||
│ │ ├── albums.vue # 搜索专辑组件
|
||||
│ │ ├── artists.vue # 搜索艺术家组件
|
||||
│ │ ├── index.vue # 搜索主组件
|
||||
│ │ ├── playlists.vue # 搜索歌单组件
|
||||
│ │ ├── songs.vue # 搜索歌曲组件
|
||||
│ │ └── videos.vue # 搜索视频组件
|
||||
│ │ └── djs.vue # 搜索电台组件
|
||||
│ ├── Setting # 设置相关组件
|
||||
│ │ └── index.vue # 设置主组件
|
||||
│ ├── Song.vue
|
||||
│ ├── State
|
||||
│ │ ├── 403.vue
|
||||
│ │ ├── 404.vue
|
||||
│ │ └── 500.vue
|
||||
│ └── Test.vue
|
||||
└── vercel.json # Vercel 部署配置
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
[](https://star-history.com/#imsyy/SPlayer&Date)
|
||||
|
||||
3
auto-imports.d.ts
vendored
@@ -65,5 +65,6 @@ declare global {
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
16
components.d.ts
vendored
@@ -8,27 +8,31 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AddPlaylist: typeof import('./src/components/Modal/AddPlaylist.vue')['default']
|
||||
CloudSongMatch: typeof import('./src/components/Modal/CloudSongMatch.vue')['default']
|
||||
CommentList: typeof import('./src/components/List/CommentList.vue')['default']
|
||||
CountDown: typeof import('./src/components/Player/CountDown.vue')['default']
|
||||
CoverDropdown: typeof import('./src/components/Cover/CoverDropdown.vue')['default']
|
||||
CoverPlayBtn: typeof import('./src/components/Cover/CoverPlayBtn.vue')['default']
|
||||
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
|
||||
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
|
||||
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
|
||||
Login: typeof import('./src/components/Modal/Login.vue')['default']
|
||||
LoginPhone: typeof import('./src/components/Modal/LoginPhone.vue')['default']
|
||||
LoginQRCode: typeof import('./src/components/Modal/LoginQRCode.vue')['default']
|
||||
Lyric: typeof import('./src/components/Player/Lyric.vue')['default']
|
||||
MainControl: typeof import('./src/components/Player/MainControl.vue')['default']
|
||||
MainCover: typeof import('./src/components/Cover/MainCover.vue')['default']
|
||||
MainLayout: typeof import('./src/components/Global/MainLayout.vue')['default']
|
||||
MainNav: typeof import('./src/components/Nav/MainNav.vue')['default']
|
||||
Menu: typeof import('./src/components/Global/Menu.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NBackTop: typeof import('naive-ui')['NBackTop']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
@@ -36,6 +40,7 @@ declare module 'vue' {
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
@@ -48,7 +53,9 @@ declare module 'vue' {
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
@@ -58,9 +65,11 @@ declare module 'vue' {
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NResult: typeof import('naive-ui')['NResult']
|
||||
@@ -68,7 +77,6 @@ declare module 'vue' {
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
@@ -77,8 +85,10 @@ declare module 'vue' {
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
Pagination: typeof import('./src/components/Global/Pagination.vue')['default']
|
||||
PlayerControl: typeof import('./src/components/Player/PlayerControl.vue')['default']
|
||||
PlayerCover: typeof import('./src/components/Player/PlayerCover.vue')['default']
|
||||
Playlist: typeof import('./src/components/Global/Playlist.vue')['default']
|
||||
PlaylistUpdate: typeof import('./src/components/Modal/PlaylistUpdate.vue')['default']
|
||||
PrivateFm: typeof import('./src/components/Player/PrivateFm.vue')['default']
|
||||
@@ -89,9 +99,11 @@ declare module 'vue' {
|
||||
SearchInp: typeof import('./src/components/Search/SearchInp.vue')['default']
|
||||
SearchSuggestions: typeof import('./src/components/Search/SearchSuggestions.vue')['default']
|
||||
SongList: typeof import('./src/components/List/SongList.vue')['default']
|
||||
SongListDrawer: typeof import('./src/components/List/SongListDrawer.vue')['default']
|
||||
SongListDropdown: typeof import('./src/components/List/SongListDropdown.vue')['default']
|
||||
SpecialCover: typeof import('./src/components/Cover/SpecialCover.vue')['default']
|
||||
SpecialCoverCard: typeof import('./src/components/Cover/SpecialCoverCard.vue')['default']
|
||||
Spectrum: typeof import('./src/components/Player/Spectrum.vue')['default']
|
||||
SvgIcon: typeof import('./src/components/Global/SvgIcon.vue')['default']
|
||||
TitleBar: typeof import('./src/components/WinDom/TitleBar.vue')['default']
|
||||
UpCloudSong: typeof import('./src/components/Modal/UpCloudSong.vue')['default']
|
||||
|
||||
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
SPlayer:
|
||||
build:
|
||||
context: .
|
||||
image: splayer
|
||||
container_name: SPlayer
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
ports:
|
||||
- 7899:7899
|
||||
restart: always
|
||||
@@ -19,15 +19,11 @@ asarUnpack:
|
||||
# Windows 平台配置
|
||||
win:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
icon: public/images/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
- nsis
|
||||
- portable
|
||||
# 管理员权限
|
||||
requestedExecutionLevel: highestAvailable
|
||||
target: nsis
|
||||
# NSIS 安装器配置
|
||||
nsis:
|
||||
# 一键式安装程序还是辅助安装程序
|
||||
@@ -44,12 +40,16 @@ nsis:
|
||||
allowElevation: true
|
||||
# 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true
|
||||
# 安装包图标
|
||||
installerIcon: public/images/icons/favicon.ico
|
||||
# 卸载命令图标
|
||||
uninstallerIcon: public/images/icons/favicon.ico
|
||||
# macOS 平台配置
|
||||
mac:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_512.png
|
||||
icon: public/images/icons/favicon-512x512.png
|
||||
# 权限继承的文件路径
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
# 扩展信息,如权限描述
|
||||
@@ -69,13 +69,12 @@ dmg:
|
||||
# Linux 平台配置
|
||||
linux:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
icon: public/images/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
- rpm
|
||||
- tar.gz
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
splitVendorChunkPlugin,
|
||||
} from "electron-vite";
|
||||
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
@@ -41,7 +42,7 @@ export default defineConfig(({ mode }) => {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "electron/preload/index.js"),
|
||||
index: resolve(__dirname, "electron/preload/index.mjs"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -71,6 +72,62 @@ export default defineConfig(({ mode }) => {
|
||||
viteCompression(),
|
||||
// splitVendorChunkPlugin
|
||||
splitVendorChunkPlugin(),
|
||||
// PWA
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
workbox: {
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
cleanupOutdatedCaches: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /(.*?)\.(woff2|woff|ttf)/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "file-cache",
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /(.*?)\.(webp|png|jpe?g|svg|gif|bmp|psd|tiff|tga|eps)/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "image-cache",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: {
|
||||
name: getEnv("RENDERER_VITE_SITE_TITLE"),
|
||||
short_name: getEnv("RENDERER_VITE_SITE_TITLE"),
|
||||
description: getEnv("RENDERER_VITE_SITE_DES"),
|
||||
display: "standalone",
|
||||
start_url: "/",
|
||||
theme_color: "#fff",
|
||||
background_color: "#efefef",
|
||||
icons: [
|
||||
{
|
||||
src: "/images/icons/favicon-32x32.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/icons/favicon-96x96.png",
|
||||
sizes: "96x96",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/icons/favicon-256x256.png",
|
||||
sizes: "256x256",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/icons/favicon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
// 服务器配置
|
||||
server: {
|
||||
@@ -100,9 +157,6 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
sourcemap: false,
|
||||
win: {
|
||||
icon: resolve(__dirname, "/public/images/logo/favicon.png"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,154 +1,252 @@
|
||||
import { join } from "path";
|
||||
import { app, shell, BrowserWindow, globalShortcut } from "electron";
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
|
||||
import { app, protocol, shell, BrowserWindow, globalShortcut, nativeImage } from "electron";
|
||||
import { platform, optimizer, is } from "@electron-toolkit/utils";
|
||||
import { startNcmServer } from "@main/startNcmServer";
|
||||
import { startMainServer } from "@main/startMainServer";
|
||||
import { configureAutoUpdater } from "@main/utils/checkUpdates";
|
||||
import createSystemInfo from "@main/utils/createSystemInfo";
|
||||
import createSystemTray from "@main/utils/createSystemTray";
|
||||
import createGlobalShortcut from "@main/utils/createGlobalShortcut";
|
||||
import mainIpcMain from "@main/mainIpcMain";
|
||||
import Store from "electron-store";
|
||||
import log from "electron-log";
|
||||
|
||||
// 主窗口
|
||||
let mainWindow;
|
||||
|
||||
// 屏蔽报错
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||
|
||||
// 单例锁
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
// 配置 log
|
||||
log.transports.file.resolvePathFn = () =>
|
||||
join(app.getPath("documents"), "/SPlayer/splayer-log.txt");
|
||||
// 设置日志文件的最大大小为 10 MB
|
||||
log.transports.file.maxSize = 10 * 1024 * 1024;
|
||||
// 绑定 console.log
|
||||
console.log = log.log.bind(log);
|
||||
join(app.getPath("documents"), "/SPlayer/SPlayer-log.txt");
|
||||
// 设置日志文件的最大大小为 2 MB
|
||||
log.transports.file.maxSize = 2 * 1024 * 1024;
|
||||
// 绑定 console 事件
|
||||
console.error = log.error.bind(log);
|
||||
console.warn = log.warn.bind(log);
|
||||
console.info = log.info.bind(log);
|
||||
console.debug = log.debug.bind(log);
|
||||
|
||||
// 创建主窗口
|
||||
const createWindow = () => {
|
||||
// 创建浏览器窗口
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280, // 窗口宽度
|
||||
height: 720, // 窗口高度
|
||||
minHeight: 700, // 最小高度
|
||||
minWidth: 1200, // 最小宽度
|
||||
center: true, // 是否出现在屏幕居中的位置
|
||||
show: false, // 初始时不显示窗口
|
||||
frame: false, // 无边框
|
||||
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
|
||||
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
|
||||
// 图标配置
|
||||
icon: join(__dirname, "../../public/images/logo/favicon.png"),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
// devTools: is.dev, //是否开启 DevTools
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 窗口准备就绪时显示窗口
|
||||
mainWindow.on("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
// mainWindow.maximize();
|
||||
});
|
||||
|
||||
// 设置窗口打开处理程序
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// 渲染路径
|
||||
// 在开发模式
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||
}
|
||||
// 生产模式
|
||||
else {
|
||||
mainWindow.loadURL(`http://127.0.0.1:${import.meta.env.MAIN_VITE_MAIN_PORT ?? 7899}`);
|
||||
// 主进程
|
||||
class MainProcess {
|
||||
constructor() {
|
||||
// 主窗口
|
||||
this.mainWindow = null;
|
||||
// 主代理
|
||||
this.mainServer = null;
|
||||
// 网易云 API
|
||||
this.ncmServer = null;
|
||||
// Store
|
||||
this.store = new Store({
|
||||
// 窗口大小
|
||||
windowSize: {
|
||||
width: { type: "number", default: 1280 },
|
||||
height: { type: "number", default: 740 },
|
||||
},
|
||||
});
|
||||
// 设置应用程序名称
|
||||
if (process.platform === "win32") app.setAppUserModelId(app.getName());
|
||||
// 初始化
|
||||
this.checkApp().then(async (lockObtained) => {
|
||||
if (lockObtained) {
|
||||
await this.init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 监听关闭
|
||||
mainWindow.on("close", (event) => {
|
||||
if (!app.isQuiting) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
// 单例锁
|
||||
async checkApp() {
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
log.error("已有一个程序正在运行,本次启动阻止");
|
||||
app.quit();
|
||||
// 未获得锁
|
||||
return false;
|
||||
}
|
||||
// 聚焦到当前程序
|
||||
else {
|
||||
app.on("second-instance", () => {
|
||||
if (this.mainWindow) {
|
||||
this.mainWindow.show();
|
||||
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
|
||||
this.mainWindow.focus();
|
||||
}
|
||||
});
|
||||
// 获得锁
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化完成
|
||||
app.whenReady().then(async () => {
|
||||
// 尝试获取单例锁
|
||||
if (!gotTheLock) {
|
||||
// 如果获取不到单例锁,表示已经有一个实例在运行
|
||||
app.quit();
|
||||
log.error("已有一个程序正在运行");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 注册应用协议
|
||||
app.setAsDefaultProtocolClient("splayer");
|
||||
// 初始化程序
|
||||
async init() {
|
||||
log.info("主进程初始化");
|
||||
|
||||
// 初始化完成并准备创建浏览器窗口
|
||||
// 为 Windows 设置应用程序用户模型 ID
|
||||
electronApp.setAppUserModelId("com.electron");
|
||||
// 启动网易云 API
|
||||
try {
|
||||
this.ncmServer = await startNcmServer({
|
||||
port: import.meta.env.MAIN_VITE_SERVER_PORT,
|
||||
host: import.meta.env.MAIN_VITE_SERVER_HOST,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("启动网易云 API 失败:", error);
|
||||
}
|
||||
|
||||
// 在开发模式下默认通过 F12 打开或关闭 DevTools
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
// 非开发环境启动代理
|
||||
if (!is.dev) {
|
||||
this.mainServer = await startMainServer();
|
||||
}
|
||||
|
||||
// 注册应用协议
|
||||
app.setAsDefaultProtocolClient("SPlayer");
|
||||
// 应用程序准备好之前注册
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } },
|
||||
]);
|
||||
|
||||
// 主应用程序事件
|
||||
this.mainAppEvents();
|
||||
}
|
||||
|
||||
// 创建主窗口
|
||||
createWindow();
|
||||
createWindow() {
|
||||
// 创建浏览器窗口
|
||||
this.mainWindow = new BrowserWindow({
|
||||
title: app.getName() || "SPlayer",
|
||||
width: this.store.get("windowSize.width") || 1280, // 窗口宽度
|
||||
height: this.store.get("windowSize.height") || 740, // 窗口高度
|
||||
minHeight: 700, // 最小高度
|
||||
minWidth: 1200, // 最小宽度
|
||||
center: true, // 是否出现在屏幕居中的位置
|
||||
show: false, // 初始时不显示窗口
|
||||
frame: false, // 无边框
|
||||
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
|
||||
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
|
||||
// 图标配置
|
||||
icon: nativeImage.createFromPath(join(__dirname, "../../public/images/icons/favicon.png")),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
// devTools: is.dev,
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 创建系统信息
|
||||
createSystemInfo(mainWindow);
|
||||
// 窗口准备就绪时显示窗口
|
||||
this.mainWindow.once("ready-to-show", () => {
|
||||
this.mainWindow.show();
|
||||
// mainWindow.maximize();
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
// 主窗口事件
|
||||
this.mainWindowEvents();
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
console.log("Received custom protocol URL:", url);
|
||||
});
|
||||
// 设置窗口打开处理程序
|
||||
this.mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// 启动网易云 API
|
||||
await startNcmServer({
|
||||
port: import.meta.env.MAIN_VITE_SERVER_PORT,
|
||||
host: import.meta.env.MAIN_VITE_SERVER_HOST,
|
||||
});
|
||||
|
||||
// 引入主 Ipc
|
||||
mainIpcMain(mainWindow);
|
||||
|
||||
// 非开发环境启动代理
|
||||
if (!is.dev) await startMainServer();
|
||||
|
||||
// 注册快捷键
|
||||
createGlobalShortcut(mainWindow);
|
||||
|
||||
// 检测更新
|
||||
configureAutoUpdater(process.platform);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
app.on("will-quit", () => {
|
||||
// 注销全部快捷键
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// 当所有窗口都关闭时退出应用,macOS 除外
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
// 渲染路径
|
||||
// 在开发模式
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||
this.mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||
}
|
||||
// 生产模式
|
||||
else {
|
||||
this.mainWindow.loadURL(`http://127.0.0.1:${import.meta.env.MAIN_VITE_MAIN_PORT ?? 7899}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 主应用程序事件
|
||||
mainAppEvents() {
|
||||
app.whenReady().then(async () => {
|
||||
// 创建主窗口
|
||||
this.createWindow();
|
||||
// 检测更新
|
||||
configureAutoUpdater();
|
||||
// 引入主 Ipc
|
||||
mainIpcMain(this.mainWindow);
|
||||
// 系统托盘
|
||||
createSystemTray(this.mainWindow);
|
||||
// 注册快捷键
|
||||
createGlobalShortcut(this.mainWindow);
|
||||
});
|
||||
|
||||
// 开发环境下 F12 打开控制台
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) this.createWindow();
|
||||
});
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
console.log("Received custom protocol URL:", url);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
app.on("will-quit", () => {
|
||||
// 注销全部快捷键
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// 当所有窗口都关闭时退出应用,macOS 除外
|
||||
app.on("window-all-closed", () => {
|
||||
if (!platform.isMacOS) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 主窗口事件
|
||||
mainWindowEvents() {
|
||||
this.mainWindow.on("show", () => {
|
||||
this.mainWindow.webContents.send("lyricsScroll");
|
||||
});
|
||||
|
||||
// this.mainWindow.on("hide", () => {
|
||||
// console.info("窗口隐藏");
|
||||
// });
|
||||
|
||||
this.mainWindow.on("focus", () => {
|
||||
this.mainWindow.webContents.send("lyricsScroll");
|
||||
});
|
||||
|
||||
// this.mainWindow.on("blur", () => {
|
||||
// console.info("窗口失去焦点");
|
||||
// });
|
||||
|
||||
this.mainWindow.on("maximize", () => {
|
||||
this.mainWindow.webContents.send("windowState", true);
|
||||
});
|
||||
|
||||
this.mainWindow.on("unmaximize", () => {
|
||||
this.mainWindow.webContents.send("windowState", false);
|
||||
});
|
||||
|
||||
this.mainWindow.on("resized", () => {
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
this.mainWindow.on("moved", () => {
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
// 窗口关闭
|
||||
this.mainWindow.on("close", (event) => {
|
||||
if (platform.isLinux) {
|
||||
app.quit();
|
||||
} else {
|
||||
if (!app.isQuiting) {
|
||||
event.preventDefault();
|
||||
this.mainWindow.hide();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new MainProcess();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ipcMain, dialog, app, clipboard, shell } from "electron";
|
||||
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
|
||||
import { readDirAsync } from "@main/utils/readDirAsync";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { write } from "node-id3";
|
||||
import { download } from "electron-dl";
|
||||
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
|
||||
import axios from "axios";
|
||||
@@ -23,7 +23,7 @@ const mainIpcMain = (win) => {
|
||||
ipcMain.on("window-maxOrRestore", (ev) => {
|
||||
const winSizeState = win.isMaximized();
|
||||
winSizeState ? win.restore() : win.maximize();
|
||||
ev.reply("window-state", win.isMaximized());
|
||||
ev.reply("windowState", win.isMaximized());
|
||||
});
|
||||
ipcMain.on("window-restore", () => {
|
||||
win.restore();
|
||||
@@ -193,32 +193,39 @@ const mainIpcMain = (win) => {
|
||||
});
|
||||
|
||||
// 下载文件至指定目录
|
||||
ipcMain.handle("downloadFile", async (_, data, song, songName, songType, path) => {
|
||||
ipcMain.handle("downloadFile", async (_, songData, options) => {
|
||||
try {
|
||||
const { url, data, lyric, name, type } = JSON.parse(songData);
|
||||
const { path, downloadMeta, downloadCover, downloadLyrics } = JSON.parse(options);
|
||||
if (fs.access(path)) {
|
||||
const songData = JSON.parse(song);
|
||||
console.info("开始下载:", songData, data);
|
||||
console.info("开始下载:", name, url);
|
||||
// 下载歌曲
|
||||
const songDownload = await download(win, data.url, {
|
||||
const songDownload = await download(win, url, {
|
||||
directory: path,
|
||||
filename: `${songName}.${songType}`,
|
||||
filename: `${name}.${type}`,
|
||||
});
|
||||
// 若关闭,则不进行元信息写入
|
||||
if (!downloadMeta) return true;
|
||||
// 下载封面
|
||||
const coverDownload = await download(win, songData.cover, {
|
||||
const coverDownload = await download(win, data.cover, {
|
||||
directory: path,
|
||||
filename: `${songName}.jpg`,
|
||||
filename: `${name}.jpg`,
|
||||
});
|
||||
// 生成歌曲文件的元数据
|
||||
const songTag = {
|
||||
title: songData.name,
|
||||
artist: Array.isArray(songData.artists)
|
||||
? songData.artists.map((ar) => ar.name).join(" / ")
|
||||
: songData.artists || "未知歌手",
|
||||
album: songData.album?.name || songData.album,
|
||||
image: coverDownload.getSavePath(),
|
||||
};
|
||||
// 读取歌曲文件
|
||||
const songFile = File.createFromPath(songDownload.getSavePath());
|
||||
// 生成图片信息
|
||||
const songCover = Picture.fromPath(coverDownload.getSavePath());
|
||||
// 保存修改后的元数据
|
||||
write(songTag, songDownload.getSavePath());
|
||||
Id3v2Settings.forceDefaultVersion = true;
|
||||
Id3v2Settings.defaultVersion = 3;
|
||||
songFile.tag.title = data.name || "未知曲目";
|
||||
songFile.tag.album = data.album?.name || "未知专辑";
|
||||
songFile.tag.performers = data?.artists?.map((ar) => ar.name) || ["未知艺术家"];
|
||||
if (downloadLyrics) songFile.tag.lyrics = lyric;
|
||||
if (downloadCover) songFile.tag.pictures = [songCover];
|
||||
// 保存元信息
|
||||
songFile.save();
|
||||
songFile.dispose();
|
||||
// 删除封面
|
||||
await fs.unlink(coverDownload.getSavePath());
|
||||
return true;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const netEaseApi = require("NeteaseCloudMusicApi");
|
||||
import netEaseApi from "NeteaseCloudMusicApi";
|
||||
|
||||
/**
|
||||
* 启动网易云音乐 API 服务器
|
||||
*
|
||||
* @async
|
||||
* @param {Object} options - 服务器配置
|
||||
* @param {number} [options.port=12141] - 服务器端口
|
||||
* @param {number} [options.port=11451] - 服务器端口
|
||||
* @param {string} [options.host="127.0.0.1"] - 服务器主机地址
|
||||
* @returns {Promise<void>} 返回一个 Promise,在 API 服务器成功启动后 resolve
|
||||
*/
|
||||
|
||||
@@ -1,29 +1,65 @@
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
import { dialog } from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import pkg from "electron-updater";
|
||||
|
||||
const checkForUpdates = () => {
|
||||
autoUpdater.checkForUpdates();
|
||||
const { autoUpdater } = pkg;
|
||||
|
||||
// 更新弹窗
|
||||
const hasNewVersion = (info) => {
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: "发现新版本 v" + info.version,
|
||||
message: "发现新版本 v" + info.version,
|
||||
detail: "是否立即下载并安装新版本?",
|
||||
buttons: ["立即下载", "取消"],
|
||||
type: "question",
|
||||
noLink: true,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
// 触发手动下载
|
||||
autoUpdater.downloadUpdate();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const configureAutoUpdater = () => {
|
||||
checkForUpdates();
|
||||
if (is.dev) return false;
|
||||
|
||||
// 监听检查更新的事件
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
console.log("Checking for update...");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
console.log("Update available:", info);
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
console.log("Update not available.");
|
||||
// 监听下载进度事件
|
||||
autoUpdater.on("download-progress", (progressObj) => {
|
||||
console.log(`更新下载进度: ${progressObj.percent}%`);
|
||||
});
|
||||
|
||||
// 下载完成
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
console.log("Update downloaded. Ready to install.");
|
||||
|
||||
// 在需要的时候,触发安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
// 显示安装弹窗
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: "下载完成",
|
||||
message: "新版本已下载完成,是否现在安装?",
|
||||
buttons: ["是", "稍后"],
|
||||
type: "question",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
// 安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 下载失败
|
||||
autoUpdater.on("error", (err) => {
|
||||
console.error("下载更新失败:", err);
|
||||
dialog.showErrorBox("下载更新失败", "请检查网络连接并稍后重试!");
|
||||
});
|
||||
|
||||
// 若有更新
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
hasNewVersion(info);
|
||||
});
|
||||
|
||||
// 检查更新
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
};
|
||||
|
||||
@@ -6,8 +6,18 @@ import { globalShortcut } from "electron";
|
||||
*/
|
||||
const createGlobalShortcut = (win) => {
|
||||
// 刷新程序
|
||||
globalShortcut.register("CommandOrControl+R", () => {
|
||||
win?.reload();
|
||||
globalShortcut.register("CmdOrCtrl+Shift+R", () => {
|
||||
if (win && win.isFocused()) win?.reload();
|
||||
});
|
||||
|
||||
// 打开开发者工具
|
||||
globalShortcut.register("CmdOrCtrl+Shift+I", () => {
|
||||
if (win && win.isFocused()) {
|
||||
win?.webContents.openDevTools({
|
||||
mode: "right",
|
||||
activate: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,59 +1,78 @@
|
||||
import { join } from "path";
|
||||
import { Tray, Menu, app, ipcMain, nativeImage, nativeTheme } from "electron";
|
||||
import { join } from "path";
|
||||
|
||||
// 当前播放歌曲数据
|
||||
// 当前歌曲数据
|
||||
let playSongName = "当前暂无播放歌曲";
|
||||
let playSongState = false;
|
||||
|
||||
/**
|
||||
* 创建系统自定义信息
|
||||
* 创建系统托盘
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createSystemInfo = (win) => {
|
||||
// 弹出列表
|
||||
app.setUserTasks([]);
|
||||
const createSystemTray = (win) => {
|
||||
// 系统托盘
|
||||
const mainTray = new Tray(join(__dirname, "../../public/images/logo/favicon.png"));
|
||||
// 给托盘图标设置气球提示
|
||||
const mainTray = new Tray(
|
||||
nativeImage
|
||||
.createFromPath(
|
||||
join(
|
||||
__dirname,
|
||||
process.platform === "win32"
|
||||
? "../../public/images/icons/favicon.ico"
|
||||
: "../../public/images/icons/favicon-32x32.png",
|
||||
),
|
||||
)
|
||||
.resize({
|
||||
height: 32,
|
||||
width: 32,
|
||||
}),
|
||||
);
|
||||
// 应用内菜单
|
||||
Menu.setApplicationMenu(createTrayMenu(win));
|
||||
// 默认名称
|
||||
win.setTitle(app.getName());
|
||||
mainTray.setTitle(app.getName());
|
||||
mainTray.setToolTip(app.getName());
|
||||
// 歌曲数据改变时
|
||||
// 左键事件
|
||||
mainTray.on("click", () => win.show());
|
||||
// 托盘菜单
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
// 系统主题改变
|
||||
nativeTheme.on("updated", () => {
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 播放歌曲改变
|
||||
ipcMain.on("songNameChange", (_, val) => {
|
||||
playSongName = val;
|
||||
// 托盘图标标题
|
||||
mainTray.setToolTip(val);
|
||||
// 更改应用标题
|
||||
win.setTitle(val);
|
||||
mainTray.setTitle(val);
|
||||
mainTray.setToolTip(val);
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 播放状态改变
|
||||
ipcMain.on("songStateChange", (_, val) => {
|
||||
playSongState = val;
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 左键事件
|
||||
mainTray.on("click", () => {
|
||||
// 显示窗口
|
||||
win.show();
|
||||
});
|
||||
// 右键事件
|
||||
mainTray.on("right-click", () => {
|
||||
mainTray.popUpContextMenu(Menu.buildFromTemplate(createTrayMenu(win)));
|
||||
});
|
||||
};
|
||||
|
||||
// 生成图标
|
||||
const createIcon = (name) => {
|
||||
// 系统是否为暗色
|
||||
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
||||
// 返回图标
|
||||
return nativeImage
|
||||
.createFromPath(
|
||||
isDarkMode
|
||||
? join(__dirname, `../../public/images/icons/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/images/icons/${name}-light.png`),
|
||||
)
|
||||
.resize({ width: 16, height: 16 });
|
||||
};
|
||||
|
||||
// 生成右键菜单
|
||||
const createTrayMenu = (win) => {
|
||||
// 系统是否为暗色
|
||||
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
||||
// 生成图标
|
||||
const createIcon = (name) => {
|
||||
return nativeImage
|
||||
.createFromPath(
|
||||
isDarkMode
|
||||
? join(__dirname, `../../public/images/icon/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/images/icon/${name}-light.png`),
|
||||
)
|
||||
.resize({ width: 16, height: 16 });
|
||||
};
|
||||
// 返回菜单
|
||||
return [
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: playSongName,
|
||||
icon: createIcon("open"),
|
||||
@@ -69,6 +88,7 @@ const createTrayMenu = (win) => {
|
||||
{
|
||||
label: "上一曲",
|
||||
icon: createIcon("prev"),
|
||||
accelerator: "CmdOrCtrl+Left",
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "prev");
|
||||
},
|
||||
@@ -76,6 +96,7 @@ const createTrayMenu = (win) => {
|
||||
{
|
||||
label: playSongState ? "暂停" : "播放",
|
||||
icon: createIcon(playSongState ? "pause" : "play"),
|
||||
accelerator: "CmdOrCtrl+Space",
|
||||
click: () => {
|
||||
win.webContents.send("playOrPause");
|
||||
},
|
||||
@@ -83,6 +104,7 @@ const createTrayMenu = (win) => {
|
||||
{
|
||||
label: "下一曲",
|
||||
icon: createIcon("next"),
|
||||
accelerator: "CmdOrCtrl+Right",
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "next");
|
||||
},
|
||||
@@ -94,7 +116,9 @@ const createTrayMenu = (win) => {
|
||||
label: "全局设置",
|
||||
icon: createIcon("setting"),
|
||||
click: () => {
|
||||
win.webContents.send("setting");
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("open-setting");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -109,7 +133,7 @@ const createTrayMenu = (win) => {
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
];
|
||||
]);
|
||||
};
|
||||
|
||||
export default createSystemInfo;
|
||||
export default createSystemTray;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { encryptQuery } from "@main/utils/kwDES";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* 网易云音乐解灰
|
||||
* 目前音源采用 咪咕音乐
|
||||
*/
|
||||
|
||||
// 请求头
|
||||
// 咪咕音乐请求头
|
||||
const requestHeader = {
|
||||
Origin: "http://music.migu.cn/",
|
||||
Referer: "http://m.music.migu.cn/v3/",
|
||||
@@ -19,48 +19,149 @@ const requestHeader = {
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getMiguSongId = async (keyword) => {
|
||||
const url =
|
||||
"https://m.music.migu.cn/migu/remoting/scr_search_tag?keyword=" +
|
||||
keyword.toString() +
|
||||
"&type=2&rows=20&pgc=1";
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.musics?.length) {
|
||||
console.log(result.data.musics[0]);
|
||||
const oldName = keyword.split("-");
|
||||
const songName = result.data.musics[0]?.songName;
|
||||
if (songName && !songName?.includes(oldName[0])) {
|
||||
console.log("匹配失败");
|
||||
try {
|
||||
const url =
|
||||
"https://m.music.migu.cn/migu/remoting/scr_search_tag?keyword=" +
|
||||
keyword.toString() +
|
||||
"&type=2&rows=20&pgc=1";
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.musics?.length) {
|
||||
// 是否与原曲吻合
|
||||
const originalName = keyword.split("-");
|
||||
const songName = result.data.musics[0]?.songName;
|
||||
if (songName && !songName?.includes(originalName[0])) {
|
||||
return null;
|
||||
}
|
||||
return result.data.musics[0].id;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取咪咕音乐歌曲 ID 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取咪咕音乐歌曲 URL
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getMiguSongUrl = async (keyword) => {
|
||||
try {
|
||||
const songId = await getMiguSongId(keyword);
|
||||
if (!songId) return null;
|
||||
console.info("咪咕解灰歌曲 ID:", songId);
|
||||
const soundQuality = "PQ";
|
||||
const url =
|
||||
"https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?netType=01&resourceType=2&songId=" +
|
||||
songId.toString() +
|
||||
"&toneFlag=" +
|
||||
soundQuality;
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.data?.url) {
|
||||
const songUrl = result.data.data.url;
|
||||
console.info("咪咕解灰歌曲 URL:", songUrl);
|
||||
return songUrl;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取咪咕音乐歌曲 URL 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取酷我音乐歌曲 ID
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getKuwoSongId = async (keyword) => {
|
||||
try {
|
||||
const url =
|
||||
"http://search.kuwo.cn/r.s?&correct=1&stype=comprehensive&encoding=utf8" +
|
||||
"&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
|
||||
keyword.toString();
|
||||
const result = await axios.get(url);
|
||||
if (
|
||||
!result.data ||
|
||||
result.data.content.length < 2 ||
|
||||
!result.data.content[1].musicpage ||
|
||||
result.data.content[1].musicpage.abslist.length < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return result.data.musics[0].id;
|
||||
// 是否与原曲吻合
|
||||
const originalName = keyword.split("-");
|
||||
const songName = result.data.content[1].musicpage.abslist[0]?.SONGNAME;
|
||||
if (songName && !songName?.includes(originalName[0])) {
|
||||
return null;
|
||||
}
|
||||
const songId = result.data.content[1].musicpage.abslist[0].MUSICRID;
|
||||
return songId.slice("MUSIC_".length);
|
||||
} catch (error) {
|
||||
console.error("获取酷我音乐歌曲 ID 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取酷我音乐歌曲 URL
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getKuwoSongUrl = async (keyword) => {
|
||||
try {
|
||||
const songId = await getKuwoSongId(keyword);
|
||||
if (!songId) return null;
|
||||
console.info("酷我解灰歌曲 ID:", songId);
|
||||
const url = encryptQuery
|
||||
? "http://mobi.kuwo.cn/mobi.s?f=kuwo&q=" +
|
||||
encryptQuery(
|
||||
"corp=kuwo&source=kwplayer_ar_8.5.5.0_apk_keluze.apk&p2p=1&type=convert_url2&sig=0&format=mp3" +
|
||||
"&rid=" +
|
||||
songId,
|
||||
)
|
||||
: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_" +
|
||||
songId;
|
||||
const result = await axios.get(url, { "user-agent": "okhttp/3.10.0" });
|
||||
if (result.data) {
|
||||
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
|
||||
console.info("酷我解灰歌曲 URL:", urlMatch);
|
||||
return urlMatch;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取酷我音乐歌曲 URL 失败:", error);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取给定关键字的音乐 URL
|
||||
* @param {string} keyword - 关键字
|
||||
* @returns {Promise<?string>} 如果找到,则解析为音乐 URL 的 Promise;如果未找到,则为 null
|
||||
* @throws {Error} 抛出错误
|
||||
* @returns {Promise<?string>} 音乐 URL
|
||||
*/
|
||||
const getNeteaseMusicUrl = async (keyword) => {
|
||||
const songId = await getMiguSongId(keyword);
|
||||
if (!songId) return null;
|
||||
const soundQuality = "PQ";
|
||||
const url =
|
||||
"https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?netType=01&resourceType=2&songId=" +
|
||||
songId.toString() +
|
||||
"&toneFlag=" +
|
||||
soundQuality;
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.data?.url) {
|
||||
return result.data.data.url;
|
||||
try {
|
||||
const [kuwoSongUrl, miguSongUrl] = await Promise.all([
|
||||
getKuwoSongUrl(keyword),
|
||||
getMiguSongUrl(keyword),
|
||||
]);
|
||||
if (kuwoSongUrl) {
|
||||
return kuwoSongUrl;
|
||||
}
|
||||
if (miguSongUrl) {
|
||||
return miguSongUrl;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取解灰 URL 全部失败:", error);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default getNeteaseMusicUrl;
|
||||
|
||||
574
electron/main/utils/kwDES.js
Normal file
@@ -0,0 +1,574 @@
|
||||
/* eslint-disable no-undef */
|
||||
/*
|
||||
Thanks to
|
||||
https://github.com/XuShaohua/kwplayer/blob/master/kuwo/DES.py
|
||||
https://github.com/Levi233/MusicPlayer/blob/master/app/src/main/java/com/chenhao/musicplayer/utils/crypt/KuwoDES.java
|
||||
*/
|
||||
|
||||
const Long = (n) => {
|
||||
const bN = BigInt(n);
|
||||
|
||||
return {
|
||||
low: Number(bN),
|
||||
valueOf: () => bN.valueOf(),
|
||||
toString: () => bN.toString(),
|
||||
not: () => Long(~bN),
|
||||
isNegative: () => bN < 0,
|
||||
or: (x) => Long(bN | BigInt(x)),
|
||||
and: (x) => Long(bN & BigInt(x)),
|
||||
xor: (x) => Long(bN ^ BigInt(x)),
|
||||
equals: (x) => bN === BigInt(x),
|
||||
multiply: (x) => Long(bN * BigInt(x)),
|
||||
shiftLeft: (x) => Long(bN << BigInt(x)),
|
||||
shiftRight: (x) => Long(bN >> BigInt(x)),
|
||||
};
|
||||
};
|
||||
|
||||
const range = (n) => Array.from(new Array(n).keys());
|
||||
const power = (base, index) =>
|
||||
Array(index)
|
||||
.fill(null)
|
||||
.reduce((result) => result.multiply(base), Long(1));
|
||||
const LongArray = (...array) => array.map((n) => (n === -1 ? Long(-1, -1) : Long(n)));
|
||||
|
||||
// EXPANSION
|
||||
const arrayE = LongArray(
|
||||
31,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
-1,
|
||||
-1,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
-1,
|
||||
-1,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
-1,
|
||||
-1,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
-1,
|
||||
-1,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
-1,
|
||||
-1,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
-1,
|
||||
-1,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
-1,
|
||||
-1,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
-1,
|
||||
-1,
|
||||
);
|
||||
|
||||
// INITIAL_PERMUTATION
|
||||
const arrayIP = LongArray(
|
||||
57,
|
||||
49,
|
||||
41,
|
||||
33,
|
||||
25,
|
||||
17,
|
||||
9,
|
||||
1,
|
||||
59,
|
||||
51,
|
||||
43,
|
||||
35,
|
||||
27,
|
||||
19,
|
||||
11,
|
||||
3,
|
||||
61,
|
||||
53,
|
||||
45,
|
||||
37,
|
||||
29,
|
||||
21,
|
||||
13,
|
||||
5,
|
||||
63,
|
||||
55,
|
||||
47,
|
||||
39,
|
||||
31,
|
||||
23,
|
||||
15,
|
||||
7,
|
||||
56,
|
||||
48,
|
||||
40,
|
||||
32,
|
||||
24,
|
||||
16,
|
||||
8,
|
||||
0,
|
||||
58,
|
||||
50,
|
||||
42,
|
||||
34,
|
||||
26,
|
||||
18,
|
||||
10,
|
||||
2,
|
||||
60,
|
||||
52,
|
||||
44,
|
||||
36,
|
||||
28,
|
||||
20,
|
||||
12,
|
||||
4,
|
||||
62,
|
||||
54,
|
||||
46,
|
||||
38,
|
||||
30,
|
||||
22,
|
||||
14,
|
||||
6,
|
||||
);
|
||||
|
||||
// INVERSE_PERMUTATION
|
||||
const arrayIP_1 = LongArray(
|
||||
39,
|
||||
7,
|
||||
47,
|
||||
15,
|
||||
55,
|
||||
23,
|
||||
63,
|
||||
31,
|
||||
38,
|
||||
6,
|
||||
46,
|
||||
14,
|
||||
54,
|
||||
22,
|
||||
62,
|
||||
30,
|
||||
37,
|
||||
5,
|
||||
45,
|
||||
13,
|
||||
53,
|
||||
21,
|
||||
61,
|
||||
29,
|
||||
36,
|
||||
4,
|
||||
44,
|
||||
12,
|
||||
52,
|
||||
20,
|
||||
60,
|
||||
28,
|
||||
35,
|
||||
3,
|
||||
43,
|
||||
11,
|
||||
51,
|
||||
19,
|
||||
59,
|
||||
27,
|
||||
34,
|
||||
2,
|
||||
42,
|
||||
10,
|
||||
50,
|
||||
18,
|
||||
58,
|
||||
26,
|
||||
33,
|
||||
1,
|
||||
41,
|
||||
9,
|
||||
49,
|
||||
17,
|
||||
57,
|
||||
25,
|
||||
32,
|
||||
0,
|
||||
40,
|
||||
8,
|
||||
48,
|
||||
16,
|
||||
56,
|
||||
24,
|
||||
);
|
||||
|
||||
// ROTATES
|
||||
const arrayLs = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
|
||||
const arrayLsMask = LongArray(0, 0x100001, 0x300003);
|
||||
const arrayMask = range(64).map((n) => power(2, n));
|
||||
arrayMask[arrayMask.length - 1] = arrayMask[arrayMask.length - 1].multiply(-1);
|
||||
|
||||
// PERMUTATION
|
||||
const arrayP = LongArray(
|
||||
15,
|
||||
6,
|
||||
19,
|
||||
20,
|
||||
28,
|
||||
11,
|
||||
27,
|
||||
16,
|
||||
0,
|
||||
14,
|
||||
22,
|
||||
25,
|
||||
4,
|
||||
17,
|
||||
30,
|
||||
9,
|
||||
1,
|
||||
7,
|
||||
23,
|
||||
13,
|
||||
31,
|
||||
26,
|
||||
2,
|
||||
8,
|
||||
18,
|
||||
12,
|
||||
29,
|
||||
5,
|
||||
21,
|
||||
10,
|
||||
3,
|
||||
24,
|
||||
);
|
||||
|
||||
// PERMUTED_CHOICE1
|
||||
const arrayPC_1 = LongArray(
|
||||
56,
|
||||
48,
|
||||
40,
|
||||
32,
|
||||
24,
|
||||
16,
|
||||
8,
|
||||
0,
|
||||
57,
|
||||
49,
|
||||
41,
|
||||
33,
|
||||
25,
|
||||
17,
|
||||
9,
|
||||
1,
|
||||
58,
|
||||
50,
|
||||
42,
|
||||
34,
|
||||
26,
|
||||
18,
|
||||
10,
|
||||
2,
|
||||
59,
|
||||
51,
|
||||
43,
|
||||
35,
|
||||
62,
|
||||
54,
|
||||
46,
|
||||
38,
|
||||
30,
|
||||
22,
|
||||
14,
|
||||
6,
|
||||
61,
|
||||
53,
|
||||
45,
|
||||
37,
|
||||
29,
|
||||
21,
|
||||
13,
|
||||
5,
|
||||
60,
|
||||
52,
|
||||
44,
|
||||
36,
|
||||
28,
|
||||
20,
|
||||
12,
|
||||
4,
|
||||
27,
|
||||
19,
|
||||
11,
|
||||
3,
|
||||
);
|
||||
|
||||
// PERMUTED_CHOICE2
|
||||
const arrayPC_2 = LongArray(
|
||||
13,
|
||||
16,
|
||||
10,
|
||||
23,
|
||||
0,
|
||||
4,
|
||||
-1,
|
||||
-1,
|
||||
2,
|
||||
27,
|
||||
14,
|
||||
5,
|
||||
20,
|
||||
9,
|
||||
-1,
|
||||
-1,
|
||||
22,
|
||||
18,
|
||||
11,
|
||||
3,
|
||||
25,
|
||||
7,
|
||||
-1,
|
||||
-1,
|
||||
15,
|
||||
6,
|
||||
26,
|
||||
19,
|
||||
12,
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
40,
|
||||
51,
|
||||
30,
|
||||
36,
|
||||
46,
|
||||
54,
|
||||
-1,
|
||||
-1,
|
||||
29,
|
||||
39,
|
||||
50,
|
||||
44,
|
||||
32,
|
||||
47,
|
||||
-1,
|
||||
-1,
|
||||
43,
|
||||
48,
|
||||
38,
|
||||
55,
|
||||
33,
|
||||
52,
|
||||
-1,
|
||||
-1,
|
||||
45,
|
||||
41,
|
||||
49,
|
||||
35,
|
||||
28,
|
||||
31,
|
||||
-1,
|
||||
-1,
|
||||
);
|
||||
|
||||
const matrixNSBox = [
|
||||
[
|
||||
14, 4, 3, 15, 2, 13, 5, 3, 13, 14, 6, 9, 11, 2, 0, 5, 4, 1, 10, 12, 15, 6, 9, 10, 1, 8, 12, 7,
|
||||
8, 11, 7, 0, 0, 15, 10, 5, 14, 4, 9, 10, 7, 8, 12, 3, 13, 1, 3, 6, 15, 12, 6, 11, 2, 9, 5, 0, 4,
|
||||
2, 11, 14, 1, 7, 8, 13,
|
||||
],
|
||||
[
|
||||
15, 0, 9, 5, 6, 10, 12, 9, 8, 7, 2, 12, 3, 13, 5, 2, 1, 14, 7, 8, 11, 4, 0, 3, 14, 11, 13, 6, 4,
|
||||
1, 10, 15, 3, 13, 12, 11, 15, 3, 6, 0, 4, 10, 1, 7, 8, 4, 11, 14, 13, 8, 0, 6, 2, 15, 9, 5, 7,
|
||||
1, 10, 12, 14, 2, 5, 9,
|
||||
],
|
||||
[
|
||||
10, 13, 1, 11, 6, 8, 11, 5, 9, 4, 12, 2, 15, 3, 2, 14, 0, 6, 13, 1, 3, 15, 4, 10, 14, 9, 7, 12,
|
||||
5, 0, 8, 7, 13, 1, 2, 4, 3, 6, 12, 11, 0, 13, 5, 14, 6, 8, 15, 2, 7, 10, 8, 15, 4, 9, 11, 5, 9,
|
||||
0, 14, 3, 10, 7, 1, 12,
|
||||
],
|
||||
[
|
||||
7, 10, 1, 15, 0, 12, 11, 5, 14, 9, 8, 3, 9, 7, 4, 8, 13, 6, 2, 1, 6, 11, 12, 2, 3, 0, 5, 14, 10,
|
||||
13, 15, 4, 13, 3, 4, 9, 6, 10, 1, 12, 11, 0, 2, 5, 0, 13, 14, 2, 8, 15, 7, 4, 15, 1, 10, 7, 5,
|
||||
6, 12, 11, 3, 8, 9, 14,
|
||||
],
|
||||
[
|
||||
2, 4, 8, 15, 7, 10, 13, 6, 4, 1, 3, 12, 11, 7, 14, 0, 12, 2, 5, 9, 10, 13, 0, 3, 1, 11, 15, 5,
|
||||
6, 8, 9, 14, 14, 11, 5, 6, 4, 1, 3, 10, 2, 12, 15, 0, 13, 2, 8, 5, 11, 8, 0, 15, 7, 14, 9, 4,
|
||||
12, 7, 10, 9, 1, 13, 6, 3,
|
||||
],
|
||||
[
|
||||
12, 9, 0, 7, 9, 2, 14, 1, 10, 15, 3, 4, 6, 12, 5, 11, 1, 14, 13, 0, 2, 8, 7, 13, 15, 5, 4, 10,
|
||||
8, 3, 11, 6, 10, 4, 6, 11, 7, 9, 0, 6, 4, 2, 13, 1, 9, 15, 3, 8, 15, 3, 1, 14, 12, 5, 11, 0, 2,
|
||||
12, 14, 7, 5, 10, 8, 13,
|
||||
],
|
||||
[
|
||||
4, 1, 3, 10, 15, 12, 5, 0, 2, 11, 9, 6, 8, 7, 6, 9, 11, 4, 12, 15, 0, 3, 10, 5, 14, 13, 7, 8,
|
||||
13, 14, 1, 2, 13, 6, 14, 9, 4, 1, 2, 14, 11, 13, 5, 0, 1, 10, 8, 3, 0, 11, 3, 5, 9, 4, 15, 2, 7,
|
||||
8, 12, 15, 10, 7, 6, 12,
|
||||
],
|
||||
[
|
||||
13, 7, 10, 0, 6, 9, 5, 15, 8, 4, 3, 10, 11, 14, 12, 5, 2, 11, 9, 6, 15, 12, 0, 3, 4, 1, 14, 13,
|
||||
1, 2, 7, 8, 1, 2, 12, 15, 10, 4, 0, 3, 13, 14, 6, 9, 7, 8, 9, 6, 15, 1, 5, 12, 3, 10, 14, 5, 8,
|
||||
7, 11, 0, 4, 13, 2, 11,
|
||||
],
|
||||
];
|
||||
|
||||
const bitTransform = (arrInt, n, l) => {
|
||||
// int[], int, long : long
|
||||
let l2 = Long(0);
|
||||
range(n).forEach((i) => {
|
||||
if (arrInt[i].isNegative() || l.and(arrayMask[arrInt[i].low]).equals(0)) return;
|
||||
l2 = l2.or(arrayMask[i]);
|
||||
});
|
||||
return l2;
|
||||
};
|
||||
|
||||
const DES64 = (longs, l) => {
|
||||
const pR = range(8).map(() => Long(0));
|
||||
const pSource = [Long(0), Long(0)];
|
||||
let L = Long(0);
|
||||
let R = Long(0);
|
||||
let out = bitTransform(arrayIP, 64, l);
|
||||
pSource[0] = out.and(0xffffffff);
|
||||
pSource[1] = out.and(-4294967296).shiftRight(32);
|
||||
|
||||
range(16).forEach((i) => {
|
||||
let SOut = Long(0);
|
||||
|
||||
R = Long(pSource[1]);
|
||||
R = bitTransform(arrayE, 64, R);
|
||||
R = R.xor(longs[i]);
|
||||
range(8).forEach((j) => {
|
||||
pR[j] = R.shiftRight(j * 8).and(255);
|
||||
});
|
||||
range(8)
|
||||
.reverse()
|
||||
.forEach((sbi) => {
|
||||
SOut = SOut.shiftLeft(4).or(matrixNSBox[sbi][pR[sbi]]);
|
||||
});
|
||||
R = bitTransform(arrayP, 32, SOut);
|
||||
L = Long(pSource[0]);
|
||||
pSource[0] = Long(pSource[1]);
|
||||
pSource[1] = L.xor(R);
|
||||
});
|
||||
pSource.reverse();
|
||||
out = pSource[1].shiftLeft(32).and(-4294967296).or(pSource[0].and(0xffffffff));
|
||||
out = bitTransform(arrayIP_1, 64, out);
|
||||
return out;
|
||||
};
|
||||
|
||||
const subKeys = (l, longs, n) => {
|
||||
// long, long[], int
|
||||
let l2 = bitTransform(arrayPC_1, 56, l);
|
||||
range(16).forEach((i) => {
|
||||
l2 = l2
|
||||
.and(arrayLsMask[arrayLs[i]])
|
||||
.shiftLeft(28 - arrayLs[i])
|
||||
.or(l2.and(arrayLsMask[arrayLs[i]].not()).shiftRight(arrayLs[i]));
|
||||
longs[i] = bitTransform(arrayPC_2, 64, l2);
|
||||
});
|
||||
if (n === 1) {
|
||||
range(8).forEach((j) => {
|
||||
[longs[j], longs[15 - j]] = [longs[15 - j], longs[j]];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const crypt = (msg, key, mode) => {
|
||||
// 处理密钥块
|
||||
let l = Long(0);
|
||||
range(8).forEach((i) => {
|
||||
l = Long(key[i])
|
||||
.shiftLeft(i * 8)
|
||||
.or(l);
|
||||
});
|
||||
|
||||
const j = Math.floor(msg.length / 8);
|
||||
// arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了
|
||||
|
||||
const arrLong1 = range(16).map(() => Long(0));
|
||||
subKeys(l, arrLong1, mode);
|
||||
|
||||
// arrLong2 存放的是前部分的明文
|
||||
const arrLong2 = range(j).map(() => Long(0));
|
||||
|
||||
range(j).forEach((m) => {
|
||||
range(8).forEach((n) => {
|
||||
arrLong2[m] = Long(msg[n + m * 8])
|
||||
.shiftLeft(n * 8)
|
||||
.or(arrLong2[m]);
|
||||
});
|
||||
});
|
||||
|
||||
// 用于存放密文
|
||||
const arrLong3 = range(Math.floor((1 + 8 * (j + 1)) / 8)).map(() => Long(0));
|
||||
|
||||
// 计算前部的数据块(除了最后一部分)
|
||||
range(j).forEach((i1) => {
|
||||
arrLong3[i1] = DES64(arrLong1, arrLong2[i1]);
|
||||
});
|
||||
|
||||
// 保存多出来的字节
|
||||
const arrByte1 = msg.slice(j * 8);
|
||||
let l2 = Long(0);
|
||||
|
||||
range(msg.length % 8).forEach((i1) => {
|
||||
l2 = Long(arrByte1[i1])
|
||||
.shiftLeft(i1 * 8)
|
||||
.or(l2);
|
||||
});
|
||||
|
||||
// 计算多出的那一位(最后一位)
|
||||
if (arrByte1.length || mode === 0) arrLong3[j] = DES64(arrLong1, l2); // 解密不需要
|
||||
|
||||
// 将密文转为字节型
|
||||
const arrByte2 = range(8 * arrLong3.length).map(() => 0);
|
||||
let i4 = 0;
|
||||
arrLong3.forEach((l3) => {
|
||||
range(8).forEach((i6) => {
|
||||
arrByte2[i4] = l3.shiftRight(i6 * 8).and(255).low;
|
||||
i4 += 1;
|
||||
});
|
||||
});
|
||||
return Buffer.from(arrByte2);
|
||||
};
|
||||
|
||||
const SECRET_KEY = Buffer.from("ylzsxkwm");
|
||||
export const encrypt = (msg) => crypt(msg, SECRET_KEY, 0);
|
||||
export const decrypt = (msg) => crypt(msg, SECRET_KEY, 1);
|
||||
export const encryptQuery = (query) => encrypt(Buffer.from(query)).toString("base64");
|
||||
39
index.html
@@ -1,24 +1,23 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%RENDERER_VITE_SITE_TITLE%</title>
|
||||
<meta name="apple-mobile-web-app-title" content="%RENDERER_VITE_SITE_TITLE%" />
|
||||
<meta name="author" content="%RENDERER_VITE_SITE_ANTHOR%" />
|
||||
<meta name="keywords" content="%RENDERER_VITE_SITE_KEYWORDS%" />
|
||||
<meta name="description" content="%RENDERER_VITE_SITE_DES%" />
|
||||
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="%RENDERER_VITE_SITE_LOGO%" />
|
||||
<link rel="apple-touch-icon" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="bookmark" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="200x200" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%RENDERER_VITE_SITE_TITLE%</title>
|
||||
<meta name="apple-mobile-web-app-title" content="%RENDERER_VITE_SITE_TITLE%" />
|
||||
<meta name="author" content="%RENDERER_VITE_SITE_ANTHOR%" />
|
||||
<meta name="keywords" content="%RENDERER_VITE_SITE_KEYWORDS%" />
|
||||
<meta name="description" content="%RENDERER_VITE_SITE_DES%" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
28
nginx.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
server {
|
||||
gzip on;
|
||||
listen 7899;
|
||||
listen [::]:7899;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location @rewrites {
|
||||
rewrite ^(.*)$ /index.html last;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_buffers 16 32k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_size 128k;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $remote_addr;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
}
|
||||
80
package.json
@@ -1,19 +1,25 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "2.0.0-beta.2",
|
||||
"version": "2.0.5",
|
||||
"description": "A minimalist music player",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "imsyy",
|
||||
"home": "https://imsyy.top",
|
||||
"github": "https://github.com/imsyy/SPlayer",
|
||||
"repository": "github:imsyy/SPlayer",
|
||||
"license": "AGPL-3.0",
|
||||
"license-file": "LICENSE",
|
||||
"engines": {
|
||||
"node": ">=16.16.0"
|
||||
"node": ">=18.16.0",
|
||||
"npm": ">=9.6.7",
|
||||
"pnpm": ">=8.14.0"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "chcp 65001 && electron-vite dev --watch",
|
||||
"dev": "electron-vite dev --watch",
|
||||
"build": "electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:win": "npm run build && electron-builder --win --config",
|
||||
@@ -21,48 +27,50 @@
|
||||
"build:linux": "npm run build && electron-builder --linux --config"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^2.0.0",
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"NeteaseCloudMusicApi": "^4.13.5",
|
||||
"axios": "^1.4.0",
|
||||
"NeteaseCloudMusicApi": "^4.15.6",
|
||||
"axios": "^1.6.7",
|
||||
"colorthief": "^2.4.0",
|
||||
"electron-dl": "^3.5.1",
|
||||
"electron-updater": "^6.1.7",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^1.6.3",
|
||||
"howler": "^2.2.3",
|
||||
"electron-dl": "^3.5.2",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.1.8",
|
||||
"express": "^4.18.3",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"howler": "^2.2.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"localforage": "^1.10.0",
|
||||
"music-metadata": "7.13.4",
|
||||
"node-id3": "^0.2.6",
|
||||
"pinia": "^2.1.6",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"music-metadata": "7.14.0",
|
||||
"node-taglib-sharp": "^5.2.3",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"plyr": "^3.7.8",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue-slider-component": "4.1.0-beta.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.1",
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"electron": "^25.6.0",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-log": "^5.0.1",
|
||||
"electron-vite": "^1.0.27",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"naive-ui": "^2.34.4",
|
||||
"prettier": "^3.0.2",
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.19.2",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.9",
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
"@rushstack/eslint-patch": "^1.7.2",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"ajv": "^8.12.0",
|
||||
"electron": "^28.2.7",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-log": "^5.1.2",
|
||||
"electron-vite": "^2.1.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"naive-ui": "^2.38.1",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.72.0",
|
||||
"terser": "^5.29.1",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.1.6",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "^3.3.4"
|
||||
"vite-plugin-pwa": "^0.17.5",
|
||||
"vue": "3.4.8"
|
||||
}
|
||||
}
|
||||
|
||||
4614
pnpm-lock.yaml
generated
724
public/font/font.min.css
vendored
BIN
public/images/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/images/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/images/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/images/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/icons/favicon-192x192.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/images/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/images/icons/favicon-512x512.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/images/icons/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/images/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/icons/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
25
public/images/icons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2400 4444 c-14 -2 -63 -8 -110 -14 -407 -50 -737 -184 -1077 -436
|
||||
-124 -93 -348 -309 -406 -394 -100 -144 -190 -286 -223 -352 -101 -200 -199
|
||||
-506 -226 -707 -17 -127 -22 -436 -8 -546 40 -338 161 -687 327 -942 138 -213
|
||||
226 -319 372 -450 237 -211 471 -353 761 -460 147 -54 156 -57 330 -88 383
|
||||
-71 516 -67 965 25 76 16 288 91 370 132 28 13 66 31 85 38 39 16 158 88 270
|
||||
164 242 164 474 410 628 668 254 422 362 920 308 1408 -16 139 -23 172 -73
|
||||
344 -80 273 -188 496 -346 711 -104 143 -302 348 -404 421 l-48 34 -192 0
|
||||
c-184 0 -193 -1 -203 -21 -8 -14 -11 -271 -10 -882 1 -474 -1 -896 -5 -937
|
||||
-29 -306 -228 -596 -507 -737 -207 -104 -426 -126 -663 -66 -168 43 -300 125
|
||||
-453 283 -118 122 -203 310 -223 495 -10 86 -3 271 11 320 41 135 58 180 95
|
||||
248 103 193 330 376 545 441 157 47 364 49 513 5 38 -11 78 -19 90 -17 l22 3
|
||||
-3 635 c-2 349 -7 638 -10 642 -22 22 -413 47 -502 32z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 55 KiB |
BIN
public/images/pic/artist.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
BIN
public/images/pic/pointer.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/images/pic/record.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/pic/video.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
screenshots/SPlayer.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 524 KiB |
157
src/App.vue
@@ -1,33 +1,33 @@
|
||||
<template>
|
||||
<Provider>
|
||||
<!-- 主框架 -->
|
||||
<n-layout :class="['all-layout', status.showFullPlayer ? 'full-player' : null]">
|
||||
<n-layout :class="['all-layout', { 'full-player': showFullPlayer }]">
|
||||
<!-- 导航栏 -->
|
||||
<n-layout-header bordered>
|
||||
<MainNav />
|
||||
<TitleBar v-if="checkPlatform.electron()" />
|
||||
</n-layout-header>
|
||||
<!-- 主内容 -->
|
||||
<!-- 主内容 - 有侧边栏 -->
|
||||
<n-layout
|
||||
v-if="showSider"
|
||||
:class="{
|
||||
'body-layout': true,
|
||||
'player-bar': Object.keys(music.playSongData)?.length && status.showPlayBar,
|
||||
'player-bar': music.getPlaySongData?.id && showPlayBar,
|
||||
}"
|
||||
position="absolute"
|
||||
has-sider
|
||||
>
|
||||
<!-- 侧边栏 -->
|
||||
<n-layout-sider
|
||||
class="main-sider"
|
||||
:collapsed="status.asideMenuCollapsed"
|
||||
:collapsed="asideMenuCollapsed"
|
||||
:native-scrollbar="false"
|
||||
:collapsed-width="64"
|
||||
:width="240"
|
||||
class="main-sider"
|
||||
show-trigger="bar"
|
||||
collapse-mode="width"
|
||||
bordered
|
||||
@collapse="status.asideMenuCollapsed = true"
|
||||
@expand="status.asideMenuCollapsed = false"
|
||||
@collapse="asideMenuCollapsed = true"
|
||||
@expand="asideMenuCollapsed = false"
|
||||
>
|
||||
<div class="sider-all">
|
||||
<Menu />
|
||||
@@ -35,32 +35,29 @@
|
||||
</n-layout-sider>
|
||||
<!-- 页面区 -->
|
||||
<n-layout :native-scrollbar="false" embedded>
|
||||
<main id="main-layout" class="main-layout">
|
||||
{{ music.getplaySongData }}
|
||||
<!-- 回顶 -->
|
||||
<n-back-top bottom="110">
|
||||
<n-icon size="26">
|
||||
<SvgIcon icon="chevron-up" />
|
||||
</n-icon>
|
||||
</n-back-top>
|
||||
<!-- 路由页面 -->
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<Transition name="router" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</main>
|
||||
<MainLayout />
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
<!-- 主内容 - 无侧边栏 -->
|
||||
<n-layout-content
|
||||
v-else
|
||||
:class="{
|
||||
'body-layout': true,
|
||||
'player-bar': music.getPlaySongData?.id && showPlayBar,
|
||||
}"
|
||||
:native-scrollbar="false"
|
||||
position="absolute"
|
||||
embedded
|
||||
>
|
||||
<MainLayout />
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
<!-- 主播放器 -->
|
||||
<MainControl />
|
||||
<!-- 全屏播放器 -->
|
||||
<FullPlayer />
|
||||
<!-- 全局播放列表 -->
|
||||
<n-config-provider v-if="status.showFullPlayer" :theme="darkTheme">
|
||||
<n-config-provider v-if="showFullPlayer" :theme="darkTheme">
|
||||
<Playlist />
|
||||
</n-config-provider>
|
||||
<Playlist v-else />
|
||||
@@ -81,18 +78,83 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { darkTheme } from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { darkTheme, NButton } from "naive-ui";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
import { initPlayer } from "@/utils/Player";
|
||||
import { checkPlatform } from "@/utils/helper";
|
||||
import { initPlayer } from "@/utils/Player";
|
||||
import userSignIn from "@/utils/userSignIn";
|
||||
import globalShortcut from "@/utils/globalShortcut";
|
||||
import globalEvents from "@/utils/globalEvents";
|
||||
import packageJson from "@/../package.json";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { autoPlay, showSider, autoSignIn } = storeToRefs(settings);
|
||||
const { showPlayBar, asideMenuCollapsed, showFullPlayer } = storeToRefs(status);
|
||||
|
||||
// 公告数据
|
||||
const annShow =
|
||||
import.meta.env.RENDERER_VITE_ANN_TITLE && import.meta.env.RENDERER_VITE_ANN_CONTENT
|
||||
? true
|
||||
: false;
|
||||
const annType = import.meta.env.RENDERER_VITE_ANN_TYPE;
|
||||
const annTitle = import.meta.env.RENDERER_VITE_ANN_TITLE;
|
||||
const annContene = import.meta.env.RENDERER_VITE_ANN_CONTENT;
|
||||
const annDuration = Number(import.meta.env.RENDERER_VITE_ANN_DURATION);
|
||||
|
||||
// PWA
|
||||
if ("serviceWorker" in navigator) {
|
||||
// 更新完成提醒
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
if (checkPlatform.electron()) {
|
||||
$notification.create({
|
||||
title: "🎉 有更新啦",
|
||||
content: "检测到软件内资源有更新,是否重新启动软件以应用更新?",
|
||||
meta: "当前版本 v " + (packageJson.version || "1.0.0"),
|
||||
action: () =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: "primary",
|
||||
onClick: () => {
|
||||
electron.ipcRenderer.send("window-relaunch");
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => "更新",
|
||||
},
|
||||
),
|
||||
onAfterLeave: () => {
|
||||
$message.info("已取消本次更新,更新将在下次启动软件后生效", {
|
||||
duration: 6000,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.info("站点资源有更新,请刷新以应用更新");
|
||||
$message.info("站点资源有更新,请刷新以应用更新", {
|
||||
closable: true,
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示公告
|
||||
const showAnnouncements = () => {
|
||||
if (annShow) {
|
||||
$notification[annType]({
|
||||
content: annTitle,
|
||||
meta: annContene,
|
||||
duration: annDuration,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 网络无法连接
|
||||
const canNotConnect = (error) => {
|
||||
@@ -102,36 +164,49 @@ const canNotConnect = (error) => {
|
||||
title: "网络连接错误",
|
||||
content: "网络连接错误,请检查您当前的网络状态",
|
||||
positiveText: "重试",
|
||||
negativeText: "前往本地歌曲",
|
||||
negativeText: checkPlatform.electron() ? "前往本地歌曲" : "取消",
|
||||
onPositiveClick: () => {
|
||||
location.reload();
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
router.push("/local");
|
||||
if (checkPlatform.electron()) router.push("/local");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 网页端键盘事件
|
||||
const handleKeyUp = (event) => {
|
||||
globalShortcut(event, router);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 挂载方法
|
||||
window.$canNotConnect = canNotConnect;
|
||||
// 主播放器
|
||||
initPlayer(settings.autoPlay);
|
||||
await initPlayer(autoPlay.value);
|
||||
// 全局事件
|
||||
globalEvents(router);
|
||||
// 键盘监听
|
||||
window.addEventListener("keyup", globalShortcut);
|
||||
if (!checkPlatform.electron()) {
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
}
|
||||
// 自动签到
|
||||
if (autoSignIn.value) await userSignIn();
|
||||
// 显示公告
|
||||
showAnnouncements();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keyup", globalShortcut);
|
||||
if (!checkPlatform.electron()) window.removeEventListener("keyup", handleKeyUp);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.all-layout {
|
||||
height: 100%;
|
||||
transition: transform 0.3s;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
opacity 0.3s;
|
||||
.n-layout-header {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
@@ -142,9 +217,6 @@ onUnmounted(() => {
|
||||
.body-layout {
|
||||
top: 60px;
|
||||
transition: bottom 0.3s;
|
||||
&.player-bar {
|
||||
bottom: 80px;
|
||||
}
|
||||
.main-sider {
|
||||
:deep(.n-scrollbar-content) {
|
||||
height: 100%;
|
||||
@@ -152,16 +224,17 @@ onUnmounted(() => {
|
||||
.sider-all {
|
||||
height: 100%;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
@media (max-width: 900px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.main-layout {
|
||||
padding: 24px;
|
||||
&.player-bar {
|
||||
bottom: 80px;
|
||||
}
|
||||
}
|
||||
&.full-player {
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
136
src/api/dj.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import axios from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 电台部分
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取电台 - 分类
|
||||
*/
|
||||
export const getDjCatelist = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/catelist",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取电台 - 推荐
|
||||
*/
|
||||
export const getDjRecommend = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/recommend",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台个性推荐
|
||||
*/
|
||||
export const getDjPersonalRec = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/personalize/recommend",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取电台 - 推荐类型
|
||||
*/
|
||||
export const getDjCategoryRec = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/category/recommend",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 私人 DJ
|
||||
*/
|
||||
export const getPrivateDj = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/aidj/content/rcmd",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 类别热门电台
|
||||
* @param {string} cateId - 类别 id
|
||||
* @param {number} [limit=50] - 返回数量,默认 50
|
||||
* @param {number} [offset=0] - 偏移数量,默认 0
|
||||
*/
|
||||
export const getRadioHot = (cateId, limit = 50, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/radio/hot",
|
||||
params: {
|
||||
cateId,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 分类推荐
|
||||
* @param {string} type - 类别 id
|
||||
*/
|
||||
export const getRecType = (type) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/recommend/type",
|
||||
params: {
|
||||
type,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 详情
|
||||
* @param {string} rid - 电台 的 id
|
||||
*/
|
||||
export const getDjDetail = (rid) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/detail",
|
||||
params: {
|
||||
rid,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 节目
|
||||
* @param {string} rid - 电台 的 id
|
||||
* @param {number} [limit=50] - 返回数量,默认 50
|
||||
* @param {number} [offset=0] - 偏移数量,默认 0
|
||||
*/
|
||||
export const getDjProgram = (rid, limit = 50, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/program",
|
||||
params: {
|
||||
rid,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 电台 - 订阅
|
||||
* @param {number} rid - 电台 的 id
|
||||
* @param {number} t - 操作类型,1为收藏,0为取消收藏
|
||||
*/
|
||||
export const likeDj = (rid, t) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/sub",
|
||||
params: {
|
||||
rid,
|
||||
t,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -67,10 +67,10 @@ export const getPlayListDetail = (id) => {
|
||||
/**
|
||||
* 获取歌单中所有歌曲信息
|
||||
* @param {number} id - 歌单id
|
||||
* @param {number} [limit=30] - 返回数量,默认30
|
||||
* @param {number} [limit=50] - 返回数量,默认50
|
||||
* @param {number} [offset=0] - 偏移数量,默认0
|
||||
*/
|
||||
export const getAllPlayList = (id, limit = 30, offset = 0) => {
|
||||
export const getAllPlayList = (id, limit = 50, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/playlist/track/all",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from "@/utils/request";
|
||||
import idMeta from "@/assets/idMeta.json";
|
||||
|
||||
/**
|
||||
* 推荐部分
|
||||
@@ -34,6 +35,21 @@ export const getPersonalized = (type, limit = 12) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 雷达歌单
|
||||
*/
|
||||
export const getRadarPlaylist = async () => {
|
||||
const allRadar = idMeta.radarPlaylist.map((playlist) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/playlist/detail",
|
||||
params: { id: playlist.id },
|
||||
});
|
||||
});
|
||||
const result = await Promise.allSettled(allRadar);
|
||||
return result.map((res) => res?.value.playlist);
|
||||
};
|
||||
|
||||
/**
|
||||
* 热门歌手列表
|
||||
* @param {number} [limit=6] - 要返回的歌手数量,默认为 6 个
|
||||
|
||||
@@ -33,6 +33,19 @@ export const getSearchSuggest = (keywords, mobile = false) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认搜索关键词
|
||||
*/
|
||||
export const getSearchDefault = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/search/default",
|
||||
params: {
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索结果
|
||||
* @param {string} keywords - 搜索关键词
|
||||
|
||||
@@ -119,6 +119,19 @@ export const getUserMv = () => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户电台的订阅列表
|
||||
*/
|
||||
export const getUserDj = () => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/dj/sublist",
|
||||
params: {
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户喜欢的音乐列表
|
||||
* @param {string} uid 用户的id
|
||||
@@ -151,80 +164,6 @@ export const setLikeSong = (id, like = true) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户云盘数据
|
||||
* @param {number} [limit=30] - 返回数量,默认30
|
||||
* @param {number} [offset=0] - 偏移数量,默认0
|
||||
*/
|
||||
export const getCloud = (limit = 30, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/user/cloud",
|
||||
params: {
|
||||
limit,
|
||||
offset,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户云盘歌曲删除
|
||||
* @param {string} id - 歌曲的id
|
||||
*/
|
||||
export const setCloudDel = (id) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/user/cloud/del",
|
||||
params: {
|
||||
id,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 云盘歌曲信息匹配纠正
|
||||
* @param {string} uid - 用户id
|
||||
* @param {string} sid - 原歌曲id
|
||||
* @param {string} asid - 要匹配的歌曲id
|
||||
*/
|
||||
export const setCloudMatch = (uid, sid, asid) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/cloud/match",
|
||||
params: {
|
||||
uid,
|
||||
sid,
|
||||
asid,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户云盘上传
|
||||
* @param {File} file - 要上传的文件
|
||||
*/
|
||||
export const upCloudSong = (file, onUploadProgress) => {
|
||||
const formData = new FormData();
|
||||
formData.append("songFile", file);
|
||||
return axios({
|
||||
url: "/cloud",
|
||||
method: "POST",
|
||||
hiddenBar: true,
|
||||
params: {
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
data: formData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
timeout: 200000,
|
||||
onUploadProgress,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
|
||||
@@ -63,3 +63,59 @@ export const getSimiVideo = (mvid) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 收藏/取消收藏视频
|
||||
* @param {number} t - 操作类型,1为收藏,2为取消收藏
|
||||
* @param {number} id - 视频id
|
||||
*/
|
||||
export const likeVideo = (t, id) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/video/sub",
|
||||
params: {
|
||||
t,
|
||||
id,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 收藏/取消收藏 MV
|
||||
* @param {number} t - 操作类型,1为收藏,2为取消收藏
|
||||
* @param {number} mvid - MV id
|
||||
*/
|
||||
export const likeMv = (t, mvid) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/mv/sub",
|
||||
params: {
|
||||
t,
|
||||
mvid,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 全部 mv
|
||||
* @param {string} area - 地区,可选值为全部,内地,港台,欧美,日本,韩国,不填则为全部
|
||||
* @param {string} type - 类型,可选值为全部,官方版,原生,现场版,网易出品,不填则为全部
|
||||
* @param {string} order - 排序,可选值为上升最快,最热,最新,不填则为上升最快
|
||||
* @param {number} [limit=12] - 返回数量,默认12
|
||||
* @param {number} [offset=0] - 偏移数量,默认0
|
||||
*/
|
||||
export const allMv = (area, type, order, limit = 12, offset = 0) => {
|
||||
return axios({
|
||||
method: "GET",
|
||||
url: "/mv/all",
|
||||
params: {
|
||||
area,
|
||||
type,
|
||||
order,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"album": "M12 11a1 1 0 0 0-1 1a1 1 0 0 0 1 1a1 1 0 0 0 1-1a1 1 0 0 0-1-1m0 5.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5s4.5 2 4.5 4.5s-2 4.5-4.5 4.5M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2Z",
|
||||
"chevron-up": "M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6l-6 6l1.41 1.41Z",
|
||||
"chevron-down": "M7.41 8.58L12 13.17l4.59-4.59L18 10l-6 6l-6-6z",
|
||||
"play": "M9.525 18.025q-.5.325-1.012.038T8 17.175V6.825q0-.6.513-.888t1.012.038l8.15 5.175q.45.3.45.85t-.45.85l-8.15 5.175Z",
|
||||
"play-circle": "M10 16.5v-9l6 4.5M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2Z",
|
||||
"pause-circle": "M15 16h-2V8h2m-4 8H9V8h2m1-6A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2",
|
||||
"play-next": "m10 16.5l6-4.5l-6-4.5M22 12c0-5.54-4.46-10-10-10c-1.17 0-2.3.19-3.38.56l.7 1.94c.85-.34 1.74-.53 2.68-.53c4.41 0 8.03 3.62 8.03 8.03c0 4.41-3.62 8.03-8.03 8.03c-4.41 0-8.03-3.62-8.03-8.03c0-.94.19-1.88.53-2.72l-1.94-.66C2.19 9.7 2 10.83 2 12c0 5.54 4.46 10 10 10s10-4.46 10-10M5.47 3.97c.85 0 1.53.71 1.53 1.5C7 6.32 6.32 7 5.47 7c-.79 0-1.5-.68-1.5-1.53c0-.79.71-1.5 1.5-1.5Z",
|
||||
"playlist-add": "M4 16q-.425 0-.713-.288T3 15q0-.425.288-.713T4 14h5q.425 0 .713.288T10 15q0 .425-.288.713T9 16H4Zm0-4q-.425 0-.713-.288T3 11q0-.425.288-.713T4 10h9q.425 0 .713.288T14 11q0 .425-.288.713T13 12H4Zm0-4q-.425 0-.713-.288T3 7q0-.425.288-.713T4 6h9q.425 0 .713.288T14 7q0 .425-.288.713T13 8H4Zm13 12q-.425 0-.713-.288T16 19v-3h-3q-.425 0-.713-.288T12 15q0-.425.288-.713T13 14h3v-3q0-.425.288-.713T17 10q.425 0 .713.288T18 11v3h3q.425 0 .713.288T22 15q0 .425-.288.713T21 16h-3v3q0 .425-.288.713T17 20Z",
|
||||
"account-music": "M11 14c1 0 2.05.16 3.2.44c-.81.87-1.2 1.89-1.2 3.06c0 .89.25 1.73.78 2.5H3v-2c0-1.19.91-2.15 2.74-2.88C7.57 14.38 9.33 14 11 14m0-2c-1.08 0-2-.39-2.82-1.17C7.38 10.05 7 9.11 7 8c0-1.08.38-2 1.18-2.82C9 4.38 9.92 4 11 4c1.11 0 2.05.38 2.83 1.18C14.61 6 15 6.92 15 8c0 1.11-.39 2.05-1.17 2.83c-.78.78-1.72 1.17-2.83 1.17m7.5-2H22v2h-2v5.5a2.5 2.5 0 0 1-2.5 2.5a2.5 2.5 0 0 1-2.5-2.5a2.5 2.5 0 0 1 2.5-2.5c.36 0 .69.07 1 .21V10Z",
|
||||
@@ -22,6 +24,7 @@
|
||||
"chevron-right": "M8.59 16.58L13.17 12L8.59 7.41L10 6l6 6l-6 6l-1.41-1.42Z",
|
||||
"account-circle": "M12 19.2c-2.5 0-4.71-1.28-6-3.2c.03-2 4-3.1 6-3.1s5.97 1.1 6 3.1a7.232 7.232 0 0 1-6 3.2M12 5a3 3 0 0 1 3 3a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3m0-3A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10c0-5.53-4.5-10-10-10Z",
|
||||
"menu-down": "m7 10l5 5l5-5H7Z",
|
||||
"menu": "M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5s1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5S5.5 6.83 5.5 6S4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5s1.5-.68 1.5-1.5s-.67-1.5-1.5-1.5zM8 19h12c.55 0 1-.45 1-1s-.45-1-1-1H8c-.55 0-1 .45-1 1s.45 1 1 1zm0-6h12c.55 0 1-.45 1-1s-.45-1-1-1H8c-.55 0-1 .45-1 1s.45 1 1 1zM7 6c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H8c-.55 0-1 .45-1 1z",
|
||||
"calendar-check": "M19 19H5V8h14m0-5h-1V1h-2v2H8V1H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-2.47 8.06L15.47 10l-4.88 4.88l-2.12-2.12l-1.06 1.06L10.59 17l5.94-5.94Z",
|
||||
"calendar-badge": "M19.5 16c-1.9 0-3.5 1.6-3.5 3.5s1.6 3.5 3.5 3.5s3.5-1.6 3.5-3.5s-1.6-3.5-3.5-3.5m-5.29 5H5a2 2 0 0 1-2-2V5c0-1.11.89-2 2-2h1V1h2v2h8V1h2v2h1a2 2 0 0 1 2 2v9.21c-.5-.13-1-.21-1.5-.21c-.17 0-.33 0-.5.03V8H5v11h9.03c-.03.17-.03.33-.03.5c0 .5.08 1 .21 1.5Z",
|
||||
"round-wb-sunny": "m6.05 4.14l-.39-.39a.993.993 0 0 0-1.4 0l-.01.01a.984.984 0 0 0 0 1.4l.39.39c.39.39 1.01.39 1.4 0l.01-.01a.984.984 0 0 0 0-1.4zM3.01 10.5H1.99c-.55 0-.99.44-.99.99v.01c0 .55.44.99.99.99H3c.56.01 1-.43 1-.98v-.01c0-.56-.44-1-.99-1zm9-9.95H12c-.56 0-1 .44-1 .99v.96c0 .55.44.99.99.99H12c.56.01 1-.43 1-.98v-.97c0-.55-.44-.99-.99-.99zm7.74 3.21c-.39-.39-1.02-.39-1.41-.01l-.39.39a.984.984 0 0 0 0 1.4l.01.01c.39.39 1.02.39 1.4 0l.39-.39a.984.984 0 0 0 0-1.4zm-1.81 15.1l.39.39a.996.996 0 1 0 1.41-1.41l-.39-.39a.993.993 0 0 0-1.4 0c-.4.4-.4 1.02-.01 1.41zM20 11.49v.01c0 .55.44.99.99.99H22c.55 0 .99-.44.99-.99v-.01c0-.55-.44-.99-.99-.99h-1.01c-.55 0-.99.44-.99.99zM12 5.5c-3.31 0-6 2.69-6 6s2.69 6 6 6s6-2.69 6-6s-2.69-6-6-6zm-.01 16.95H12c.55 0 .99-.44.99-.99v-.96c0-.55-.44-.99-.99-.99h-.01c-.55 0-.99.44-.99.99v.96c0 .55.44.99.99.99zm-7.74-3.21c.39.39 1.02.39 1.41 0l.39-.39a.993.993 0 0 0 0-1.4l-.01-.01a.996.996 0 0 0-1.41 0l-.39.39c-.38.4-.38 1.02.01 1.41z",
|
||||
@@ -47,6 +50,7 @@
|
||||
"favorite-rounded": "M12 20.325q-.35 0-.713-.125t-.637-.4l-1.725-1.575q-2.65-2.425-4.788-4.813T2 8.15Q2 5.8 3.575 4.225T7.5 2.65q1.325 0 2.5.562t2 1.538q.825-.975 2-1.538t2.5-.562q2.35 0 3.925 1.575T22 8.15q0 2.875-2.125 5.275T15.05 18.25l-1.7 1.55q-.275.275-.637.4t-.713.125Z",
|
||||
"history": "M12 21q-3.15 0-5.575-1.913T3.275 14.2q-.1-.375.15-.687t.675-.363q.4-.05.725.15t.45.6q.6 2.25 2.475 3.675T12 19q2.925 0 4.963-2.038T19 12q0-2.925-2.038-4.963T12 5q-1.725 0-3.225.8T6.25 8H8q.425 0 .713.288T9 9q0 .425-.288.713T8 10H4q-.425 0-.713-.288T3 9V5q0-.425.288-.713T4 4q.425 0 .713.288T5 5v1.35q1.275-1.6 3.113-2.475T12 3q1.875 0 3.513.713t2.85 1.924q1.212 1.213 1.925 2.85T21 12q0 1.875-.713 3.513t-1.924 2.85q-1.213 1.212-2.85 1.925T12 21Zm1-9.4l2.5 2.5q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275l-2.8-2.8q-.15-.15-.225-.337T11 11.975V8q0-.425.288-.713T12 7q.425 0 .713.288T13 8v3.6Z",
|
||||
"delete": "M19 4h-3.5l-1-1h-5l-1 1H5v2h14M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6v12Z",
|
||||
"delete-sweep": "M15 16h4v2h-4zm0-8h7v2h-7zm0 4h6v2h-6zM3 18c0 1.1.9 2 2 2h6c1.1 0 2-.9 2-2V8H3zM14 5h-3l-1-1H6L5 5H2v2h12z",
|
||||
"fire": "M17.66 11.2c-.23-.3-.51-.56-.77-.82c-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32c-2.59 2.08-3.61 5.75-2.39 8.9c.04.1.08.2.08.33c0 .22-.15.42-.35.5c-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5c.14.6.41 1.2.71 1.73c1.08 1.73 2.95 2.97 4.96 3.22c2.14.27 4.43-.12 6.07-1.6c1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6c-1.12.4-2.24-.16-2.9-.82c1.19-.28 1.9-1.16 2.11-2.05c.17-.8-.15-1.46-.28-2.23c-.12-.74-.1-1.37.17-2.06c.19.38.39.76.63 1.06c.77 1 1.98 1.44 2.24 2.8c.04.14.06.28.06.43c.03.82-.33 1.72-.93 2.27Z",
|
||||
"search-rounded": "M9.5 16q-2.725 0-4.612-1.888T3 9.5q0-2.725 1.888-4.612T9.5 3q2.725 0 4.612 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l5.6 5.6q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275l-5.6-5.6q-.75.6-1.725.95T9.5 16Zm0-2q1.875 0 3.188-1.313T14 9.5q0-1.875-1.313-3.188T9.5 5Q7.625 5 6.312 6.313T5 9.5q0 1.875 1.313 3.188T9.5 14Z",
|
||||
"search-off": "m7 17.7l1.4 1.425q.15.15.35.15t.35-.15q.15-.15.15-.363T9.1 18.4L7.7 17l1.425-1.425q.15-.15.15-.35t-.15-.35q-.15-.15-.35-.15t-.35.15L7 16.3l-1.425-1.425q-.15-.15-.35-.15t-.35.15q-.15.15-.15.35t.15.35L6.3 17l-1.425 1.425q-.15.15-.15.35t.15.35q.15.15.35.15t.35-.15L7 17.7ZM7 22q-2.075 0-3.538-1.463T2 17q0-2.075 1.463-3.538T7 12q2.075 0 3.538 1.463T12 17q0 2.075-1.463 3.538T7 22Zm7.2-7.4q-.3-.325-.638-.663T12.9 13.3q.95-.6 1.525-1.6T15 9.5q0-1.875-1.313-3.188T10.5 5Q8.625 5 7.312 6.313T6 9.5q0 .15.013.288t.037.287q-.45.05-.987.2t-.963.35q-.05-.275-.075-.55T4 9.5q0-2.725 1.888-4.612T10.5 3q2.725 0 4.612 1.888T17 9.5q0 1.075-.338 2.038t-.937 1.762l5.575 5.6q.275.275.288.688t-.288.712q-.275.275-.7.275t-.7-.275l-5.7-5.7Z",
|
||||
@@ -80,5 +84,12 @@
|
||||
"download": "M16.59 9H15V4c0-.55-.45-1-1-1h-4c-.55 0-1 .45-1 1v5H7.41c-.89 0-1.34 1.08-.71 1.71l4.59 4.59c.39.39 1.02.39 1.41 0l4.59-4.59c.63-.63.19-1.71-.7-1.71zM5 19c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1z",
|
||||
"radio": "M3.24 6.15C2.51 6.43 2 7.17 2 8v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8c0-1.11-.89-2-2-2H8.3l8.26-3.34L15.88 1L3.24 6.15zM7 20c-1.66 0-3-1.34-3-3s1.34-3 3-3s3 1.34 3 3s-1.34 3-3 3zm13-8h-2v-2h-2v2H4V8h16v4z",
|
||||
"person-add": "M15.39 14.56C13.71 13.7 11.53 13 9 13s-4.71.7-6.39 1.56A2.97 2.97 0 0 0 1 17.22V20h16v-2.78c0-1.12-.61-2.15-1.61-2.66zM9 12c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4zm11-3V7c0-.55-.45-1-1-1s-1 .45-1 1v2h-2c-.55 0-1 .45-1 1s.45 1 1 1h2v2c0 .55.45 1 1 1s1-.45 1-1v-2h2c.55 0 1-.45 1-1s-.45-1-1-1h-2z",
|
||||
"person-remove": "M14 8c0-2.21-1.79-4-4-4S6 5.79 6 8s1.79 4 4 4s4-1.79 4-4zM2 18v1c0 .55.45 1 1 1h14c.55 0 1-.45 1-1v-1c0-2.66-5.33-4-8-4s-8 1.34-8 4zm16-8h4c.55 0 1 .45 1 1s-.45 1-1 1h-4c-.55 0-1-.45-1-1s.45-1 1-1z"
|
||||
"person-remove": "M14 8c0-2.21-1.79-4-4-4S6 5.79 6 8s1.79 4 4 4s4-1.79 4-4zM2 18v1c0 .55.45 1 1 1h14c.55 0 1-.45 1-1v-1c0-2.66-5.33-4-8-4s-8 1.34-8 4zm16-8h4c.55 0 1 .45 1 1s-.45 1-1 1h-4c-.55 0-1-.45-1-1s.45-1 1-1z",
|
||||
"github": "M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2Z",
|
||||
"phone": "M16 1H8C6.34 1 5 2.34 5 4v16c0 1.66 1.34 3 3 3h8c1.66 0 3-1.34 3-3V4c0-1.66-1.34-3-3-3zm1 17H7V4h10v14zm-3 3h-4v-1h4v1z",
|
||||
"password": "M2 17h20v2H2v-2zm1.15-4.05L4 11.47l.85 1.48l1.3-.75l-.85-1.48H7v-1.5H5.3l.85-1.47L4.85 7L4 8.47L3.15 7l-1.3.75l.85 1.47H1v1.5h1.7l-.85 1.48l1.3.75zm6.7-.75l1.3.75l.85-1.48l.85 1.48l1.3-.75l-.85-1.48H15v-1.5h-1.7l.85-1.47l-1.3-.75L12 8.47L11.15 7l-1.3.75l.85 1.47H9v1.5h1.7l-.85 1.48zM23 9.22h-1.7l.85-1.47l-1.3-.75L20 8.47L19.15 7l-1.3.75l.85 1.47H17v1.5h1.7l-.85 1.48l1.3.75l.85-1.48l.85 1.48l1.3-.75l-.85-1.48H23v-1.5z",
|
||||
"star": "m12 17.27l4.15 2.51c.76.46 1.69-.22 1.49-1.08l-1.1-4.72l3.67-3.18c.67-.58.31-1.68-.57-1.75l-4.83-.41l-1.89-4.46c-.34-.81-1.5-.81-1.84 0L9.19 8.63l-4.83.41c-.88.07-1.24 1.17-.57 1.75l3.67 3.18l-1.1 4.72c-.2.86.73 1.54 1.49 1.08l4.15-2.5z",
|
||||
"record": "M17 18.25v3.25H7v-3.25c0-1.38 2.24-2.5 5-2.5s5 1.12 5 2.5M12 5.5a6.5 6.5 0 0 1 6.5 6.5c0 1.25-.35 2.42-.96 3.41L16 14.04c.32-.61.5-1.31.5-2.04c0-2.5-2-4.5-4.5-4.5s-4.5 2-4.5 4.5c0 .73.18 1.43.5 2.04l-1.54 1.37c-.61-.99-.96-2.16-.96-3.41A6.5 6.5 0 0 1 12 5.5m0-4A10.5 10.5 0 0 1 22.5 12c0 2.28-.73 4.39-1.96 6.11l-1.5-1.35c.92-1.36 1.46-3 1.46-4.76A8.5 8.5 0 0 0 12 3.5A8.5 8.5 0 0 0 3.5 12c0 1.76.54 3.4 1.46 4.76l-1.5 1.35A10.473 10.473 0 0 1 1.5 12A10.5 10.5 0 0 1 12 1.5m0 8a2.5 2.5 0 0 1 2.5 2.5a2.5 2.5 0 0 1-2.5 2.5A2.5 2.5 0 0 1 9.5 12A2.5 2.5 0 0 1 12 9.5Z",
|
||||
"storage": "M4 20h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2s.9 2 2 2m0-3h2v2H4zM2 6c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2m4 1H4V5h2zm-2 7h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2s.9 2 2 2m0-3h2v2H4z",
|
||||
"lrc-text": "M13.8 22H5c-1.7 0-3-1.3-3-3v-1h11.1c-.1.3-.1.7-.1 1c0 1.1.3 2.1.8 3m0-6H5V5c0-1.7 1.3-3 3-3h11c1.7 0 3 1.3 3 3v1h-2V5c0-.6-.4-1-1-1s-1 .4-1 1v8.1c-1.8.3-3.3 1.4-4.2 2.9M8 8h7V6H8zm0 4h6v-2H8zm9 4v6l5-3z"
|
||||
}
|
||||
|
||||
28
src/assets/idMeta.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"radarPlaylist": [
|
||||
{
|
||||
"id": 3136952023,
|
||||
"name": "私人雷达"
|
||||
},
|
||||
{
|
||||
"id": 5320167908,
|
||||
"name": "时光雷达"
|
||||
},
|
||||
{
|
||||
"id": 5327906368,
|
||||
"name": "乐迷雷达"
|
||||
},
|
||||
{
|
||||
"id": 5362359247,
|
||||
"name": "宝藏雷达"
|
||||
},
|
||||
{
|
||||
"id": 5300458264,
|
||||
"name": "新歌雷达"
|
||||
},
|
||||
{
|
||||
"id": 5341776086,
|
||||
"name": "神秘雷达"
|
||||
}
|
||||
]
|
||||
}
|
||||
161
src/components/Cover/CoverPlayBtn.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<!-- 封面列表 - 播放按钮 -->
|
||||
<template>
|
||||
<div class="play-btn" @click.stop>
|
||||
<n-button
|
||||
:loading="playLoading"
|
||||
color="#efefef"
|
||||
tag="div"
|
||||
type="primary"
|
||||
class="play"
|
||||
size="large"
|
||||
strong
|
||||
secondary
|
||||
circle
|
||||
@click.stop="playAllSongs"
|
||||
>
|
||||
<template #icon>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-icon :key="`${isHasSongs}-${playState}`" size="50">
|
||||
<SvgIcon :icon="isHasSongs !== -1 && playState ? 'pause-circle' : 'play-circle'" />
|
||||
</n-icon>
|
||||
</Transition>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getAllPlayList } from "@/api/playlist";
|
||||
import { getAlbumDetail } from "@/api/album";
|
||||
import { getDjProgram } from "@/api/dj";
|
||||
import { musicData, siteStatus, siteData } from "@/stores";
|
||||
import { playOrPause, initPlayer } from "@/utils/Player";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { userLikeData } = storeToRefs(data);
|
||||
const { playList, playSongData } = storeToRefs(music);
|
||||
const { playIndex, playMode, playHeartbeatMode, playState } = storeToRefs(status);
|
||||
|
||||
const props = defineProps({
|
||||
// id
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
// 歌单类型
|
||||
type: {
|
||||
type: String,
|
||||
default: "playlist",
|
||||
},
|
||||
});
|
||||
// 播放按钮数据
|
||||
const playLoading = ref(false);
|
||||
const playListData = ref(null);
|
||||
|
||||
// 是否处于当前歌单
|
||||
const isHasSongs = computed(() => {
|
||||
if (!playListData.value || playListData.value === 400) return -1;
|
||||
const songId = music.getPlaySongData?.id;
|
||||
const existingIndex = playListData.value.findIndex((song) => song.id === songId);
|
||||
return existingIndex;
|
||||
});
|
||||
|
||||
// 获取歌单数据
|
||||
const getPlaylistData = async () => {
|
||||
// 为了播放速度,仅加载列表前 500 首
|
||||
console.log(props.type, props.id);
|
||||
// 按列表类别获取数据
|
||||
switch (props.type) {
|
||||
case "playlist": {
|
||||
if (props.id === 1024) {
|
||||
console.log("播放我喜欢的音乐");
|
||||
const id = userLikeData.value.playlists?.[0]?.id || null;
|
||||
if (!isLogin() || !id) return 400;
|
||||
const result = await getAllPlayList(id, 500);
|
||||
return formatData(result.songs, "song");
|
||||
} else {
|
||||
const result = await getAllPlayList(props.id, 500);
|
||||
return formatData(result.songs, "song");
|
||||
}
|
||||
}
|
||||
case "album": {
|
||||
const result = await getAlbumDetail(props.id);
|
||||
return formatData(result.songs, "song");
|
||||
}
|
||||
case "dj": {
|
||||
const result = await getDjProgram(props.id, 500);
|
||||
return formatData(result.programs, "dj");
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 播放歌单
|
||||
const playAllSongs = async () => {
|
||||
try {
|
||||
if (!props.id) return false;
|
||||
// 开启加载状态
|
||||
if (props.type !== "mv") {
|
||||
// 若不处于歌单内
|
||||
if (isHasSongs.value === -1) {
|
||||
playLoading.value = true;
|
||||
// 获取歌单数据
|
||||
playListData.value = await getPlaylistData();
|
||||
console.log(playListData.value);
|
||||
if (!playListData.value) {
|
||||
playLoading.value = false;
|
||||
return $message.error("获取播放列表时出现错误");
|
||||
}
|
||||
if (playListData.value === 400) {
|
||||
playLoading.value = false;
|
||||
return $message.error("请登录后使用");
|
||||
}
|
||||
console.log("不在歌单内");
|
||||
// 更改模式和歌单
|
||||
playHeartbeatMode.value = false;
|
||||
playMode.value = props.type === "dj" ? "dj" : "normal";
|
||||
playList.value = playListData.value.slice();
|
||||
playSongData.value = playListData.value[0];
|
||||
playIndex.value = 0;
|
||||
// 初始化播放器
|
||||
await initPlayer(true);
|
||||
playLoading.value = false;
|
||||
$message.info("已开始播放", { showIcon: false });
|
||||
}
|
||||
// 若处于歌单内
|
||||
else {
|
||||
console.log("处于歌单内");
|
||||
playSongData.value = playListData.value[isHasSongs.value];
|
||||
playIndex.value = isHasSongs.value;
|
||||
// 播放
|
||||
playOrPause();
|
||||
}
|
||||
} else {
|
||||
// 跳转播放器
|
||||
router.push({
|
||||
path: "/videos-player",
|
||||
query: {
|
||||
id: props.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取播放列表时出现错误:", error);
|
||||
$message.error("获取播放列表时出现错误");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.play-btn {
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
</style>
|
||||
@@ -36,7 +36,17 @@
|
||||
>
|
||||
<template #placeholder>
|
||||
<div :class="['cover-loading', type]">
|
||||
<img class="loading-img" src="/images/pic/album.jpg?assest" alt="song" />
|
||||
<img
|
||||
class="loading-img"
|
||||
:src="
|
||||
type === 'mv'
|
||||
? '/images/pic/video.png?assest'
|
||||
: type === 'artist'
|
||||
? '/images/pic/artist.jpg?assest'
|
||||
: '/images/pic/album.jpg?assest'
|
||||
"
|
||||
alt="song"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div :class="['cover-loading', type]">
|
||||
<n-spin size="small" />
|
||||
@@ -56,21 +66,31 @@
|
||||
<n-text class="add-desc">{{ item.desc }}</n-text>
|
||||
</div>
|
||||
<!-- 播放按钮 -->
|
||||
<n-icon class="play">
|
||||
<SvgIcon :icon="type !== 'artist' ? 'play-circle' : 'account-music'" />
|
||||
<CoverPlayBtn v-if="type !== 'artist'" :id="item.id" :type="type" />
|
||||
<n-icon v-else class="play-btn">
|
||||
<SvgIcon icon="account-music" />
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 信息 -->
|
||||
<div class="cover-content">
|
||||
<n-text class="name">{{ item.name }}</n-text>
|
||||
<!-- 创建者 -->
|
||||
<n-text v-if="item.creator" class="creator" depth="3">
|
||||
<n-text v-if="item?.creator" class="creator" depth="3">
|
||||
{{ item.creator?.nickname || item.creator || "未知" }}
|
||||
</n-text>
|
||||
<!-- 电台简介 -->
|
||||
<n-text v-if="item?.rcmdText" class="creator" depth="3">
|
||||
{{ item.rcmdText || "未知简介" }}
|
||||
</n-text>
|
||||
<!-- 歌手 -->
|
||||
<div v-if="item.artists" class="artists">
|
||||
<n-text v-for="ar in item.artists" :key="ar.id" class="ar">
|
||||
{{ ar.name }}
|
||||
<div v-if="item.artists && type !== 'dj'" class="artists">
|
||||
<n-text
|
||||
v-for="ar in item.artists"
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click.stop="router.push(`/artist?id=${ar.id}`)"
|
||||
>
|
||||
{{ ar.name || ar.userName }}
|
||||
</n-text>
|
||||
</div>
|
||||
<!-- 歌曲数量 -->
|
||||
@@ -193,6 +213,14 @@ const jumpLink = (data, type) => {
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "dj":
|
||||
router.push({
|
||||
path: "/dj",
|
||||
query: {
|
||||
id: data?.id,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -255,7 +283,9 @@ const jumpLink = (data, type) => {
|
||||
top: -80px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
box-sizing: border-box;
|
||||
background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
|
||||
opacity: 0;
|
||||
transition:
|
||||
@@ -267,14 +297,18 @@ const jumpLink = (data, type) => {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
}
|
||||
.play {
|
||||
.play-btn {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
opacity: 0;
|
||||
color: #efefefde;
|
||||
transform: translateY(6px);
|
||||
font-size: 50px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
z-index: 3;
|
||||
transition:
|
||||
opacity 0.3s,
|
||||
@@ -297,8 +331,10 @@ const jumpLink = (data, type) => {
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
display: inline-flex;
|
||||
color: var(--n-close-icon-color);
|
||||
transition: color 0.3s;
|
||||
opacity: 0.6;
|
||||
transition:
|
||||
color 0.3s,
|
||||
opacity 0.3s;
|
||||
cursor: pointer;
|
||||
&::after {
|
||||
content: "/";
|
||||
@@ -311,10 +347,7 @@ const jumpLink = (data, type) => {
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--n-code-text-color);
|
||||
&::after {
|
||||
color: var(--n-close-icon-color);
|
||||
}
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,7 +371,7 @@ const jumpLink = (data, type) => {
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
.play {
|
||||
.play-btn {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
&:hover {
|
||||
@@ -367,8 +400,8 @@ const jumpLink = (data, type) => {
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.play {
|
||||
// display: none;
|
||||
.play-btn {
|
||||
font-size: 50px;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
@@ -33,9 +33,7 @@
|
||||
</template>
|
||||
</n-image>
|
||||
<!-- 播放按钮 -->
|
||||
<n-icon v-if="showIcon" class="play" @click.stop>
|
||||
<SvgIcon icon="play-circle" />
|
||||
</n-icon>
|
||||
<CoverPlayBtn v-if="showIcon" :id="data.id" class="play" />
|
||||
<!-- 日期 -->
|
||||
<div v-if="showDate" class="cover-date">
|
||||
<n-icon class="date-icon">
|
||||
@@ -145,15 +143,24 @@ const props = defineProps({
|
||||
.play {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
font-size: 60px;
|
||||
color: #ffffffe6;
|
||||
transform: scale(0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
opacity 0.3s;
|
||||
cursor: pointer;
|
||||
:deep(.play) {
|
||||
--n-width: 50px;
|
||||
--n-height: 50px;
|
||||
--n-font-size: 20px;
|
||||
.n-icon {
|
||||
font-size: 60px !important;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
@@ -212,7 +219,6 @@ const props = defineProps({
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--n-close-color-hover);
|
||||
transform: translate3d(-2px, 0, 0);
|
||||
.cover-main-img {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
50
src/components/Global/MainLayout.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<main id="main-layout" :class="['main-layout', { 'no-sider': !showSider }]">
|
||||
<!-- 回顶 -->
|
||||
<n-back-top
|
||||
:bottom="music.getPlaySongData?.id && showPlayBar ? 110 : 50"
|
||||
style="transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||
>
|
||||
<n-icon size="26">
|
||||
<SvgIcon icon="chevron-up" />
|
||||
</n-icon>
|
||||
</n-back-top>
|
||||
<!-- 路由页面 -->
|
||||
<router-view v-slot="{ Component }" class="main-router">
|
||||
<keep-alive>
|
||||
<Transition name="router" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { showPlayBar } = storeToRefs(status);
|
||||
const { showSider } = storeToRefs(settings);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-layout {
|
||||
padding: 24px;
|
||||
&.no-sider {
|
||||
padding: 0;
|
||||
background-color: var(--n-color);
|
||||
.main-router {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 10vw;
|
||||
@media (max-width: 1200px) {
|
||||
padding: 24px 5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,15 +3,16 @@
|
||||
<n-menu
|
||||
ref="mainMenuRef"
|
||||
v-model:value="menuActiveKey"
|
||||
class="main-menu"
|
||||
:root-indent="36"
|
||||
:class="['main-menu', { cover: siderShowCover }]"
|
||||
:root-indent="showSider ? 36 : 26"
|
||||
:indent="0"
|
||||
:collapsed="status.asideMenuCollapsed"
|
||||
:collapsed="asideMenuCollapsed.value"
|
||||
:defaultExpandedKeys="['user-playlists', 'favorite-playlists']"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
@contextmenu="openSideDropdown($event)"
|
||||
@contextmenu.stop
|
||||
@update:value="checkMenuItem"
|
||||
/>
|
||||
<!-- 右键菜单 -->
|
||||
<CoverDropdown ref="coverDropdownRef" />
|
||||
@@ -21,8 +22,8 @@
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteStatus, siteData, musicData } from "@/stores";
|
||||
import { NIcon, NText, NButton } from "naive-ui";
|
||||
import { siteStatus, siteData, musicData, siteSettings } from "@/stores";
|
||||
import { NIcon, NText, NButton, NAvatar } from "naive-ui";
|
||||
import { useRouter, RouterLink } from "vue-router";
|
||||
import { getHeartRateList } from "@/api/playlist";
|
||||
import { checkPlatform } from "@/utils/helper";
|
||||
@@ -36,9 +37,12 @@ const router = useRouter();
|
||||
const data = siteData();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { userData, userLikeData } = storeToRefs(data);
|
||||
const { playList, playListOld, playIndex, playSongData, playHeartbeatMode, playMode } =
|
||||
storeToRefs(music);
|
||||
const settings = siteSettings();
|
||||
const { siderShowCover } = storeToRefs(settings);
|
||||
const { asideMenuCollapsed, showSider, showFullPlayer, playIndex, playMode, playHeartbeatMode } =
|
||||
storeToRefs(status);
|
||||
const { userData, userLikeData, userLoginStatus } = storeToRefs(data);
|
||||
const { playList, playListOld, playSongData, privateFmSong } = storeToRefs(music);
|
||||
|
||||
// 子组件
|
||||
const coverDropdownRef = ref(null);
|
||||
@@ -90,7 +94,7 @@ const menuOptions = computed(() => [
|
||||
label: "在线音乐",
|
||||
key: "online",
|
||||
children: [],
|
||||
show: !status.asideMenuCollapsed,
|
||||
show: !asideMenuCollapsed.value,
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
@@ -120,19 +124,24 @@ const menuOptions = computed(() => [
|
||||
key: "discover",
|
||||
icon: renderIcon("discover-fill"),
|
||||
},
|
||||
{
|
||||
label: "私人漫游",
|
||||
key: "fm",
|
||||
icon: renderIcon("radio"),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: "videos",
|
||||
name: "dj-hot",
|
||||
},
|
||||
},
|
||||
() => ["视频"],
|
||||
() => ["播客电台"],
|
||||
),
|
||||
key: "videos",
|
||||
icon: renderIcon("video"),
|
||||
key: "dj-hot",
|
||||
icon: renderIcon("record"),
|
||||
},
|
||||
{
|
||||
key: "divider-1",
|
||||
@@ -143,7 +152,7 @@ const menuOptions = computed(() => [
|
||||
label: "我的音乐",
|
||||
key: "user",
|
||||
children: [],
|
||||
show: !status.asideMenuCollapsed,
|
||||
show: !asideMenuCollapsed.value,
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
@@ -154,27 +163,53 @@ const menuOptions = computed(() => [
|
||||
path: "/like-songs",
|
||||
},
|
||||
class: "user-playlist",
|
||||
menuid: "like-songs",
|
||||
},
|
||||
() => [
|
||||
h(NText, null, () => ["喜欢的音乐"]),
|
||||
h(NButton, {
|
||||
size: "small",
|
||||
type: "tertiary",
|
||||
round: true,
|
||||
strong: true,
|
||||
secondary: true,
|
||||
renderIcon: renderIcon("heartbit", "26"),
|
||||
onclick: (event) => {
|
||||
event.stopPropagation();
|
||||
startHeartRate();
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
style: {
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
},
|
||||
}),
|
||||
[
|
||||
h("span", null, ["喜欢的音乐"]),
|
||||
h(NButton, {
|
||||
size: "small",
|
||||
type: "tertiary",
|
||||
round: true,
|
||||
strong: true,
|
||||
secondary: true,
|
||||
class: asideMenuCollapsed.value ? "heart-rate-btn collapsed" : "heart-rate-btn",
|
||||
renderIcon: renderIcon("heartbit", "26"),
|
||||
onclick: () => {
|
||||
startHeartRate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
key: "like-songs",
|
||||
icon: renderIcon("favorite-rounded"),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: "like",
|
||||
},
|
||||
},
|
||||
() => ["我的收藏"],
|
||||
),
|
||||
key: "like",
|
||||
icon: renderIcon("star"),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
@@ -222,13 +257,18 @@ const menuOptions = computed(() => [
|
||||
key: "divider-2",
|
||||
type: "divider",
|
||||
},
|
||||
{ ...userPlaylists.value, show: !status.asideMenuCollapsed },
|
||||
{ ...favoritePlaylists.value, show: !status.asideMenuCollapsed },
|
||||
{ ...userPlaylists.value, show: !asideMenuCollapsed.value },
|
||||
{ ...favoritePlaylists.value, show: !asideMenuCollapsed.value },
|
||||
]);
|
||||
|
||||
// 更改用户的歌单
|
||||
const changeUserPlaylists = (data) => {
|
||||
if (!isLogin() || !data?.length) return false;
|
||||
// 未登录
|
||||
if (!isLogin() || !data?.length) {
|
||||
userPlaylists.value.children = [];
|
||||
favoritePlaylists.value.children = [];
|
||||
return false;
|
||||
}
|
||||
// 用户 id
|
||||
const userId = userData.value?.userId;
|
||||
// 创建的歌单
|
||||
@@ -239,52 +279,102 @@ const changeUserPlaylists = (data) => {
|
||||
userPlaylists.value.children = userPlaylistsData.slice(1).map((v) => {
|
||||
return {
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
siderShowCover.value
|
||||
? h(
|
||||
"div",
|
||||
{
|
||||
class: "user-pl-cover",
|
||||
onclick: () => {
|
||||
router.push({
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
class: "user-playlist",
|
||||
menuId: v.id,
|
||||
},
|
||||
() => [h(NText, null, () => [v.name])],
|
||||
),
|
||||
[
|
||||
h(NAvatar, { src: v?.coverSize?.s, fallbackSrc: "/images/pic/album.jpg?assest" }),
|
||||
h(NText, null, () => [v.name]),
|
||||
],
|
||||
)
|
||||
: h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
},
|
||||
},
|
||||
class: "user-playlist",
|
||||
},
|
||||
() => [h(NText, null, () => [v.name])],
|
||||
),
|
||||
key: v.id,
|
||||
icon: renderIcon("queue-music-rounded"),
|
||||
icon: siderShowCover.value ? null : renderIcon("queue-music-rounded"),
|
||||
};
|
||||
});
|
||||
favoritePlaylists.value.children = favoritePlaylistsData.map((v) => {
|
||||
return {
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
siderShowCover.value
|
||||
? h(
|
||||
"div",
|
||||
{
|
||||
class: "user-pl-cover",
|
||||
onclick: () => {
|
||||
router.push({
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
class: "user-playlist",
|
||||
menuId: v.id,
|
||||
},
|
||||
() => [h(NText, null, () => [v.name])],
|
||||
),
|
||||
[
|
||||
h(NAvatar, { src: v?.coverSize?.s, fallbackSrc: "/images/pic/album.jpg?assest" }),
|
||||
h(NText, null, () => [v.name]),
|
||||
],
|
||||
)
|
||||
: h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
path: "/playlist",
|
||||
query: {
|
||||
id: v.id,
|
||||
},
|
||||
},
|
||||
class: "user-playlist",
|
||||
},
|
||||
() => [h(NText, null, () => [v.name])],
|
||||
),
|
||||
key: v.id,
|
||||
icon: renderIcon("queue-music-rounded"),
|
||||
icon: siderShowCover.value ? null : renderIcon("queue-music-rounded"),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 选中菜单项
|
||||
const checkMenuItem = (key) => {
|
||||
console.log(key);
|
||||
const checkMenuItem = async (key) => {
|
||||
// 例外路由
|
||||
const otherRouter = ["search", "videos-player", "playlist", "like-songs"];
|
||||
// 私人漫游
|
||||
if (key === "fm") {
|
||||
if (!privateFmSong.value || !Object.keys(privateFmSong.value)?.length) {
|
||||
return $message.error("开启私人漫游出错,请重试");
|
||||
}
|
||||
if (playMode.value === "fm") {
|
||||
fadePlayOrPause();
|
||||
} else {
|
||||
// 更改播放模式
|
||||
playMode.value = "fm";
|
||||
await initPlayer(true);
|
||||
}
|
||||
showFullPlayer.value = true;
|
||||
$message.info("已开启私人漫游", { icon: renderIcon("radio") });
|
||||
}
|
||||
// 特殊处理
|
||||
if (!key) {
|
||||
menuActiveKey.value = "home";
|
||||
@@ -319,20 +409,11 @@ const checkMenuItem = (key) => {
|
||||
mainMenuRef.value?.showOption(key);
|
||||
};
|
||||
|
||||
// 开启右键菜单
|
||||
const openSideDropdown = (e) => {
|
||||
e.preventDefault();
|
||||
if (!e.target.classList.contains("user-playlist")) return false;
|
||||
// 获取 id
|
||||
const menuId = e.target.getAttribute("menuid");
|
||||
coverDropdownRef.value?.openDropdown(e, "playlist", menuId);
|
||||
};
|
||||
|
||||
// 开启心动模式
|
||||
const startHeartRate = debounce(async () => {
|
||||
try {
|
||||
if (!isLogin()) return false;
|
||||
if (playHeartbeatMode.value) return await fadePlayOrPause();
|
||||
if (playHeartbeatMode.value) return fadePlayOrPause();
|
||||
// 基础数据
|
||||
const likeSongs = userLikeData.value.songs;
|
||||
const songId = playSongData.value?.id;
|
||||
@@ -381,10 +462,8 @@ watch(
|
||||
|
||||
// 监听用户歌单变化
|
||||
watch(
|
||||
() => userLikeData.value.playlists,
|
||||
(val) => {
|
||||
changeUserPlaylists(val);
|
||||
},
|
||||
[() => userLikeData.value.playlists, () => userLoginStatus.value, () => siderShowCover.value],
|
||||
() => changeUserPlaylists(userLikeData.value.playlists),
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
@@ -394,6 +473,7 @@ onMounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-menu {
|
||||
padding-bottom: 14px;
|
||||
:deep(.n-menu-item) {
|
||||
.n-menu-item-content {
|
||||
&.n-menu-item-content--selected {
|
||||
@@ -406,12 +486,25 @@ onMounted(() => {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
// 我喜欢的音乐
|
||||
// 普通歌单
|
||||
.user-playlist {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
// 带封面歌单
|
||||
.user-pl-cover {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.n-avatar {
|
||||
border-radius: 8px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 折叠菜单
|
||||
@@ -444,5 +537,29 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
&.cover {
|
||||
:deep(.n-submenu-children) {
|
||||
--n-item-height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 特殊样式 -->
|
||||
<style lang="scss">
|
||||
.heart-rate-btn {
|
||||
&:hover {
|
||||
background-color: var(--main-second-color) !important;
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
&.collapsed {
|
||||
margin-left: 12px;
|
||||
background-color: #efefef40;
|
||||
color: #efefef;
|
||||
&:hover {
|
||||
background-color: #efefef60 !important;
|
||||
color: #efefef !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<!-- 全局播放列表 -->
|
||||
<template>
|
||||
<n-drawer
|
||||
v-model:show="status.playListShow"
|
||||
:class="status.showFullPlayer ? 'main-playlist player' : 'main-playlist'"
|
||||
v-model:show="playListShow"
|
||||
:class="['main-playlist', { 'full-player': showFullPlayer }]"
|
||||
:style="{
|
||||
'--color': coverColor,
|
||||
'--color-bg': coverColor + '14',
|
||||
'--cover-main-color': `rgb(${coverTheme?.light?.shadeTwo})` || '#efefef',
|
||||
'--cover-second-color': `rgba(${coverTheme?.light?.shadeTwo}, 0.14)` || '#efefef14',
|
||||
}"
|
||||
:auto-focus="false"
|
||||
@after-enter="playlistOpen"
|
||||
@after-leave="status.playListShow = false"
|
||||
@mask-click="status.playListShow = false"
|
||||
@after-leave="playListShow = false"
|
||||
@mask-click="playListShow = false"
|
||||
>
|
||||
<n-drawer-content :native-scrollbar="false" closable>
|
||||
<template #header>
|
||||
@@ -20,28 +21,100 @@
|
||||
</n-text>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 提示 -->
|
||||
<n-alert v-if="playList?.length >= 400" class="alert" :show-icon="false">
|
||||
因歌单数量过大,无法自动定位,请手动查找
|
||||
</n-alert>
|
||||
<!-- 歌曲列表 -->
|
||||
<div class="list">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-virtual-list
|
||||
v-if="playList?.length"
|
||||
ref="playListRef"
|
||||
:item-size="76"
|
||||
:items="playListData"
|
||||
:default-scroll-index="playIndex"
|
||||
style="max-height: calc(100vh - 158px)"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<div
|
||||
:id="`songs-${index}`"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'songs-item',
|
||||
{ play: playSongData?.id === item?.id, player: showFullPlayer },
|
||||
]"
|
||||
@click.stop="playSong(item, index)"
|
||||
@dblclick.stop="playSong(item, index)"
|
||||
>
|
||||
<!-- 序号 -->
|
||||
<n-text v-if="playSongData?.id !== item?.id" class="num" depth="3">
|
||||
{{ index + 1 }}
|
||||
</n-text>
|
||||
<n-icon v-else class="play" size="18">
|
||||
<SvgIcon icon="music-note" />
|
||||
</n-icon>
|
||||
<!-- 信息 -->
|
||||
<div class="info">
|
||||
<!-- 歌曲名 -->
|
||||
<n-text class="name" depth="2">{{ item?.name || "未知曲目" }}</n-text>
|
||||
<!-- 歌手 -->
|
||||
<div v-if="Array.isArray(item?.artists)" class="artist">
|
||||
<n-text v-for="ar in item.artists" :key="ar.id" depth="3" class="ar">
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div v-else-if="playMode === 'dj'" class="artist">
|
||||
<n-text class="ar"> 电台节目 </n-text>
|
||||
</div>
|
||||
<div v-else class="artist">
|
||||
<n-text class="ar"> {{ item?.artists || "未知艺术家" }} </n-text>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 删除 -->
|
||||
<n-icon class="delete" size="18" @click.stop="removeSong(index)">
|
||||
<SvgIcon icon="delete" />
|
||||
</n-icon>
|
||||
</div>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
<n-empty
|
||||
v-else
|
||||
description="播放列表暂无歌曲,快去添加吧"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-data-table
|
||||
v-if="playList?.length"
|
||||
class="pl-list"
|
||||
:columns="columns"
|
||||
:data="playList"
|
||||
:bordered="false"
|
||||
:bottom-bordered="false"
|
||||
:max-height="playList?.length >= 400 ? 'calc(100vh - 162px)' : '100%'"
|
||||
virtual-scroll
|
||||
/>
|
||||
<n-empty
|
||||
v-else
|
||||
description="播放列表暂无歌曲,快去添加吧"
|
||||
class="tip"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
<n-grid v-if="playList?.length" :cols="2" x-gap="16" class="controls">
|
||||
<n-gi>
|
||||
<!-- 定位歌曲 -->
|
||||
<n-button
|
||||
size="large"
|
||||
tag="div"
|
||||
strong
|
||||
secondary
|
||||
@click="playListRef?.scrollTo({ index: playIndex })"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="location" />
|
||||
</n-icon>
|
||||
</template>
|
||||
当前播放
|
||||
</n-button>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<!-- 清空列表 -->
|
||||
<n-button size="large" tag="div" strong secondary @click="cleanPlaylists">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SvgIcon icon="delete-sweep" />
|
||||
</n-icon>
|
||||
</template>
|
||||
清空列表
|
||||
</n-button>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</Transition>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
@@ -50,19 +123,30 @@
|
||||
<script setup>
|
||||
import { NText, NIcon } from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicData, siteStatus } from "@/stores";
|
||||
import {
|
||||
initPlayer,
|
||||
fadePlayOrPause,
|
||||
changePlayIndex,
|
||||
soundStop,
|
||||
checkPlayer,
|
||||
} from "@/utils/Player";
|
||||
import { musicData, siteStatus, siteSettings } from "@/stores";
|
||||
import { initPlayer, fadePlayOrPause, changePlayIndex, soundStop } from "@/utils/Player";
|
||||
import SvgIcon from "@/components/Global/SvgIcon";
|
||||
import debounce from "@/utils/debounce";
|
||||
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { coverColor, playSongData, playList, playIndex, playMode } = storeToRefs(music);
|
||||
const settings = siteSettings();
|
||||
const { useMusicCache } = storeToRefs(settings);
|
||||
const { playSongData, playList } = storeToRefs(music);
|
||||
const { coverTheme, showFullPlayer, playListShow, playIndex, playMode, playLoading } =
|
||||
storeToRefs(status);
|
||||
|
||||
const playListRef = ref(null);
|
||||
|
||||
// 播放列表数据
|
||||
const playListData = computed(() => {
|
||||
return playList.value?.[0]
|
||||
? playList.value.slice().map((v, i) => {
|
||||
v.key = `${i}`;
|
||||
return v;
|
||||
})
|
||||
: [];
|
||||
});
|
||||
|
||||
// 抽屉开启
|
||||
const playlistOpen = () => {
|
||||
@@ -75,75 +159,15 @@ const playlistOpen = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 表格数据
|
||||
const columns = computed(() => [
|
||||
{
|
||||
key: "songs",
|
||||
className: "songs-item",
|
||||
render(song, index) {
|
||||
return createSongs(song, index);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 列表歌曲模块
|
||||
const createSongs = (song, index) => {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
id: `pl-song-${index}`,
|
||||
class: {
|
||||
songs: true,
|
||||
play: playSongData.value?.id === song?.id,
|
||||
player: status.showFullPlayer,
|
||||
},
|
||||
onClick: () => {
|
||||
playSong(song, index);
|
||||
},
|
||||
},
|
||||
[
|
||||
// 序号
|
||||
playSongData.value?.id !== song?.id
|
||||
? h(NText, { class: "num", depth: "3" }, () => [index + 1])
|
||||
: h(NIcon, { class: "play", size: "18" }, () => [h(SvgIcon, { icon: "music-note" })]),
|
||||
// 信息
|
||||
h("div", { class: "info" }, [
|
||||
// 名称
|
||||
h(NText, { class: "name", depth: "2" }, () => [song?.name || "未知曲目"]),
|
||||
// 歌手
|
||||
Array.isArray(song.artists)
|
||||
? h(
|
||||
"div",
|
||||
{ class: "artist" },
|
||||
song.artists.map((ar) =>
|
||||
h(NText, { class: "ar", depth: "3", key: ar.id }, () => [ar.name]),
|
||||
),
|
||||
)
|
||||
: h("div", { class: "artist" }, [
|
||||
h(NText, { class: "ar", depth: "3" }, () => [song.artists || "未知艺术家"]),
|
||||
]),
|
||||
]),
|
||||
// 移除
|
||||
h(
|
||||
NIcon,
|
||||
{
|
||||
class: "delete",
|
||||
size: "18",
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
removeSong(index);
|
||||
},
|
||||
},
|
||||
() => [h(SvgIcon, { icon: "delete" })],
|
||||
),
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// 播放歌曲
|
||||
const playSong = async (song, index) => {
|
||||
const playSong = debounce(async (song, index) => {
|
||||
// 若开启了缓存且正在加载
|
||||
if (useMusicCache.value && playLoading.value) {
|
||||
$message.warning("歌曲正在缓冲中,请稍后");
|
||||
return false;
|
||||
}
|
||||
// 更改模式
|
||||
playMode.value = "normal";
|
||||
if (playMode.value !== "dj") playMode.value = "normal";
|
||||
// 更改播放索引
|
||||
playIndex.value = index;
|
||||
// 是否为当前播放歌曲
|
||||
@@ -153,28 +177,42 @@ const playSong = async (song, index) => {
|
||||
} else {
|
||||
console.log("与当前播放歌曲不一致");
|
||||
playSongData.value = song;
|
||||
// 渐出音乐
|
||||
if (checkPlayer()) await fadePlayOrPause("pause");
|
||||
// 初始化播放器
|
||||
initPlayer(true);
|
||||
await initPlayer(true);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// 清空列表
|
||||
const cleanPlaylists = () => {
|
||||
soundStop();
|
||||
playIndex.value = 0;
|
||||
playList.value = [];
|
||||
playSongData.value = {};
|
||||
playListShow.value = false;
|
||||
showFullPlayer.value = false;
|
||||
$message.success("已清空播放列表");
|
||||
};
|
||||
|
||||
// 移除歌曲
|
||||
const removeSong = async (index) => {
|
||||
if (index < playIndex.value) {
|
||||
playIndex.value--;
|
||||
} else if (index === playIndex.value) {
|
||||
// 如果删除的是当前播放歌曲,则下一曲
|
||||
// 若删除时仅剩一首
|
||||
if (playList.value.length === 1) {
|
||||
cleanPlaylists();
|
||||
return false;
|
||||
}
|
||||
// 若为当前播放
|
||||
if (index === playIndex.value) {
|
||||
playList.value.splice(index, 1);
|
||||
changePlayIndex("next", true);
|
||||
}
|
||||
playList.value.splice(index, 1);
|
||||
// 检查当前播放歌曲的索引是否超出了列表范围
|
||||
if (playIndex.value >= playList.value.length) {
|
||||
playIndex.value = 0;
|
||||
playList.value = [];
|
||||
playSongData.value = {};
|
||||
soundStop();
|
||||
// 若为当前播放之前
|
||||
else if (index < playIndex.value) {
|
||||
playIndex.value--;
|
||||
playList.value.splice(index, 1);
|
||||
}
|
||||
// 若大于当前播放
|
||||
else if (index > playIndex.value) {
|
||||
playList.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -186,146 +224,132 @@ const removeSong = async (index) => {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.alert {
|
||||
height: 48px;
|
||||
.list {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pl-list {
|
||||
:deep(.n-data-table-thead) {
|
||||
display: none;
|
||||
}
|
||||
:deep(.n-data-table-tbody) {
|
||||
.songs-item {
|
||||
padding: 0;
|
||||
.songs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
background-color: var(--n-border-color-modal);
|
||||
transition:
|
||||
transform 0.3s,
|
||||
border-color 0.3s,
|
||||
box-shadow 0.3s,
|
||||
background-color 0.3s;
|
||||
cursor: pointer;
|
||||
.num,
|
||||
.play {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.artist {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
overflow: hidden;
|
||||
.songs-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--n-close-color-hover);
|
||||
transition:
|
||||
transform 0.3s,
|
||||
border-color 0.3s,
|
||||
box-shadow 0.3s,
|
||||
background-color 0.3s;
|
||||
cursor: pointer;
|
||||
.num,
|
||||
.play {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.artist {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
margin-right: 6px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--n-close-color-hover);
|
||||
}
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--main-second-color);
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
.artist {
|
||||
.ar {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.player {
|
||||
background-color: var(--cover-second-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--cover-second-color);
|
||||
border-color: var(--cover-main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--cover-main-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--main-color);
|
||||
box-shadow:
|
||||
0 1px 2px -2px var(--main-boxshadow-color),
|
||||
0 3px 6px 0 var(--main-boxshadow-color),
|
||||
0 5px 12px 4px var(--main-boxshadow-hover-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.995);
|
||||
}
|
||||
}
|
||||
}
|
||||
.n-data-table-tr {
|
||||
&:last-child {
|
||||
.songs {
|
||||
margin-bottom: 0;
|
||||
.delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
margin-right: 6px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--n-close-color-hover);
|
||||
}
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--main-second-color);
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
.artist {
|
||||
.ar {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.player {
|
||||
background-color: var(--cover-second-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
&.play {
|
||||
background-color: var(--cover-second-color);
|
||||
border-color: var(--cover-main-color);
|
||||
a,
|
||||
span,
|
||||
.n-icon {
|
||||
color: var(--cover-main-color) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--cover-main-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.995);
|
||||
}
|
||||
}
|
||||
}
|
||||
.tip {
|
||||
border-radius: 8px;
|
||||
.controls {
|
||||
height: 40px;
|
||||
margin-top: 16px;
|
||||
.n-button {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -336,14 +360,19 @@ const removeSong = async (index) => {
|
||||
.main-playlist {
|
||||
width: 400px !important;
|
||||
border-radius: 12px 0 0 12px;
|
||||
transition: width 0.3s;
|
||||
.n-drawer-header {
|
||||
height: 70px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.n-scrollbar-content {
|
||||
padding: 16px !important;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
&.player {
|
||||
&.full-player {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
.n-drawer-header {
|
||||
@@ -360,5 +389,9 @@ const removeSong = async (index) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
width: 100% !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,12 +35,13 @@ import {
|
||||
useNotification,
|
||||
} from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteSettings, siteData } from "@/stores";
|
||||
import { siteSettings, siteStatus } from "@/stores";
|
||||
import themeColorData from "@/assets/themeColor.json";
|
||||
|
||||
const data = siteData();
|
||||
const osThemeRef = useOsTheme();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const osThemeRef = useOsTheme();
|
||||
const { coverTheme } = storeToRefs(status);
|
||||
const { themeType, themeAuto, themeTypeName, themeTypeData, themeAutoCover } =
|
||||
storeToRefs(settings);
|
||||
|
||||
@@ -63,62 +64,70 @@ const changeTheme = () => {
|
||||
|
||||
// 配置主题色
|
||||
const changeThemeColor = (val, isCover = false) => {
|
||||
const color =
|
||||
isCover && Object.keys(val)?.length
|
||||
try {
|
||||
// 获取主色数据
|
||||
const mainColorData =
|
||||
isCover && Object.keys(val)?.length
|
||||
? val[themeType.value]
|
||||
: val !== "custom"
|
||||
? themeColorData[val]
|
||||
: themeTypeData.value;
|
||||
// 微调主题色
|
||||
const primaryColor = isCover ? `rgb(${mainColorData.bg})` : mainColorData.primaryColor;
|
||||
const primaryColorHover = isCover ? `rgba(${mainColorData.bg}, 0.29)` : primaryColor + "29";
|
||||
const primaryColorPressed = isCover ? `rgba(${mainColorData.bg}, 0.26)` : primaryColor + "26";
|
||||
const primaryColorSuppl = isCover ? `rgba(${mainColorData.bg}, 0.05)` : primaryColor + "05";
|
||||
// 自适应主题背景色
|
||||
const coverAutobgCover = isCover
|
||||
? themeType.value === "dark"
|
||||
? val.light.bg
|
||||
: val.dark.bg
|
||||
: val !== "custom"
|
||||
? themeColorData[val]
|
||||
: themeTypeData.value;
|
||||
// 微调主题色
|
||||
const primaryColor = isCover ? `rgb(${color})` : color.primaryColor;
|
||||
const primaryColorHover = isCover ? `rgba(${color}, 0.29)` : primaryColor + "29";
|
||||
const primaryColorPressed = isCover ? `rgba(${color}, 0.26)` : primaryColor + "26";
|
||||
const primaryColorSuppl = isCover ? `rgba(${color}, 0.05)` : primaryColor + "05";
|
||||
// 自适应主题背景色
|
||||
const coverAutobgCover = isCover
|
||||
? themeType.value === "dark"
|
||||
? val.dark.bg
|
||||
: "239, 239, 239"
|
||||
: null;
|
||||
// 更新主题覆盖
|
||||
themeOverrides.value = {
|
||||
common:
|
||||
isCover && Object.keys(val)?.length
|
||||
? {
|
||||
primaryColor,
|
||||
primaryColorHover,
|
||||
primaryColorPressed,
|
||||
primaryColorSuppl,
|
||||
textColor1: `rgba(${color}, 0.9)`,
|
||||
textColor2: `rgba(${color}, 0.82)`,
|
||||
textColor3: `rgba(${color}, 0.52)`,
|
||||
bodyColor: `rgba(${val.dark.mainBg}, 0.52)`,
|
||||
cardColor: `rgb(${coverAutobgCover})`,
|
||||
tagColor: `rgb(${coverAutobgCover})`,
|
||||
modalColor: `rgb(${coverAutobgCover})`,
|
||||
popoverColor: `rgb(${coverAutobgCover})`,
|
||||
}
|
||||
: color,
|
||||
Icon: { color: isCover ? primaryColor : null },
|
||||
};
|
||||
if (!isCover) themeTypeData.value = color;
|
||||
// 更新全局颜色变量
|
||||
setCssVariable("--main-color", primaryColor);
|
||||
setCssVariable(
|
||||
"--main-color-bg",
|
||||
isCover ? `rgb(${val[themeType.value]?.bg})` : "rgb(16, 16, 20)",
|
||||
);
|
||||
setCssVariable("--main-second-color", primaryColorHover);
|
||||
setCssVariable("--main-boxshadow-color", primaryColorPressed);
|
||||
setCssVariable("--main-boxshadow-hover-color", primaryColorSuppl);
|
||||
: "250, 250, 252"
|
||||
: null;
|
||||
// 更新主题覆盖
|
||||
themeOverrides.value = {
|
||||
common:
|
||||
isCover && Object.keys(val)?.length
|
||||
? themeType.value === "dark"
|
||||
? {
|
||||
primaryColor,
|
||||
primaryColorHover,
|
||||
primaryColorPressed,
|
||||
primaryColorSuppl,
|
||||
textColor1: `rgba(${mainColorData.bg}, 0.9)`,
|
||||
textColor2: `rgba(${mainColorData.bg}, 0.82)`,
|
||||
textColor3: `rgba(${mainColorData.bg}, 0.52)`,
|
||||
bodyColor: `rgb(${val.dark.mainBg})`,
|
||||
cardColor: `rgb(${coverAutobgCover})`,
|
||||
tagColor: `rgb(${coverAutobgCover})`,
|
||||
modalColor: `rgb(${coverAutobgCover})`,
|
||||
popoverColor: `rgb(${coverAutobgCover})`,
|
||||
}
|
||||
: {
|
||||
primaryColor,
|
||||
primaryColorHover,
|
||||
primaryColorPressed,
|
||||
primaryColorSuppl,
|
||||
}
|
||||
: mainColorData,
|
||||
Icon: { color: isCover ? primaryColor : null },
|
||||
};
|
||||
if (!isCover) themeTypeData.value = mainColorData;
|
||||
// 更新全局颜色变量
|
||||
setCssVariable("--main-color", primaryColor);
|
||||
setCssVariable("--main-second-color", primaryColorHover);
|
||||
setCssVariable("--main-boxshadow-color", primaryColorPressed);
|
||||
setCssVariable("--main-boxshadow-hover-color", primaryColorSuppl);
|
||||
} catch (error) {
|
||||
themeOverrides.value = {};
|
||||
console.error("切换主题色出现错误:", error);
|
||||
$message.error("切换主题色出现错误,已使用默认配置");
|
||||
}
|
||||
};
|
||||
|
||||
// 修改全局颜色
|
||||
const setCssVariable = (name, value) => {
|
||||
document.documentElement.style.setProperty(name, value);
|
||||
// document.body.style.setProperty(name, value);
|
||||
// document.documentElement.style.setProperty(name, value);
|
||||
document.body.style.setProperty(name, value);
|
||||
};
|
||||
|
||||
// 挂载 naive 组件
|
||||
@@ -148,8 +157,10 @@ watch(
|
||||
() => {
|
||||
changeTheme();
|
||||
changeThemeColor(
|
||||
themeAutoCover.value ? data.coverTheme : themeTypeName.value,
|
||||
themeAutoCover.value,
|
||||
themeAutoCover.value && Object.keys(coverTheme.value)?.length
|
||||
? coverTheme.value
|
||||
: themeTypeName.value,
|
||||
themeAutoCover.value && Object.keys(coverTheme.value)?.length,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -170,9 +181,8 @@ watch(
|
||||
(val) => changeThemeColor(val.label),
|
||||
);
|
||||
watch(
|
||||
() => data.coverTheme,
|
||||
() => coverTheme.value,
|
||||
(val) => {
|
||||
console.log(val);
|
||||
if (themeAutoCover.value) changeThemeColor(val, themeAutoCover.value);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -293,7 +293,7 @@ const toLikeComment = throttle(
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--n-border-color);
|
||||
background-color: var(--main-second-color);
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<!-- 歌曲列表 -->
|
||||
<template>
|
||||
<Transition name="fade" mode="out-in" @after-enter="checkHasPlaying">
|
||||
<div v-if="data !== 'empty' && data?.length && data[0] !== 'empty'" class="song-list">
|
||||
<div v-if="data?.[0]?.id" class="song-list">
|
||||
<div v-if="showTitle" class="song-list-header">
|
||||
<n-text class="num" depth="3"> # </n-text>
|
||||
<n-text :class="{ info: true, 'has-cover': data[0].cover && showCover }" depth="3">
|
||||
歌曲
|
||||
<n-text :class="['info', { 'has-cover': data[0].cover && showCover }]" depth="3">
|
||||
{{ type === "song" ? "歌曲" : "声音" }}
|
||||
</n-text>
|
||||
<n-text v-if="data[0].album && showAlbum" class="album" depth="3"> 专辑 </n-text>
|
||||
<n-text v-if="data[0].duration" class="duration" depth="3"> 时长 </n-text>
|
||||
<n-text v-if="data[0].size" class="size" depth="3"> 大小 </n-text>
|
||||
<n-text v-if="data[0].album && showAlbum" class="album hidden" depth="3"> 专辑 </n-text>
|
||||
<n-text v-if="data[0].updateTime && type === 'dj'" class="update hidden" depth="3">
|
||||
更新日期
|
||||
</n-text>
|
||||
<n-text v-if="type !== 'dj'" class="control" />
|
||||
<n-text v-if="data[0].playCount && type === 'dj'" class="count hidden" depth="3">
|
||||
播放量
|
||||
</n-text>
|
||||
<n-text v-if="data[0].duration" class="duration hidden" depth="3"> 时长 </n-text>
|
||||
<n-text v-if="data[0].size" class="size hidden" depth="3"> 大小 </n-text>
|
||||
</div>
|
||||
<n-card
|
||||
v-for="(item, index) in data.slice(
|
||||
@@ -25,18 +32,19 @@
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}"
|
||||
:class="music.getPlaySongData?.id === item?.id ? 'songs play' : 'songs'"
|
||||
:class="Number(music.getPlaySongData?.id) === Number(item?.id) ? 'songs play' : 'songs'"
|
||||
hoverable
|
||||
@click="checkCanClick(data, item, songsIndex + index)"
|
||||
@dblclick.stop="playSong(data, item, songsIndex + index)"
|
||||
@contextmenu="
|
||||
songListDropdownRef?.openDropdown($event, data, item, songsIndex + index, sourceId)
|
||||
songListDropdownRef?.openDropdown($event, data, item, songsIndex + index, sourceId, type)
|
||||
"
|
||||
>
|
||||
<!-- 序号 -->
|
||||
<n-text v-if="music.getPlaySongData?.id !== item?.id" class="num" depth="3">
|
||||
{{ songsIndex + index + 1 }}
|
||||
</n-text>
|
||||
<n-icon v-else class="play" size="22">
|
||||
<n-icon v-else class="num" size="22">
|
||||
<SvgIcon icon="music-note" />
|
||||
</n-icon>
|
||||
<!-- 封面 -->
|
||||
@@ -63,7 +71,10 @@
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
<!-- 名称 -->
|
||||
<n-text class="name" depth="2">{{ item?.name || "未知曲目" }}</n-text>
|
||||
<!-- @click.stop="type !== 'dj' && !item.path ? router.push(`/song?id=${item.id}`) : null" -->
|
||||
<n-text class="name" depth="2">
|
||||
{{ item?.name || "未知曲目" }}
|
||||
</n-text>
|
||||
<!-- 特权 -->
|
||||
<n-tag
|
||||
v-if="showPrivilege && item.fee === 1 && userData.detail?.profile?.vipType !== 11"
|
||||
@@ -118,32 +129,78 @@
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click.stop="router.push(`/artist?id=${ar.id}`)"
|
||||
@dblclick.stop
|
||||
>
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div v-else-if="type === 'dj'" class="artist">
|
||||
<n-text class="ar" @dblclick.stop> 电台节目 </n-text>
|
||||
</div>
|
||||
<div v-else class="artist">
|
||||
<n-text class="ar"> {{ item.artists || "未知艺术家" }} </n-text>
|
||||
<n-text class="ar" @dblclick.stop> {{ item.artists || "未知艺术家" }} </n-text>
|
||||
</div>
|
||||
<!-- 别名 -->
|
||||
<n-text v-if="item.alia" class="alia" depth="3">{{ item.alia }}</n-text>
|
||||
</div>
|
||||
<!-- 专辑 -->
|
||||
<template v-if="showAlbum">
|
||||
<template v-if="showAlbum && type !== 'dj'">
|
||||
<n-text
|
||||
v-if="item.album"
|
||||
class="album"
|
||||
@click.stop="item.album !== 'string' ? router.push(`/album?id=${item.album.id}`) : null"
|
||||
class="album hidden"
|
||||
@click.stop="
|
||||
typeof item.album === 'object' ? router.push(`/album?id=${item.album.id}`) : null
|
||||
"
|
||||
@dblclick.stop
|
||||
>
|
||||
{{ typeof item.album === "string" ? item.album : item.album.name }}
|
||||
{{ typeof item.album === "object" ? item.album?.name || "未知专辑" : item.album }}
|
||||
</n-text>
|
||||
<n-text v-else class="album">未知专辑</n-text>
|
||||
<n-text v-else class="album hidden">未知专辑</n-text>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<div v-if="type !== 'dj'" class="action">
|
||||
<!-- 喜欢歌曲 -->
|
||||
<n-icon
|
||||
:depth="dataStore.getSongIsLike(item?.id) ? 0 : 3"
|
||||
class="favorite"
|
||||
size="20"
|
||||
@click.stop="
|
||||
dataStore.changeLikeList(item?.id, !dataStore.getSongIsLike(item?.id), item?.path)
|
||||
"
|
||||
@dblclick.stop
|
||||
>
|
||||
<SvgIcon
|
||||
:icon="
|
||||
dataStore.getSongIsLike(item?.id) ? 'favorite-rounded' : 'favorite-outline-rounded'
|
||||
"
|
||||
/>
|
||||
</n-icon>
|
||||
<!-- 更多操作 -->
|
||||
<n-icon
|
||||
class="more mobile"
|
||||
depth="3"
|
||||
size="20"
|
||||
@click.stop="
|
||||
songListDrawerRef?.drawerOpen(data, item, songsIndex + index, sourceId, type)
|
||||
"
|
||||
@dblclick.stop
|
||||
>
|
||||
<SvgIcon icon="more" />
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 更新日期 -->
|
||||
<n-text v-if="type === 'dj' && item.updateTime" class="update hidden" depth="3">
|
||||
{{ djFormatDate(item.updateTime) }}
|
||||
</n-text>
|
||||
<!-- 播放量 -->
|
||||
<n-text v-if="type === 'dj' && item.playCount" class="count hidden" depth="3">
|
||||
{{ item.playCount }}次
|
||||
</n-text>
|
||||
<!-- 时长 -->
|
||||
<n-text v-if="item.duration" class="duration" depth="3">{{ item.duration }}</n-text>
|
||||
<n-text v-if="item.duration" class="duration hidden" depth="3">{{ item.duration }}</n-text>
|
||||
<n-text v-else class="duration"> -- </n-text>
|
||||
<!-- 大小 -->
|
||||
<n-text v-if="item.size" class="size" depth="3">{{ item.size }}M</n-text>
|
||||
<n-text v-if="item.size" class="size hidden" depth="3">{{ item.size }}M</n-text>
|
||||
</n-card>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
@@ -153,7 +210,21 @@
|
||||
@pageNumberChange="pageNumberChange"
|
||||
/>
|
||||
<!-- 右键菜单 -->
|
||||
<SongListDropdown ref="songListDropdownRef" @playSong="playSong" />
|
||||
<SongListDropdown
|
||||
ref="songListDropdownRef"
|
||||
@playSong="playSong"
|
||||
@delCloudSong="delCloudSong"
|
||||
@deletePlaylistSong="deletePlaylistSong"
|
||||
@delLocalSong="delLocalSong"
|
||||
/>
|
||||
<!-- 移动端菜单 -->
|
||||
<SongListDrawer
|
||||
ref="songListDrawerRef"
|
||||
@playSong="playSong"
|
||||
@delCloudSong="delCloudSong"
|
||||
@deletePlaylistSong="deletePlaylistSong"
|
||||
@delLocalSong="delLocalSong"
|
||||
/>
|
||||
<!-- 定位歌曲 -->
|
||||
<Transition name="shrink" mode="out-in">
|
||||
<n-card
|
||||
@@ -181,30 +252,46 @@
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
<!-- 错误 -->
|
||||
<n-empty
|
||||
v-else-if="data === 'error' || data?.[0] === 'error'"
|
||||
description="列表获取出错,请重试"
|
||||
style="margin-top: 60px"
|
||||
size="large"
|
||||
/>
|
||||
<!-- 加载动画 -->
|
||||
<n-spin v-else class="loading" size="small">
|
||||
<template #description> 加载中 </template>
|
||||
</n-spin>
|
||||
<div v-else class="loading">
|
||||
<n-skeleton :repeat="10" text />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { siteData, siteSettings, musicData } from "@/stores";
|
||||
import { setCloudDel } from "@/api/cloud";
|
||||
import { addSongToPlayList } from "@/api/playlist";
|
||||
import { siteData, siteSettings, musicData, siteStatus } from "@/stores";
|
||||
import { initPlayer, fadePlayOrPause, addSongToNext } from "@/utils/Player";
|
||||
import { djFormatDate } from "@/utils/timeTools";
|
||||
|
||||
const router = useRouter();
|
||||
const music = musicData();
|
||||
const dataStore = siteData();
|
||||
const status = siteStatus();
|
||||
const settings = siteSettings();
|
||||
const { userData } = storeToRefs(dataStore);
|
||||
const { loadSize } = storeToRefs(settings);
|
||||
const { playList, playIndex, playSongData, playSongSource, playHeartbeatMode, playMode } =
|
||||
storeToRefs(music);
|
||||
const { loadSize, playSearch, useMusicCache } = storeToRefs(settings);
|
||||
const { playList, playSongData, playSongSource } = storeToRefs(music);
|
||||
const { playIndex, playMode, playHeartbeatMode, playLoading } = storeToRefs(status);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const props = defineProps({
|
||||
// 列表类型
|
||||
type: {
|
||||
type: String,
|
||||
default: "song",
|
||||
},
|
||||
// 列表数据
|
||||
data: {
|
||||
type: [Array, String],
|
||||
@@ -245,7 +332,8 @@ const props = defineProps({
|
||||
// 分页数据
|
||||
const pageNumber = ref(1);
|
||||
|
||||
// 右键菜单
|
||||
// 子组件
|
||||
const songListDrawerRef = ref(null);
|
||||
const songListDropdownRef = ref(null);
|
||||
|
||||
// 当前索引
|
||||
@@ -272,8 +360,13 @@ const checkHasPlaying = (isScoll = null) => {
|
||||
// 播放歌曲
|
||||
const playSong = async (data, song, index) => {
|
||||
console.log(data, song, index);
|
||||
// 若开启了缓存且正在加载
|
||||
if (useMusicCache.value && playLoading.value) {
|
||||
$message.warning("歌曲正在缓冲中,请稍后");
|
||||
return false;
|
||||
}
|
||||
// 更改模式
|
||||
playMode.value = "normal";
|
||||
playMode.value = props.type === "song" ? "normal" : "dj";
|
||||
// 检查当前页面
|
||||
const isPage = router.currentRoute.value.matched?.[0].path || null;
|
||||
// 是否关闭心动模式
|
||||
@@ -284,7 +377,12 @@ const playSong = async (data, song, index) => {
|
||||
fadePlayOrPause();
|
||||
} else {
|
||||
// 若为特殊状态
|
||||
if (isPage === "/search" || isPage === "/history" || playHeartbeatMode.value) {
|
||||
if (
|
||||
(isPage === "/search" && !playSearch.value) ||
|
||||
isPage === "/history" ||
|
||||
playHeartbeatMode.value
|
||||
) {
|
||||
console.log("仅播放当前歌曲");
|
||||
addSongToNext(song, true);
|
||||
} else {
|
||||
// 添加播放列表
|
||||
@@ -295,7 +393,7 @@ const playSong = async (data, song, index) => {
|
||||
console.log("与当前播放歌曲不一致");
|
||||
playSongData.value = song;
|
||||
// 初始化播放器
|
||||
initPlayer(true);
|
||||
await initPlayer(true);
|
||||
}
|
||||
// 附加来源
|
||||
playSongSource.value = Number(props.sourceId);
|
||||
@@ -313,6 +411,72 @@ const pageNumberChange = (page) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 检查是否可执行双击
|
||||
const checkCanClick = (data, item, index) => {
|
||||
if (window.innerWidth > 700) return false;
|
||||
playSong(data, item, index);
|
||||
};
|
||||
|
||||
// 云盘歌曲删除
|
||||
const delCloudSong = (data, song, index) => {
|
||||
console.log(data, song, index);
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从云盘中删除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
const result = await setCloudDel(song.id);
|
||||
if (result.code == 200) {
|
||||
data.splice(index, 1);
|
||||
$message.success("删除成功");
|
||||
} else {
|
||||
$message.error("删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 歌单歌曲删除
|
||||
const deletePlaylistSong = (pid, song, data, index) => {
|
||||
if (!pid || !song) return $message.error("无法正确定位到歌单,请重试");
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从歌单中移除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
const result = await addSongToPlayList(pid, song?.id, "del");
|
||||
if (result.status === 200) {
|
||||
data.length === 1 ? data.splice(0, 1, "empty") : data.splice(index, 1);
|
||||
$message.success("歌曲删除成功");
|
||||
} else {
|
||||
$message.error("歌曲删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 本地歌曲删除
|
||||
const delLocalSong = (data, song, index) => {
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从本地磁盘中删除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
console.log(data, song, index);
|
||||
const result = await electron.ipcRenderer.invoke("deleteFile", song?.path);
|
||||
if (result) {
|
||||
data.length === 1 ? data.splice(0, 1, "empty") : data.splice(index, 1);
|
||||
$message.success("歌曲删除成功");
|
||||
} else {
|
||||
$message.error("歌曲删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 监听歌曲变化
|
||||
watch(
|
||||
() => music.getPlaySongData?.id,
|
||||
@@ -352,8 +516,20 @@ onBeforeUnmount(() => {
|
||||
.has-cover {
|
||||
margin-right: 66px;
|
||||
}
|
||||
.duration {
|
||||
.control {
|
||||
width: 40px;
|
||||
}
|
||||
.update {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
margin-right: auto;
|
||||
}
|
||||
.count {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.duration {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
.size {
|
||||
@@ -394,8 +570,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.num,
|
||||
.play {
|
||||
.num {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
@@ -483,8 +658,36 @@ onBeforeUnmount(() => {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
.duration {
|
||||
.action {
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
.favorite {
|
||||
padding-top: 1px;
|
||||
transition: transform 0.3s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.more {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.update {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
.count {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.duration {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
.size {
|
||||
@@ -496,7 +699,7 @@ onBeforeUnmount(() => {
|
||||
border-color: var(--main-color);
|
||||
a,
|
||||
span,
|
||||
.play {
|
||||
.num {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
.artist {
|
||||
@@ -546,12 +749,49 @@ onBeforeUnmount(() => {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.song-list-header,
|
||||
.songs {
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.songs {
|
||||
.num {
|
||||
font-size: 12px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
}
|
||||
.info {
|
||||
.title {
|
||||
.name {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.artist {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.action {
|
||||
width: 60px;
|
||||
justify-content: flex-end;
|
||||
.more {
|
||||
display: inline-block;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.loading {
|
||||
margin: 60px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
:deep(.n-skeleton) {
|
||||
&:nth-of-type(1) {
|
||||
margin-top: 0;
|
||||
}
|
||||
height: 80px;
|
||||
margin-top: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
323
src/components/List/SongListDrawer.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<!-- 歌曲列表 - 移动端菜单 -->
|
||||
<template>
|
||||
<n-drawer
|
||||
v-model:show="drawerShow"
|
||||
:auto-focus="false"
|
||||
height="calc(100vh - 200px)"
|
||||
placement="bottom"
|
||||
class="song-list-drawer"
|
||||
@after-leave="drawerShow = false"
|
||||
@mask-click="drawerShow = false"
|
||||
>
|
||||
<n-drawer-content :native-scrollbar="false" :body-content-style="{ padding: 0 }" closable>
|
||||
<template #header>
|
||||
<div v-if="!songData?.path" class="song-data">
|
||||
<n-image
|
||||
:src="songData?.coverSize?.s || songData?.cover"
|
||||
class="cover"
|
||||
preview-disabled
|
||||
/>
|
||||
<div class="song-detail">
|
||||
<n-text class="name">{{ songData?.name || "未知曲目" }}</n-text>
|
||||
<template v-if="songType === 'song'">
|
||||
<div v-if="songData?.artists && Array.isArray(songData.artists)" class="all-ar">
|
||||
<n-text v-for="ar in songData.artists" :key="ar.id" class="ar" depth="3">
|
||||
{{ ar.name }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div v-else class="all-ar">
|
||||
<n-text class="ar" depth="3">
|
||||
{{ songData.artists || "未知艺术家" }}
|
||||
</n-text>
|
||||
</div>
|
||||
</template>
|
||||
<n-text v-else class="ar">
|
||||
{{ songData?.artists || "未知艺术家" }}
|
||||
</n-text>
|
||||
</div>
|
||||
</div>
|
||||
<n-text v-else>更多操作</n-text>
|
||||
</template>
|
||||
<div class="all-menu">
|
||||
<div
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
emit('playSong', playlistData, songData, songIndex);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="play" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 立即播放 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSong && playMode !== 'dj' && music.getPlaySongData?.id !== songData.id && !isFm"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
playMode = 'normal';
|
||||
addSongToNext(songData);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="play-next" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 下一首播放 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSong && !isLocalSong"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
addPlaylistRef?.openAddToPlaylist(songData?.id);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="playlist-add" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 添加到歌单 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSong && !isLocalSong"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
router.push({
|
||||
path: '/comment',
|
||||
query: {
|
||||
id: songData.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="20">
|
||||
<SvgIcon icon="comment-text" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 查看评论 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSong && isHasMv"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
router.push({
|
||||
path: '/videos-player',
|
||||
query: {
|
||||
id: songData.mv,
|
||||
},
|
||||
});
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="video" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 观看 MV </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isCloud && isUserPlaylist"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
emit('deletePlaylistSong', songSourceId, songData, playlistData, songIndex);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="delete" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 从歌单中删除 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isCloud"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
emit('delCloudSong', playlistData, songData, songIndex);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="delete" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 从云盘中删除 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isCloud"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
cloudSongMatchRef?.openCloudSongMatch(songData, songIndex);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="edit" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 云盘歌曲纠正 </n-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSong && !isLocalSong"
|
||||
class="menu-item"
|
||||
@click="
|
||||
() => {
|
||||
drawerShow = false;
|
||||
downloadSongRef?.openDownloadModal(songData);
|
||||
}
|
||||
"
|
||||
>
|
||||
<n-icon size="22">
|
||||
<SvgIcon icon="download" />
|
||||
</n-icon>
|
||||
<n-text class="name"> 下载歌曲 </n-text>
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
<!-- 添加到歌单 -->
|
||||
<AddPlaylist ref="addPlaylistRef" />
|
||||
<!-- 下载歌曲 -->
|
||||
<DownloadSong ref="downloadSongRef" />
|
||||
<!-- 云盘歌曲纠正 -->
|
||||
<CloudSongMatch ref="cloudSongMatchRef" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { addSongToNext } from "@/utils/Player";
|
||||
import { musicData, siteData, siteStatus } from "@/stores";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const music = musicData();
|
||||
const status = siteStatus();
|
||||
const { playMode } = storeToRefs(status);
|
||||
const { userData, userLikeData } = storeToRefs(data);
|
||||
const emit = defineEmits(["playSong", "delCloudSong", "deletePlaylistSong", "delLocalSong"]);
|
||||
|
||||
// 子组件
|
||||
const addPlaylistRef = ref(null);
|
||||
const downloadSongRef = ref(null);
|
||||
const cloudSongMatchRef = ref(null);
|
||||
|
||||
// 菜单数据
|
||||
const drawerShow = ref(false);
|
||||
const songType = ref("song");
|
||||
const songData = ref(null);
|
||||
const songIndex = ref(null);
|
||||
const songSourceId = ref(null);
|
||||
const playlistData = ref(null);
|
||||
|
||||
// 歌曲状态
|
||||
const isFm = computed(() => playMode.value === "fm");
|
||||
const isSong = computed(() => songType.value === "song");
|
||||
const isLocalSong = computed(() => !!songData.value?.path);
|
||||
const isHasMv = computed(() => !!songData.value?.mv && songData.value.mv !== 0);
|
||||
const isCloud = computed(() => router.currentRoute.value.name === "cloud");
|
||||
const isUserPlaylist = computed(() => {
|
||||
// 用户 id
|
||||
const userId = userData.value?.userId;
|
||||
// 用户歌单
|
||||
const userPlaylistsData = userLikeData.value.playlists?.filter(
|
||||
(playlist) => playlist.userId === userId,
|
||||
);
|
||||
return songSourceId.value !== 0 && userPlaylistsData.some((pl) => pl.id == songSourceId.value);
|
||||
});
|
||||
|
||||
// 开启菜单
|
||||
const drawerOpen = (data, song, index, sourceId, type) => {
|
||||
console.log(song, type);
|
||||
drawerShow.value = true;
|
||||
songData.value = song;
|
||||
songType.value = type;
|
||||
songIndex.value = index;
|
||||
songSourceId.value = sourceId;
|
||||
playlistData.value = data;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
drawerOpen,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.song-data {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.cover {
|
||||
margin-right: 12px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.song-detail {
|
||||
.name {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.all-ar {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
display: inline-flex;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.all-menu {
|
||||
.menu-item {
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
.n-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.name {
|
||||
transform: translateY(1px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--n-close-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.song-list-drawer {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -17,23 +17,25 @@
|
||||
<AddPlaylist ref="addPlaylistRef" />
|
||||
<!-- 下载歌曲 -->
|
||||
<DownloadSong ref="downloadSongRef" />
|
||||
<!-- 云盘歌曲纠正 -->
|
||||
<CloudSongMatch ref="cloudSongMatchRef" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NIcon, NImage, NText } from "naive-ui";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { musicData, siteData } from "@/stores";
|
||||
import { musicData, siteData, siteStatus } from "@/stores";
|
||||
import { useRouter } from "vue-router";
|
||||
import { addSongToNext } from "@/utils/Player";
|
||||
import { setCloudDel } from "@/api/cloud";
|
||||
import { addSongToPlayList } from "@/api/playlist";
|
||||
import { copyData } from "@/utils/helper";
|
||||
import SvgIcon from "@/components/Global/SvgIcon";
|
||||
|
||||
const emit = defineEmits(["playSong"]);
|
||||
const router = useRouter();
|
||||
const music = musicData();
|
||||
const emit = defineEmits(["playSong", "delCloudSong", "deletePlaylistSong", "delLocalSong"]);
|
||||
const data = siteData();
|
||||
const { playSongData, playMode } = storeToRefs(music);
|
||||
const music = musicData();
|
||||
const router = useRouter();
|
||||
const status = siteStatus();
|
||||
const { playMode } = storeToRefs(status);
|
||||
const { userData, userLikeData } = storeToRefs(data);
|
||||
|
||||
// 右键菜单数据
|
||||
@@ -45,6 +47,7 @@ const dropdownOptions = ref(null);
|
||||
// 子组件
|
||||
const addPlaylistRef = ref(null);
|
||||
const downloadSongRef = ref(null);
|
||||
const cloudSongMatchRef = ref(null);
|
||||
|
||||
// 图标渲染
|
||||
const renderIcon = (icon, size, translate = 0) => {
|
||||
@@ -60,7 +63,7 @@ const renderIcon = (icon, size, translate = 0) => {
|
||||
};
|
||||
|
||||
// 歌曲信息
|
||||
const renderSong = (song) => {
|
||||
const renderSong = (song, isSong) => {
|
||||
return () =>
|
||||
h(
|
||||
"div",
|
||||
@@ -68,29 +71,35 @@ const renderSong = (song) => {
|
||||
className: "song-data",
|
||||
},
|
||||
[
|
||||
h(NImage, { src: song?.coverSize?.s || song?.cover, class: "cover" }),
|
||||
h(NImage, {
|
||||
src: song?.coverSize?.s || song?.cover,
|
||||
class: "cover",
|
||||
previewDisabled: true,
|
||||
}),
|
||||
h("div", { class: "song-detail" }, [
|
||||
h(NText, { class: "name" }, () => [song?.name || "未知曲目"]),
|
||||
song.artists && Array.isArray(song.artists)
|
||||
? h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
song.artists.map((ar) =>
|
||||
h(NText, { key: ar.id, class: "ar", depth: 3 }, () => [ar.name]),
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
h(NText, { class: "ar", depth: 3 }, () => [song.artists || "未知艺术家"]),
|
||||
),
|
||||
isSong
|
||||
? song.artists && Array.isArray(song.artists)
|
||||
? h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
song.artists.map((ar) =>
|
||||
h(NText, { key: ar.id, class: "ar", depth: 3 }, () => [ar.name]),
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
{ class: "all-ar" },
|
||||
h(NText, { class: "ar", depth: 3 }, () => [song.artists || "未知艺术家"]),
|
||||
)
|
||||
: h(NText, { class: "ar", depth: 3 }, () => ["电台节目"]),
|
||||
]),
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// 打开右键菜单
|
||||
const openDropdown = (e, data, song, index, sourceId) => {
|
||||
const openDropdown = (e, data, song, index, sourceId, type) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
dropdownShow.value = false;
|
||||
@@ -101,7 +110,10 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
(playlist) => playlist.userId === userId,
|
||||
);
|
||||
// 当前状态
|
||||
const isLocalSong = song?.path ? true : false;
|
||||
const isFm = playMode.value === "fm";
|
||||
const isSong = type === "song";
|
||||
const isLocalSong = !!song?.path;
|
||||
const isHasMv = !!song?.mv && song.mv !== 0;
|
||||
const isCloud = router.currentRoute.value.name === "cloud";
|
||||
const isUserPlaylist = sourceId !== 0 && userPlaylistsData.some((pl) => pl.id == sourceId);
|
||||
// 生成菜单
|
||||
@@ -112,7 +124,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
key: "song-data",
|
||||
type: "render",
|
||||
show: !isLocalSong,
|
||||
render: renderSong(song),
|
||||
render: renderSong(song, isSong),
|
||||
},
|
||||
{
|
||||
key: "line-song",
|
||||
@@ -132,9 +144,10 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "next-play",
|
||||
label: "下一首播放",
|
||||
show: playSongData.value?.id === song.id || playMode.value === "fm" ? false : true,
|
||||
show: isSong && playMode.value !== "dj" && music.getPlaySongData?.id !== song.id && !isFm,
|
||||
props: {
|
||||
onClick: () => {
|
||||
playMode.value = "normal";
|
||||
addSongToNext(song);
|
||||
},
|
||||
},
|
||||
@@ -143,7 +156,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "add-pl",
|
||||
label: "添加到歌单",
|
||||
show: song?.path ? false : true,
|
||||
show: isSong && !isLocalSong,
|
||||
props: {
|
||||
onClick: () => {
|
||||
addPlaylistRef.value?.openAddToPlaylist(song?.id);
|
||||
@@ -154,7 +167,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "comment",
|
||||
label: "查看评论",
|
||||
show: song?.path ? false : true,
|
||||
show: isSong && !isLocalSong,
|
||||
props: {
|
||||
onClick: () => {
|
||||
router.push({
|
||||
@@ -170,7 +183,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "mv",
|
||||
label: "观看 MV",
|
||||
show: song.mv && song.mv !== 0 ? true : false,
|
||||
show: isSong && isHasMv,
|
||||
props: {
|
||||
onClick: () => {
|
||||
router.push({
|
||||
@@ -183,6 +196,39 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
},
|
||||
icon: renderIcon("video"),
|
||||
},
|
||||
{
|
||||
label: "更多操作",
|
||||
key: "others",
|
||||
show: !isLocalSong,
|
||||
icon: renderIcon("more"),
|
||||
children: [
|
||||
{
|
||||
key: "copy",
|
||||
label: `复制${isSong ? "歌曲" : "节目"} ID`,
|
||||
props: {
|
||||
onClick: () => {
|
||||
const songId = song?.id?.toString();
|
||||
copyData(songId);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("content-copy"),
|
||||
},
|
||||
{
|
||||
key: "share",
|
||||
label: `分享${isSong ? "歌曲" : "节目"}链接`,
|
||||
props: {
|
||||
onClick: () => {
|
||||
const shareUrl = isSong
|
||||
? `https://music.163.com/song?id=${song?.id?.toString()}`
|
||||
: `https://music.163.com/#/dj?id=${song?.id?.toString()}`;
|
||||
copyData(shareUrl, `复制${isSong ? "歌曲" : "节目"}链接`);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("share"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
key: "line-cloud",
|
||||
type: "divider",
|
||||
@@ -194,18 +240,29 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
show: !isCloud && isUserPlaylist,
|
||||
props: {
|
||||
onClick: () => {
|
||||
deletePlaylistSong(sourceId, song, data, index);
|
||||
emit("deletePlaylistSong", sourceId, song, data, index);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("delete"),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "云盘歌曲纠正",
|
||||
show: isCloud,
|
||||
props: {
|
||||
onClick: () => {
|
||||
cloudSongMatchRef.value?.openCloudSongMatch(song, index);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("edit"),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "从云盘中删除",
|
||||
show: isCloud,
|
||||
props: {
|
||||
onClick: () => {
|
||||
delCloudSong(data, song, index);
|
||||
emit("delCloudSong", data, song, index);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("delete"),
|
||||
@@ -224,10 +281,10 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "delete",
|
||||
label: "从本地磁盘中删除",
|
||||
show: isLocalSong,
|
||||
show: isLocalSong && music.getPlaySongData?.id !== song.id,
|
||||
props: {
|
||||
onClick: () => {
|
||||
delLocalSong(data, song, index);
|
||||
emit("delLocalSong", data, song, index);
|
||||
},
|
||||
},
|
||||
icon: renderIcon("delete"),
|
||||
@@ -254,7 +311,7 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
{
|
||||
key: "download",
|
||||
label: "下载歌曲",
|
||||
show: !isLocalSong,
|
||||
show: isSong && !isLocalSong,
|
||||
props: {
|
||||
onClick: () => {
|
||||
downloadSongRef.value?.openDownloadModal(song);
|
||||
@@ -274,65 +331,6 @@ const openDropdown = (e, data, song, index, sourceId) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 云盘歌曲删除
|
||||
const delCloudSong = (data, song, index) => {
|
||||
console.log(data, song, index);
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从云盘中删除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
const result = await setCloudDel(song.id);
|
||||
if (result.code == 200) {
|
||||
data.splice(index, 1);
|
||||
$message.success("删除成功");
|
||||
} else {
|
||||
$message.error("删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 歌单歌曲删除
|
||||
const deletePlaylistSong = (pid, song, data, index) => {
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从歌单中移除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
const result = await addSongToPlayList(pid, song?.id, "del");
|
||||
if (result.status === 200) {
|
||||
data.length === 1 ? data.splice(0, 1, "empty") : data.splice(index, 1);
|
||||
$message.success("歌曲删除成功");
|
||||
} else {
|
||||
$message.error("歌曲删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 本地歌曲删除
|
||||
const delLocalSong = (data, song, index) => {
|
||||
$dialog.warning({
|
||||
title: "确认删除",
|
||||
content: `确认从本地磁盘中删除 ${song.name}?该操作无法撤销!`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
console.log(data, song, index);
|
||||
const result = await electron.ipcRenderer.invoke("deleteFile", song?.path);
|
||||
if (result) {
|
||||
data.length === 1 ? data.splice(0, 1, "empty") : data.splice(index, 1);
|
||||
$message.success("歌曲删除成功");
|
||||
} else {
|
||||
$message.error("歌曲删除失败,请重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openDropdown,
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
>
|
||||
<template #prefix>
|
||||
<n-image
|
||||
:src="item.coverImgUrl.replace(/^http:/, 'https:') + '?param=100y100'"
|
||||
:src="item?.coverSize?.s || '/images/pic/album.jpg?assest'"
|
||||
class="cover"
|
||||
preview-disabled
|
||||
lazy
|
||||
@@ -51,7 +51,7 @@
|
||||
</template>
|
||||
<n-thing :title="item.name">
|
||||
<template #description>
|
||||
<n-text depth="3" class="size">{{ item.trackCount }} 首音乐</n-text>
|
||||
<n-text depth="3" class="size">{{ item.count }} 首音乐</n-text>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
@@ -87,7 +87,7 @@ const addToPlayList = async (pid, tracks, index) => {
|
||||
$message.success("添加歌曲至歌单成功");
|
||||
if (index === 0) await data.setUserLikeSongs();
|
||||
} else {
|
||||
$message.error("添加失败,请重试");
|
||||
$message.error(result?.message || "添加失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
248
src/components/Modal/CloudSongMatch.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<!-- 云盘歌曲纠正 -->
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="cloudSongMatchShow"
|
||||
:bordered="false"
|
||||
:on-after-leave="closeCloudSongMatch"
|
||||
title="歌曲纠正"
|
||||
preset="card"
|
||||
>
|
||||
<n-form class="cloud-match" :label-width="80" :model="cloudMatchValue">
|
||||
<n-form-item label="原歌曲 ID" path="asid">
|
||||
<n-input-number v-model:value="cloudMatchValue.sid" :show-button="false" disabled />
|
||||
</n-form-item>
|
||||
<n-form-item label="匹配的 ID" path="asid">
|
||||
<n-input-number
|
||||
v-model:value="cloudMatchValue.asid"
|
||||
:show-button="false"
|
||||
placeholder="请输入要匹配的歌曲 ID"
|
||||
/>
|
||||
<n-button
|
||||
:disabled="!cloudMatchValue.asid"
|
||||
style="margin-left: 12px"
|
||||
@click="checkMatchSong(cloudMatchValue.asid)"
|
||||
>
|
||||
检查
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<!-- 纠正歌曲数据 -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-card
|
||||
v-if="cloudMatchSongData"
|
||||
:key="cloudMatchSongData"
|
||||
:content-style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
height: '100%',
|
||||
}"
|
||||
class="song-detail"
|
||||
>
|
||||
<n-image
|
||||
:src="cloudMatchSongData?.coverSize?.s || cloudMatchSongData?.cover"
|
||||
class="cover-img"
|
||||
preview-disabled
|
||||
@load="
|
||||
(e) => {
|
||||
e.target.style.opacity = 1;
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="cover-loading">
|
||||
<img class="loading-img" src="/images/pic/song.jpg?assest" alt="loading-img" />
|
||||
</div>
|
||||
</template>
|
||||
</n-image>
|
||||
<div class="content">
|
||||
<div class="name">{{ cloudMatchSongData?.name || "未知曲目" }}</div>
|
||||
<div class="artist">
|
||||
<n-icon depth="3" size="20">
|
||||
<SvgIcon icon="account-music" />
|
||||
</n-icon>
|
||||
<div
|
||||
v-if="cloudMatchSongData?.artists && Array.isArray(cloudMatchSongData?.artists)"
|
||||
class="all-ar"
|
||||
>
|
||||
<span v-for="ar in cloudMatchSongData.artists" :key="ar.id" class="ar">
|
||||
{{ ar.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="all-ar">
|
||||
<span class="ar"> {{ cloudMatchSongData?.artists || "未知艺术家" }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</Transition>
|
||||
<template #footer>
|
||||
<n-flex justify="end">
|
||||
<n-button @click="closeCloudSongMatch"> 取消 </n-button>
|
||||
<n-button
|
||||
:disabled="!cloudMatchValue.asid"
|
||||
type="primary"
|
||||
@click="setCloudSongMatchBtn(cloudMatchValue)"
|
||||
>
|
||||
纠正
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteData, indexedDBData } from "@/stores";
|
||||
import { setCloudMatch } from "@/api/cloud";
|
||||
import { getSongDetail } from "@/api/song";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const data = siteData();
|
||||
const indexedDB = indexedDBData();
|
||||
const { userData } = storeToRefs(data);
|
||||
|
||||
// 歌曲信息纠正数据
|
||||
const cloudMatchId = ref(null);
|
||||
const cloudMatchIndex = ref(null);
|
||||
const cloudSongMatchShow = ref(false);
|
||||
const cloudMatchSongData = ref(null);
|
||||
const cloudMatchValue = ref({
|
||||
uid: userData.value?.userId,
|
||||
sid: null,
|
||||
asid: null,
|
||||
});
|
||||
|
||||
// 检查纠正歌曲id
|
||||
const checkMatchSong = async (id) => {
|
||||
try {
|
||||
if (!id) return false;
|
||||
const songId = id.toString();
|
||||
const result = await getSongDetail(songId);
|
||||
if (result.code === 200 && result.songs.length > 0) {
|
||||
$message.success("匹配的歌曲检查通过");
|
||||
cloudMatchSongData.value = formatData(result.songs[0], "song")[0];
|
||||
} else {
|
||||
$message.warning("请检查要匹配的歌曲 ID 是否正确");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("检查纠正歌曲失败:", error);
|
||||
$message.error("检查纠正歌曲失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 歌曲纠正
|
||||
const setCloudSongMatchBtn = async (data) => {
|
||||
if (Number(data.sid) === Number(data.asid)) {
|
||||
return $message.warning("与原歌曲 ID 一致,无需修改");
|
||||
}
|
||||
if (!cloudMatchSongData.value) {
|
||||
return $message.warning("未检测到正确的匹配检查结果");
|
||||
}
|
||||
const result = await setCloudMatch(data.uid, data.sid, data.asid);
|
||||
console.log(result);
|
||||
if (result.code === 200) {
|
||||
// 更改歌曲信息
|
||||
try {
|
||||
cloudMatchSongData.value.pc = undefined;
|
||||
const allCloudSongs = await indexedDB.getfilesDB("userCloudList");
|
||||
allCloudSongs[cloudMatchIndex.value] = JSON.parse(JSON.stringify(cloudMatchSongData.value));
|
||||
await indexedDB.setfilesDB("userCloudList", allCloudSongs.slice());
|
||||
// 刷新列表
|
||||
if (typeof $refreshCloudCatch !== "undefined") $refreshCloudCatch();
|
||||
} catch (error) {
|
||||
console.error("更改云盘列表时出错:", error);
|
||||
$message.error("更改云盘列表时出错,请刷新后重试");
|
||||
}
|
||||
closeCloudSongMatch();
|
||||
$message.success("歌曲信息纠正成功");
|
||||
} else {
|
||||
$message.error("歌曲信息纠正失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 开启歌曲纠正
|
||||
const openCloudSongMatch = (data, index) => {
|
||||
cloudMatchIndex.value = index;
|
||||
cloudMatchValue.value.sid = data.id;
|
||||
cloudSongMatchShow.value = true;
|
||||
};
|
||||
|
||||
// 关闭歌曲纠正
|
||||
const closeCloudSongMatch = () => {
|
||||
cloudSongMatchShow.value = false;
|
||||
cloudMatchId.value = null;
|
||||
cloudMatchValue.value.asid = null;
|
||||
cloudMatchSongData.value = null;
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
openCloudSongMatch,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cloud-match {
|
||||
:deep(.n-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.song-detail {
|
||||
height: 100px;
|
||||
border-radius: 8px;
|
||||
.cover-img {
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
margin-right: 16px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 10px 6px #00000008;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
.name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.artist {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.n-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.all-ar {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.ar {
|
||||
display: inline-flex;
|
||||
&::after {
|
||||
content: "/";
|
||||
margin: 0 4px;
|
||||
}
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,7 @@
|
||||
<!-- 隐私歌单 -->
|
||||
<n-checkbox v-model:checked="createPrivacy"> 设为隐私歌单 </n-checkbox>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="closeCreatePlaylist"> 取消 </n-button>
|
||||
<n-button
|
||||
:disabled="!createName"
|
||||
@@ -30,7 +30,7 @@
|
||||
>
|
||||
新建
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
当前为云盘歌曲,下载的文件均为最高音质
|
||||
</n-alert>
|
||||
<n-radio-group v-model:value="downloadChoose" class="download-group" name="downloadGroup">
|
||||
<n-space vertical>
|
||||
<n-flex vertical>
|
||||
<n-radio
|
||||
v-for="item in downloadLevel"
|
||||
:key="item"
|
||||
@@ -32,23 +32,24 @@
|
||||
</n-text>
|
||||
</div>
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
<n-text v-else>歌曲信息获取中</n-text>
|
||||
</Transition>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="closeDownloadModal"> 关闭 </n-button>
|
||||
<n-button
|
||||
:disabled="!downloadChoose"
|
||||
:loading="downloadStatus"
|
||||
:focusable="false"
|
||||
type="primary"
|
||||
@click="toSongDownload(songData, downloadChoose)"
|
||||
@click="toSongDownload(songData, lyricData, downloadChoose)"
|
||||
>
|
||||
下载
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
@@ -58,19 +59,20 @@ import { storeToRefs } from "pinia";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import { useRouter } from "vue-router";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import { getSongDetail, getSongDownload } from "@/api/song";
|
||||
import { downloadFile } from "@/utils/helper";
|
||||
import { getSongDetail, getSongDownload, getSongLyric } from "@/api/song";
|
||||
import { downloadFile, checkPlatform } from "@/utils/helper";
|
||||
import formatData from "@/utils/formatData";
|
||||
|
||||
const router = useRouter();
|
||||
const data = siteData();
|
||||
const settings = siteSettings();
|
||||
const { userData } = storeToRefs(data);
|
||||
const { downloadPath } = storeToRefs(settings);
|
||||
const { downloadPath, downloadMeta, downloadCover, downloadLyrics } = storeToRefs(settings);
|
||||
|
||||
// 歌曲下载数据
|
||||
const songId = ref(null);
|
||||
const songData = ref(null);
|
||||
const lyricData = ref(null);
|
||||
const downloadStatus = ref(false);
|
||||
const downloadSongShow = ref(false);
|
||||
const downloadChoose = ref(null);
|
||||
@@ -79,11 +81,13 @@ const downloadLevel = ref(null);
|
||||
// 获取歌曲详情
|
||||
const getMusicDetailData = async (id) => {
|
||||
try {
|
||||
const result = await getSongDetail(id);
|
||||
const songResult = await getSongDetail(id);
|
||||
const lyricResult = await getSongLyric(id);
|
||||
// 获取歌曲详情
|
||||
songData.value = formatData(result?.songs?.[0], "song")[0];
|
||||
songData.value = formatData(songResult?.songs?.[0], "song")[0];
|
||||
lyricData.value = lyricResult?.lrc?.lyric || null;
|
||||
// 生成音质列表
|
||||
generateLists(result);
|
||||
generateLists(songResult);
|
||||
} catch (error) {
|
||||
closeDownloadModal();
|
||||
console.error("歌曲信息获取失败:", error);
|
||||
@@ -91,26 +95,42 @@ const getMusicDetailData = async (id) => {
|
||||
};
|
||||
|
||||
// 歌曲下载
|
||||
const toSongDownload = async (song, br) => {
|
||||
console.log(song, br);
|
||||
downloadStatus.value = true;
|
||||
// 获取下载数据
|
||||
const result = await getSongDownload(song?.id, br);
|
||||
// 开始下载
|
||||
if (!downloadPath.value) {
|
||||
$notification["warning"]({
|
||||
content: "缺少配置",
|
||||
meta: "请前往设置页配置默认下载目录",
|
||||
duration: 3000,
|
||||
const toSongDownload = async (song, lyric, br) => {
|
||||
try {
|
||||
console.log(song, lyric, br);
|
||||
downloadStatus.value = true;
|
||||
// 获取下载数据
|
||||
const result = await getSongDownload(song?.id, br);
|
||||
// 开始下载
|
||||
if (!downloadPath.value && checkPlatform.electron()) {
|
||||
$notification["warning"]({
|
||||
content: "缺少配置",
|
||||
meta: "请前往设置页配置默认下载目录",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
if (!result.data?.url) {
|
||||
downloadStatus.value = false;
|
||||
return $message.error("下载失败,请重试");
|
||||
}
|
||||
// 获取下载结果
|
||||
const isDownloaded = await downloadFile(result.data, song, lyric, {
|
||||
path: downloadPath.value,
|
||||
downloadMeta: downloadMeta.value,
|
||||
downloadCover: downloadCover.value,
|
||||
downloadLyrics: downloadLyrics.value,
|
||||
});
|
||||
}
|
||||
const isDownloaded = await downloadFile(result.data, song, downloadPath.value);
|
||||
if (isDownloaded) {
|
||||
$message.success("下载完成");
|
||||
closeDownloadModal();
|
||||
} else {
|
||||
downloadStatus.value = false;
|
||||
$message.error("下载失败,请重试");
|
||||
console.log(lyric);
|
||||
if (isDownloaded) {
|
||||
$message.success("下载完成");
|
||||
closeDownloadModal();
|
||||
} else {
|
||||
downloadStatus.value = false;
|
||||
$message.error("下载失败,请重试");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("歌曲下载出错:", error);
|
||||
$message.error("歌曲下载失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -189,12 +209,12 @@ const openDownloadModal = (data) => {
|
||||
router.currentRoute.value.name === "cloud" ||
|
||||
data?.fee === 0 ||
|
||||
data?.pc ||
|
||||
userData.detail?.profile?.vipType !== 0
|
||||
userData.value.detail?.profile?.vipType !== 0
|
||||
) {
|
||||
return toDownload();
|
||||
}
|
||||
// 权限不足
|
||||
if (data?.fee !== 0 && userData.detail?.profile?.vipType !== 11 && !data?.pc) {
|
||||
if (data?.fee !== 0 && userData.value.detail?.profile?.vipType !== 11 && !data?.pc) {
|
||||
return $message.warning("账号会员等级不足,请提升权限");
|
||||
}
|
||||
$message.warning("账号会员等级不足,请提升权限");
|
||||
|
||||
@@ -2,29 +2,31 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="loginModalShow"
|
||||
style="width: 400px"
|
||||
class="login"
|
||||
:auto-focus="false"
|
||||
:mask-closable="false"
|
||||
:bordered="false"
|
||||
:close-on-esc="false"
|
||||
:closable="false"
|
||||
style="width: 400px"
|
||||
preset="card"
|
||||
transform-origin="center"
|
||||
>
|
||||
<div class="login-content">
|
||||
<div class="title">
|
||||
<img class="logo" src="/images/logo/favicon.png?asset" alt="logo" />
|
||||
<img class="logo" src="/images/icons/favicon.png?asset" alt="logo" />
|
||||
</div>
|
||||
<!-- 登录方式 -->
|
||||
<n-tabs class="login-tabs" default-value="login-qr" type="segment" animated>
|
||||
<n-tab-pane name="login-qr" tab="扫码登录">
|
||||
<loginQRCode @setLoginData="setLoginData" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="login-phone" tab="验证码登录"> 444 </n-tab-pane>
|
||||
<n-tab-pane name="login-phone" tab="验证码登录">
|
||||
<loginPhone @setLoginData="setLoginData" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<!-- 关闭登录弹窗 -->
|
||||
<n-button
|
||||
:focusable="false"
|
||||
class="close"
|
||||
strong
|
||||
secondary
|
||||
@@ -43,12 +45,15 @@
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { siteData } from "@/stores";
|
||||
import { siteData, siteSettings } from "@/stores";
|
||||
import { getLoginState, refreshLogin } from "@/api/login";
|
||||
import { setCookies, toLogout, isLogin } from "@/utils/auth";
|
||||
import userSignIn from "@/utils/userSignIn";
|
||||
|
||||
const data = siteData();
|
||||
const settings = siteSettings();
|
||||
const { userData } = storeToRefs(data);
|
||||
const { autoSignIn } = storeToRefs(settings);
|
||||
|
||||
// 登录数据
|
||||
const loginModalShow = ref(false);
|
||||
@@ -72,6 +77,7 @@ const openLoginModal = () => {
|
||||
|
||||
// 储存登录信息
|
||||
const setLoginData = async (loginData) => {
|
||||
console.log(loginData);
|
||||
if (!loginData) return false;
|
||||
if (loginData.code === 200) {
|
||||
// 关闭登录弹窗
|
||||
@@ -80,6 +86,9 @@ const setLoginData = async (loginData) => {
|
||||
setCookies(loginData.cookie);
|
||||
// 获取用户信息
|
||||
await data.setUserProfile();
|
||||
await data.setDailySongsData();
|
||||
// 签到
|
||||
if (autoSignIn.value) await userSignIn();
|
||||
// 更改状态
|
||||
data.userLoginStatus = true;
|
||||
$message.success("登录成功");
|
||||
@@ -168,7 +177,7 @@ onBeforeMount(() => {
|
||||
.close {
|
||||
position: absolute;
|
||||
bottom: -58px;
|
||||
background-color: var(--n-color-embedded);
|
||||
background-color: var(--n-color-modal);
|
||||
&:hover {
|
||||
background-color: var(--n-color-embedded);
|
||||
}
|
||||
|
||||
149
src/components/Modal/LoginPhone.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<!-- 登录 - 手机号 -->
|
||||
<template>
|
||||
<div class="login-phone">
|
||||
<n-form
|
||||
ref="phoneFormRef"
|
||||
:model="phoneFormData"
|
||||
:rules="phoneFormRules"
|
||||
:show-label="false"
|
||||
class="phone-form"
|
||||
>
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="phoneFormData.phone" placeholder="请输入手机号">
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<SvgIcon icon="phone" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
<n-form-item path="captcha">
|
||||
<n-input-number
|
||||
v-model:value="phoneFormData.captcha"
|
||||
:show-button="false"
|
||||
style="width: 100%"
|
||||
placeholder="请输入短信验证码"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<SvgIcon icon="password" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input-number>
|
||||
<n-button
|
||||
:disabled="captchaDisabled"
|
||||
type="primary"
|
||||
style="margin-left: 12px"
|
||||
@click="getCaptcha(phoneFormData.phone)"
|
||||
>
|
||||
{{ captchaText }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button style="width: 100%" type="primary" @click="phoneLogin"> 登录 </n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { sentCaptcha, verifyCaptcha, toLogin } from "@/api/login";
|
||||
import { formRules } from "@/utils/formRules";
|
||||
|
||||
const emit = defineEmits(["setLoginData"]);
|
||||
const { numberRule, mobileRule } = formRules();
|
||||
|
||||
// 手机号数据
|
||||
const phoneFormRef = ref(null);
|
||||
const phoneFormData = ref({
|
||||
phone: null,
|
||||
captcha: null,
|
||||
});
|
||||
const phoneFormRules = {
|
||||
phone: mobileRule,
|
||||
captcha: numberRule,
|
||||
};
|
||||
const captchaTimeOut = ref(null);
|
||||
const captchaText = ref("获取验证码");
|
||||
const captchaDisabled = ref(false);
|
||||
|
||||
// 获取验证码
|
||||
const getCaptcha = (phone) => {
|
||||
clearInterval(captchaTimeOut.value);
|
||||
phoneFormRef.value?.validate(
|
||||
async (errors) => {
|
||||
if (!errors) {
|
||||
console.log(phone + "发送验证码");
|
||||
const result = await sentCaptcha(phone);
|
||||
console.log(result);
|
||||
if (result.code == 200) {
|
||||
$message.success("验证码发送成功");
|
||||
let countDown = 60;
|
||||
captchaDisabled.value = true;
|
||||
captchaTimeOut.value = setInterval(() => {
|
||||
countDown--;
|
||||
captchaText.value = countDown + "s";
|
||||
if (countDown === 0) {
|
||||
clearInterval(captchaTimeOut.value);
|
||||
captchaText.value = "重新获取";
|
||||
captchaDisabled.value = false;
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
$message.error("验证码发送失败,请重试");
|
||||
}
|
||||
} else {
|
||||
$message.error("请检查你的输入");
|
||||
}
|
||||
},
|
||||
(rule) => {
|
||||
return rule?.key === "phone";
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 手机号登录
|
||||
const phoneLogin = (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
phoneFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
const verifyRes = await verifyCaptcha(
|
||||
phoneFormData._value.phone,
|
||||
phoneFormData._value.captcha,
|
||||
);
|
||||
console.log(verifyRes);
|
||||
if (verifyRes.code == 200) {
|
||||
const result = await toLogin(phoneFormData._value.phone, phoneFormData._value.captcha);
|
||||
console.log(result);
|
||||
if (result.code === 200) {
|
||||
// 去除 HTTPOnly
|
||||
result.cookie = result.cookie.replaceAll(" HTTPOnly", "");
|
||||
// 是否含有 MUSIC_U
|
||||
if (result.cookie && result.cookie.includes("MUSIC_U")) {
|
||||
// 储存登录信息
|
||||
emit("setLoginData", result);
|
||||
} else {
|
||||
$message.error("登录出错,请重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$message.error("请检查你的输入");
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
phoneFormData.value.captcha = null;
|
||||
console.error("登录出错:", error);
|
||||
$message.error("登录出错,请重试");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-phone {
|
||||
.phone-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||