mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 13:17:46 +00:00
Compare commits
116 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71817baad0 | |||
| f6735c9c26 | |||
| 708a1a8eb8 | |||
|
|
70ab357799 | ||
|
|
c7e69158d2 | ||
|
|
022009b140 | ||
|
|
6849632abe | ||
|
|
c929beaf6c | ||
|
|
dfb09b3ccd | ||
|
|
9809a758b4 | ||
|
|
c9739b2d0e | ||
|
|
25e274a4f8 | ||
|
|
fc1c8d8512 | ||
|
|
e27879aa94 | ||
|
|
d219e48541 | ||
|
|
107f5765b0 | ||
|
|
adafffd86b | ||
|
|
9d807d1d63 | ||
|
|
481ba6bce3 | ||
|
|
df82c7cd22 | ||
|
|
bd5af9c721 | ||
|
|
7cb063d511 | ||
|
|
904e61bee6 | ||
|
|
24477694f8 | ||
|
|
87ef48b826 | ||
|
|
da8afc12cf | ||
|
|
84613dcf8a | ||
|
|
dd8aa175d1 | ||
|
|
e3caa24ca4 | ||
|
|
ae352f27e1 | ||
|
|
552a1d4b44 | ||
|
|
b0ed85689b | ||
|
|
a18e093d4a | ||
|
|
79a7c6d991 | ||
|
|
1a2c3e2843 | ||
|
|
741fdc973c | ||
|
|
1400636201 | ||
|
|
6e737b50ee | ||
|
|
c409e3b6ed | ||
|
|
e738d1e46d | ||
|
|
380c55a653 | ||
|
|
9241b3a26a | ||
|
|
3093b6f386 | ||
|
|
42366f4a32 | ||
|
|
6d6fd9a88c | ||
|
|
dc1e0aaf90 | ||
|
|
a5cb1f729d | ||
|
|
e997cd9907 | ||
|
|
c5c7ccc89e | ||
|
|
61d0b5953f | ||
|
|
a5bf5c7dfd | ||
|
|
fd40a29180 | ||
|
|
486b04b70b | ||
|
|
6ad756b215 | ||
|
|
ed1daab1f6 | ||
|
|
f2f4e2ce58 | ||
|
|
845bc8a921 | ||
|
|
f2efc4e682 | ||
|
|
f4d3d67132 | ||
|
|
e14e6d73c6 | ||
|
|
4ec550dc46 | ||
|
|
dd6d4bf1c6 | ||
|
|
a6e433bdc5 | ||
|
|
59898c7883 | ||
|
|
1b7e33c222 | ||
|
|
221ca63d3d | ||
|
|
b7f7ac8d31 | ||
|
|
65f5df8a60 | ||
|
|
8a50337854 | ||
|
|
7b97ac0139 | ||
|
|
1cb3e4b29f | ||
|
|
c89ebbdd22 | ||
|
|
ce738f6b40 | ||
|
|
2a0af8f975 | ||
|
|
2f452dbe74 | ||
|
|
622f95439d | ||
|
|
210e65dd9a | ||
|
|
241de709da | ||
|
|
c6804decfc | ||
|
|
75d3e28ef8 | ||
|
|
99371def54 | ||
|
|
70d2713643 | ||
|
|
41b72563ff | ||
|
|
345f3588bd | ||
|
|
022f740c3f | ||
|
|
ce778afff6 | ||
|
|
9fcb6da960 | ||
|
|
b589f82b6c | ||
|
|
f9e6164245 | ||
|
|
43c5bda806 | ||
|
|
6d3508c62a | ||
|
|
9e64222bdf | ||
|
|
7b911c1658 | ||
|
|
a31d552788 | ||
|
|
000cfda922 | ||
|
|
0abd616ca1 | ||
|
|
2a2ac5a37d | ||
|
|
1496a8a0d0 | ||
|
|
6b690baef6 | ||
|
|
b9cdade832 | ||
|
|
fbc1e9903e | ||
|
|
439f368fd6 | ||
|
|
bbbd729fdf | ||
|
|
c1efcb895c | ||
|
|
cb59eb94a1 | ||
|
|
f355d5da50 | ||
|
|
6e1d58964e | ||
|
|
c3aea5ee8d | ||
|
|
f064859a27 | ||
|
|
9e787bab03 | ||
|
|
97ac4117db | ||
|
|
8cd8ae4255 | ||
|
|
35edd84c22 | ||
|
|
4613feff18 | ||
|
|
fab099c6fb | ||
|
|
107bf53a39 |
65 changed files with 6524 additions and 4511 deletions
4
.envrc
Normal file
4
.envrc
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
source_url "https://raw.githubusercontent.com/cachix/devenv/82c0147677e510b247d8b9165c54f73d32dfd899/direnvrc" "sha256-7u4iDd1nZpxL4tCzmPG0dQgC5V+/44Ba+tHkPob1v2k="
|
||||||
|
|
||||||
|
export NIXPKGS_ALLOW_INSECURE=1
|
||||||
|
use devenv
|
||||||
13
.github/workflows/build.yaml
vendored
13
.github/workflows/build.yaml
vendored
|
|
@ -17,11 +17,11 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, windows-latest, ubuntu-18.04]
|
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update &&
|
sudo apt-get update &&
|
||||||
sudo apt-get install --no-install-recommends -y rpm &&
|
sudo apt-get install --no-install-recommends -y rpm &&
|
||||||
sudo apt-get install --no-install-recommends -y bsdtar &&
|
sudo apt-get install --no-install-recommends -y libarchive-tools &&
|
||||||
sudo apt-get install --no-install-recommends -y libopenjp2-tools
|
sudo apt-get install --no-install-recommends -y libopenjp2-tools
|
||||||
|
|
||||||
- name: Install Snapcraft (on Ubuntu)
|
- name: Install Snapcraft (on Ubuntu)
|
||||||
|
|
@ -81,6 +81,7 @@ jobs:
|
||||||
- name: Build/release Electron app
|
- name: Build/release Electron app
|
||||||
uses: samuelmeuli/action-electron-builder@v1.6.0
|
uses: samuelmeuli/action-electron-builder@v1.6.0
|
||||||
env:
|
env:
|
||||||
|
VUE_APP_NETEASE_API_URL: /api
|
||||||
VUE_APP_ELECTRON_API_URL: /api
|
VUE_APP_ELECTRON_API_URL: /api
|
||||||
VUE_APP_ELECTRON_API_URL_DEV: http://127.0.0.1:10754
|
VUE_APP_ELECTRON_API_URL_DEV: http://127.0.0.1:10754
|
||||||
VUE_APP_LASTFM_API_KEY: 09c55292403d961aa517ff7f5e8a3d9c
|
VUE_APP_LASTFM_API_KEY: 09c55292403d961aa517ff7f5e8a3d9c
|
||||||
|
|
@ -96,19 +97,19 @@ jobs:
|
||||||
|
|
||||||
use_vue_cli: true
|
use_vue_cli: true
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: YesPlayMusic-mac
|
name: YesPlayMusic-mac
|
||||||
path: dist_electron/*-universal.dmg
|
path: dist_electron/*-universal.dmg
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: YesPlayMusic-win
|
name: YesPlayMusic-win
|
||||||
path: dist_electron/*Setup*.exe
|
path: dist_electron/*Setup*.exe
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: YesPlayMusic-linux
|
name: YesPlayMusic-linux
|
||||||
path: dist_electron/*.AppImage
|
path: dist_electron/*.AppImage
|
||||||
|
|
|
||||||
48
.github/workflows/sync.yml
vendored
Normal file
48
.github/workflows/sync.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
name: Upstream Sync
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
actions: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 * * * *' # every hour
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync_latest_from_upstream:
|
||||||
|
name: Sync latest commits from upstream repo
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.repository.fork }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Clean issue notice
|
||||||
|
uses: actions-cool/issues-helper@v3
|
||||||
|
with:
|
||||||
|
actions: 'close-issues'
|
||||||
|
labels: '🚨 Sync Fail'
|
||||||
|
|
||||||
|
- name: Sync upstream changes
|
||||||
|
id: sync
|
||||||
|
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
||||||
|
with:
|
||||||
|
upstream_sync_repo: qier222/YesPlayMusic
|
||||||
|
upstream_sync_branch: master
|
||||||
|
target_sync_branch: master
|
||||||
|
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
||||||
|
test_mode: false
|
||||||
|
|
||||||
|
- name: Sync check
|
||||||
|
if: failure()
|
||||||
|
uses: actions-cool/issues-helper@v3
|
||||||
|
with:
|
||||||
|
actions: 'create-issue'
|
||||||
|
title: '🚨 同步失败 | Sync Fail'
|
||||||
|
labels: '🚨 Sync Fail'
|
||||||
|
body: |
|
||||||
|
Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork.
|
||||||
|
|
||||||
|
由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次。
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -32,3 +32,12 @@ NeteaseCloudMusicApi-master.zip
|
||||||
# Local Netlify folder
|
# Local Netlify folder
|
||||||
.netlify
|
.netlify
|
||||||
vercel.json
|
vercel.json
|
||||||
|
# Devenv
|
||||||
|
.devenv*
|
||||||
|
devenv.local.nix
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.direnv
|
||||||
|
|
||||||
|
# pre-commit
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
|
|
||||||
40
Dockerfile
40
Dockerfile
|
|
@ -1,43 +1,25 @@
|
||||||
FROM node:16.13.1-alpine as build
|
FROM node:16.13.1-alpine as build
|
||||||
ENV VUE_APP_NETEASE_API_URL=/api
|
ENV VUE_APP_NETEASE_API_URL=/api
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add --no-cache python3 make g++ git
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories &&\
|
||||||
|
apk add --no-cache python3 make g++ git
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN yarn build
|
RUN yarn config set electron_mirror https://npmmirror.com/mirrors/electron/ && \
|
||||||
|
yarn build
|
||||||
|
|
||||||
FROM nginx:1.20.2-alpine as app
|
FROM nginx:1.20.2-alpine as app
|
||||||
RUN echo $'server { \n\
|
|
||||||
gzip on;\n\
|
|
||||||
listen 80; \n\
|
|
||||||
listen [::]:80; \n\
|
|
||||||
server_name localhost; \n\
|
|
||||||
\n\
|
|
||||||
location / { \n\
|
|
||||||
root /usr/share/nginx/html; \n\
|
|
||||||
index index.html; \n\
|
|
||||||
try_files $uri $uri/ /index.html; \n\
|
|
||||||
} \n\
|
|
||||||
\n\
|
|
||||||
location @rewrites { \n\
|
|
||||||
rewrite ^(.*)$ /index.html last; \n\
|
|
||||||
} \n\
|
|
||||||
\n\
|
|
||||||
location /api/ { \n\
|
|
||||||
proxy_set_header Host $host; \n\
|
|
||||||
proxy_set_header X-Real-IP $remote_addr; \n\
|
|
||||||
proxy_set_header X-Forwarded-For $remote_addr; \n\
|
|
||||||
proxy_set_header X-Forwarded-Host $remote_addr; \n\
|
|
||||||
proxy_set_header X-NginX-Proxy true; \n\
|
|
||||||
proxy_pass http://localhost:3000/; \n\
|
|
||||||
} \n\
|
|
||||||
}' > /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main libuv \
|
COPY --from=build /app/package.json /usr/local/lib/
|
||||||
|
|
||||||
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories &&\
|
||||||
|
apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main libuv \
|
||||||
&& apk add --no-cache --update-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main nodejs npm \
|
&& apk add --no-cache --update-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main nodejs npm \
|
||||||
&& npm i -g NeteaseCloudMusicApi
|
&& npm i -g $(awk -F \" '{if($2=="NeteaseCloudMusicApi") print $2"@"$4}' /usr/local/lib/package.json) \
|
||||||
|
&& rm -f /usr/local/lib/package.json
|
||||||
|
|
||||||
|
COPY --from=build /app/docker/nginx.conf.example /etc/nginx/conf.d/default.conf
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
CMD nginx ; exec npx NeteaseCloudMusicApi
|
CMD nginx ; exec npx NeteaseCloudMusicApi
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020-2022 qier222
|
Copyright (c) 2020-2023 qier222
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
43
README.md
43
README.md
|
|
@ -8,7 +8,7 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
高颜值的第三方网易云播放器
|
高颜值的第三方网易云播放器
|
||||||
<br />
|
<br />
|
||||||
<a href="https://music.qier222.com" target="blank"><strong>🌎 访问DEMO</strong></a> |
|
<a href="https://music.ineko.cc" target="blank"><strong>🌎 访问DEMO</strong></a> |
|
||||||
<a href="#%EF%B8%8F-安装" target="blank"><strong>📦️ 下载安装包</strong></a> |
|
<a href="#%EF%B8%8F-安装" target="blank"><strong>📦️ 下载安装包</strong></a> |
|
||||||
<a href="https://t.me/yesplaymusic" target="blank"><strong>💬 加入交流群</strong></a>
|
<a href="https://t.me/yesplaymusic" target="blank"><strong>💬 加入交流群</strong></a>
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -16,7 +16,12 @@
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[![Library][library-screenshot]](https://music.qier222.com)
|
[![Library][library-screenshot]](https://music.ineko.cc)
|
||||||
|
|
||||||
|
|
||||||
|
## 全新版本
|
||||||
|
全新2.0 Alpha测试版已发布,欢迎前往 [Releases](https://github.com/qier222/YesPlayMusic/releases) 页面下载。
|
||||||
|
当前版本将会进入维护模式,除重大bug修复外,不会再更新新功能。
|
||||||
|
|
||||||
## ✨ 特性
|
## ✨ 特性
|
||||||
|
|
||||||
|
|
@ -37,7 +42,7 @@
|
||||||
- 🟥 支持 Last.fm Scrobble
|
- 🟥 支持 Last.fm Scrobble
|
||||||
- ☁️ 支持音乐云盘
|
- ☁️ 支持音乐云盘
|
||||||
- ⌨️ 自定义快捷键和全局快捷键
|
- ⌨️ 自定义快捷键和全局快捷键
|
||||||
- 🎧 支持Mpris
|
- 🎧 支持 Mpris
|
||||||
- 🛠 更多特性开发中
|
- 🛠 更多特性开发中
|
||||||
|
|
||||||
## 📦️ 安装
|
## 📦️ 安装
|
||||||
|
|
@ -55,6 +60,10 @@ Electron 版本由 [@hawtim](https://github.com/hawtim) 和 [@qier222](https://g
|
||||||
|
|
||||||
除了下载安装包使用,你还可以将本项目部署到 Vercel 或你的服务器上。下面是部署到 Vercel 的方法。
|
除了下载安装包使用,你还可以将本项目部署到 Vercel 或你的服务器上。下面是部署到 Vercel 的方法。
|
||||||
|
|
||||||
|
本项目的 Demo (https://music.qier222.com) 就是部署在 Vercel 上的网站。
|
||||||
|
|
||||||
|
[](https://vercel.com/?utm_source=ohmusic&utm_campaign=oss)
|
||||||
|
|
||||||
1. 部署网易云 API,详情参见 [Binaryify/NeteaseCloudMusicApi](https://neteasecloudmusicapi.vercel.app/#/?id=%e5%ae%89%e8%a3%85)
|
1. 部署网易云 API,详情参见 [Binaryify/NeteaseCloudMusicApi](https://neteasecloudmusicapi.vercel.app/#/?id=%e5%ae%89%e8%a3%85)
|
||||||
。你也可以将 API 部署到 Vercel。
|
。你也可以将 API 部署到 Vercel。
|
||||||
|
|
||||||
|
|
@ -116,6 +125,16 @@ yarn run build
|
||||||
|
|
||||||
7. 将 `/dist` 目录下的文件上传到你的 Web 服务器
|
7. 将 `/dist` 目录下的文件上传到你的 Web 服务器
|
||||||
|
|
||||||
|
## ⚙️ 宝塔面板 docker应用商店 部署
|
||||||
|
|
||||||
|
1. 安装宝塔面板,前往[宝塔面板官网](https://www.bt.cn/new/download.html) ,选择正式版的脚本下载安装。
|
||||||
|
|
||||||
|
2. 安装后登录宝塔面板,在左侧导航栏中点击 Docker,首次进入会提示安装Docker服务,点击立即安装,按提示完成安装
|
||||||
|
|
||||||
|
3. 安装完成后在应用商店中找到YesPlayMusic,点击安装,配置域名、端口等基本信息即可完成安装。
|
||||||
|
|
||||||
|
4. 安装后在浏览器输入上一步骤设置的域名即可访问。
|
||||||
|
|
||||||
## ⚙️ Docker 部署
|
## ⚙️ Docker 部署
|
||||||
|
|
||||||
1. 构建 Docker Image
|
1. 构建 Docker Image
|
||||||
|
|
@ -138,6 +157,24 @@ docker-compose up -d
|
||||||
|
|
||||||
YesPlayMusic 地址为 `http://localhost`
|
YesPlayMusic 地址为 `http://localhost`
|
||||||
|
|
||||||
|
## ⚙️ 部署至 Replit
|
||||||
|
|
||||||
|
1. 新建 Repl,选择 Bash 模板
|
||||||
|
|
||||||
|
2. 在 Replit shell 中运行以下命令
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash <(curl -s -L https://raw.githubusercontent.com/qier222/YesPlayMusic/main/install-replit.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 首次运行成功后,只需点击绿色按钮 `Run` 即可再次运行
|
||||||
|
|
||||||
|
4. 由于 replit 个人版限制内存为 1G(教育版为 3G),构建过程中可能会失败,请再次运行上述命令或运行以下命令:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /home/runner/${REPL_SLUG}/music && yarn install && yarn run build
|
||||||
|
```
|
||||||
|
|
||||||
## 👷♂️ 打包客户端
|
## 👷♂️ 打包客户端
|
||||||
|
|
||||||
如果在 Release 页面没有找到适合你的设备的安装包的话,你可以根据下面的步骤来打包自己的客户端。
|
如果在 Release 页面没有找到适合你的设备的安装包的话,你可以根据下面的步骤来打包自己的客户端。
|
||||||
|
|
|
||||||
132
devenv.lock
Normal file
132
devenv.lock
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"devenv": {
|
||||||
|
"locked": {
|
||||||
|
"dir": "src/modules",
|
||||||
|
"lastModified": 1730412360,
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"rev": "45847cb1f14a6d8cfa86ea943703c54a8798ae7e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"dir": "src/modules",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696426674,
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"pre-commit-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1730272153,
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-stable": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1730327045,
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "080166c15633801df010977d9d7474b4a6c549d7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-24.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nodejs16": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1700230496,
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a71323f68d4377d12c04a5410e214495ec598d4c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a71323f68d4377d12c04a5410e214495ec598d4c",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pre-commit-hooks": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"nixpkgs-stable": "nixpkgs-stable"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1730302582,
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "pre-commit-hooks.nix",
|
||||||
|
"rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "pre-commit-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"devenv": "devenv",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"nodejs16": "nodejs16",
|
||||||
|
"pre-commit-hooks": "pre-commit-hooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
53
devenv.nix
Normal file
53
devenv.nix
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
{ pkgs, lib, config, inputs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
nodejs16 = import inputs.nodejs16 { system = pkgs.stdenv.system; };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# https://devenv.sh/basics/
|
||||||
|
env.GREET = "devenv";
|
||||||
|
|
||||||
|
# https://devenv.sh/packages/
|
||||||
|
packages = [ pkgs.git ] ++ lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk; [
|
||||||
|
frameworks.AppKit
|
||||||
|
]);
|
||||||
|
|
||||||
|
# https://devenv.sh/languages/
|
||||||
|
languages.javascript.enable = true;
|
||||||
|
languages.javascript.package = nodejs16.pkgs.nodejs_16;
|
||||||
|
languages.javascript.corepack.enable = true;
|
||||||
|
# languages.rust.enable = true;
|
||||||
|
|
||||||
|
# https://devenv.sh/processes/
|
||||||
|
# processes.cargo-watch.exec = "cargo-watch";
|
||||||
|
|
||||||
|
# https://devenv.sh/services/
|
||||||
|
# services.postgres.enable = true;
|
||||||
|
|
||||||
|
# https://devenv.sh/scripts/
|
||||||
|
scripts.hello.exec = ''
|
||||||
|
echo hello from $GREET
|
||||||
|
'';
|
||||||
|
|
||||||
|
enterShell = ''
|
||||||
|
hello
|
||||||
|
git --version
|
||||||
|
'';
|
||||||
|
|
||||||
|
# https://devenv.sh/tasks/
|
||||||
|
# tasks = {
|
||||||
|
# "myproj:setup".exec = "mytool build";
|
||||||
|
# "devenv:enterShell".after = [ "myproj:setup" ];
|
||||||
|
# };
|
||||||
|
|
||||||
|
# https://devenv.sh/tests/
|
||||||
|
enterTest = ''
|
||||||
|
echo "Running tests"
|
||||||
|
git --version | grep --color=auto "${pkgs.git.version}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# https://devenv.sh/pre-commit-hooks/
|
||||||
|
# pre-commit.hooks.shellcheck.enable = true;
|
||||||
|
|
||||||
|
# See full reference at https://devenv.sh/reference/options/
|
||||||
|
}
|
||||||
19
devenv.yaml
Normal file
19
devenv.yaml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
|
||||||
|
inputs:
|
||||||
|
nixpkgs:
|
||||||
|
url: github:nixos/nixpkgs/nixpkgs-unstable
|
||||||
|
nodejs16:
|
||||||
|
url: github:nixos/nixpkgs/a71323f68d4377d12c04a5410e214495ec598d4c
|
||||||
|
|
||||||
|
# https://github.com/cachix/devenv/issues/792#issuecomment-2043166453
|
||||||
|
impure: true
|
||||||
|
# If you're using non-OSS software, you can set allowUnfree to true.
|
||||||
|
# allowUnfree: true
|
||||||
|
|
||||||
|
# If you're willing to use a package that's vulnerable
|
||||||
|
# permittedInsecurePackages:
|
||||||
|
# - "openssl-1.1.1w"
|
||||||
|
|
||||||
|
# If you have more than one devenv you can merge them
|
||||||
|
#imports:
|
||||||
|
# - ./backend
|
||||||
|
|
@ -4,6 +4,36 @@ services:
|
||||||
context: .
|
context: .
|
||||||
image: yesplaymusic
|
image: yesplaymusic
|
||||||
container_name: YesPlayMusic
|
container_name: YesPlayMusic
|
||||||
|
volumes:
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- ./docker/nginx.conf.example:/etc/nginx/conf.d/default.conf:ro
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
restart: always
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- UnblockNeteaseMusic
|
||||||
|
environment:
|
||||||
|
- NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||||
|
networks:
|
||||||
|
my_network:
|
||||||
|
|
||||||
|
UnblockNeteaseMusic:
|
||||||
|
image: pan93412/unblock-netease-music-enhanced
|
||||||
|
command: -o kugou kuwo migu bilibili pyncmd -p 80:443 -f 45.127.129.53 -e -
|
||||||
|
# environment:
|
||||||
|
# JSON_LOG: true
|
||||||
|
# LOG_LEVEL: debug
|
||||||
|
networks:
|
||||||
|
my_network:
|
||||||
|
aliases:
|
||||||
|
- music.163.com
|
||||||
|
- interface.music.163.com
|
||||||
|
- interface3.music.163.com
|
||||||
|
- interface.music.163.com.163jiasu.com
|
||||||
|
- interface3.music.163.com.163jiasu.com
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
networks:
|
||||||
|
my_network:
|
||||||
|
driver: bridge
|
||||||
|
|
|
||||||
28
docker/nginx.conf.example
Normal file
28
docker/nginx.conf.example
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
server {
|
||||||
|
gzip on;
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
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/;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
install-replit.sh
Normal file
28
install-replit.sh
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
# 初始化 .replit 和 replit.nix
|
||||||
|
if [[ $1 == i ]];then
|
||||||
|
echo -e 'run = ["bash", "main.sh"]\n\nentrypoint = "main.sh"' >.replit
|
||||||
|
echo -e "{ pkgs }: {\n\t\tdeps = [\n\t\t\tpkgs.nodejs-16_x\n\t\t\tpkgs.yarn\n\t\t\tpkgs.bashInteractive\n\t\t];\n}" > replit.nix
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 安装
|
||||||
|
if [[ ! -d api ]];then
|
||||||
|
mkdir api
|
||||||
|
git clone https://github.com/Binaryify/NeteaseCloudMusicApi ./api && \
|
||||||
|
cd api && npm install && cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d music ]];then
|
||||||
|
mkdir music
|
||||||
|
git clone https://github.com/qier222/YesPlayMusic ./music && \
|
||||||
|
cd music && cp .env.example .env && npm install --force && npm run build && cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
PID=`ps -ef | grep npm | awk '{print $2}' | sed '$d'`
|
||||||
|
|
||||||
|
if [[ ! -z ${PID} ]];then echo $PID | xargs kill;fi
|
||||||
|
nohup bash -c 'cd api && PORT=35216 node app.js' > api.log 2>&1
|
||||||
|
nohup bash -c 'npx serve music/dist/' > music.log 2>&1
|
||||||
13
package.json
13
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "yesplaymusic",
|
"name": "yesplaymusic",
|
||||||
"version": "0.4.5",
|
"version": "0.4.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A third party music player for Netease Music",
|
"description": "A third party music player for Netease Music",
|
||||||
"author": "qier222<qier222@outlook.com>",
|
"author": "qier222<qier222@outlook.com>",
|
||||||
|
|
@ -22,9 +22,12 @@
|
||||||
"netease_api:run": "npx NeteaseCloudMusicApi"
|
"netease_api:run": "npx NeteaseCloudMusicApi"
|
||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "background.js",
|
||||||
|
"engines": {
|
||||||
|
"node": "14 || 16"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@unblockneteasemusic/rust-napi": "^0.3.0-pre.1",
|
"@unblockneteasemusic/rust-napi": "^0.4.0",
|
||||||
"NeteaseCloudMusicApi": "^4.5.2",
|
"NeteaseCloudMusicApi": "^4.23.3",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"change-case": "^4.1.2",
|
"change-case": "^4.1.2",
|
||||||
"cli-color": "^2.0.0",
|
"cli-color": "^2.0.0",
|
||||||
|
|
@ -44,6 +47,8 @@
|
||||||
"electron-log": "^4.3.0",
|
"electron-log": "^4.3.0",
|
||||||
"electron-store": "^8.0.1",
|
"electron-store": "^8.0.1",
|
||||||
"electron-updater": "^5.0.1",
|
"electron-updater": "^5.0.1",
|
||||||
|
"esbuild": "^0.20.1",
|
||||||
|
"esbuild-loader": "^4.0.3",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-fileupload": "^1.2.0",
|
"express-fileupload": "^1.2.0",
|
||||||
"express-http-proxy": "^1.6.2",
|
"express-http-proxy": "^1.6.2",
|
||||||
|
|
@ -59,7 +64,6 @@
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pac-proxy-agent": "^4.1.0",
|
"pac-proxy-agent": "^4.1.0",
|
||||||
"plyr": "^3.6.2",
|
"plyr": "^3.6.2",
|
||||||
"prettier": "2.5.1",
|
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"register-service-worker": "^1.7.1",
|
"register-service-worker": "^1.7.1",
|
||||||
"svg-sprite-loader": "^6.0.11",
|
"svg-sprite-loader": "^6.0.11",
|
||||||
|
|
@ -87,6 +91,7 @@
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
"eslint-plugin-vue": "^7.9.0",
|
"eslint-plugin-vue": "^7.9.0",
|
||||||
"husky": "^4.3.0",
|
"husky": "^4.3.0",
|
||||||
|
"prettier": "2.5.1",
|
||||||
"sass": "^1.26.11",
|
"sass": "^1.26.11",
|
||||||
"sass-loader": "^10.0.2",
|
"sass-loader": "^10.0.2",
|
||||||
"vue-cli-plugin-electron-builder": "~2.1.1",
|
"vue-cli-plugin-electron-builder": "~2.1.1",
|
||||||
|
|
|
||||||
BIN
public/img/icons/menu-dark@88.png
Normal file
BIN
public/img/icons/menu-dark@88.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,2 +1,2 @@
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow:
|
Disallow: /
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import { mapTrackPlayableStatus } from '@/utils/common';
|
import { mapTrackPlayableStatus } from '@/utils/common';
|
||||||
|
import { isAccountLoggedIn } from '@/utils/auth';
|
||||||
|
import { getTrackDetail } from '@/api/track';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取歌手单曲
|
* 获取歌手单曲
|
||||||
|
|
@ -14,7 +16,13 @@ export function getArtist(id) {
|
||||||
id,
|
id,
|
||||||
timestamp: new Date().getTime(),
|
timestamp: new Date().getTime(),
|
||||||
},
|
},
|
||||||
}).then(data => {
|
}).then(async data => {
|
||||||
|
if (!isAccountLoggedIn()) {
|
||||||
|
const trackIDs = data.hotSongs.map(t => t.id);
|
||||||
|
const tracks = await getTrackDetail(trackIDs.join(','));
|
||||||
|
data.hotSongs = tracks.songs;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
data.hotSongs = mapTrackPlayableStatus(data.hotSongs);
|
data.hotSongs = mapTrackPlayableStatus(data.hotSongs);
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ export function dailyRecommendPlaylist(params) {
|
||||||
return request({
|
return request({
|
||||||
url: '/recommend/resource',
|
url: '/recommend/resource',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
params: {
|
||||||
params,
|
params,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,12 @@ import {
|
||||||
* 说明 : 使用歌单详情接口后 , 能得到的音乐的 id, 但不能得到的音乐 url, 调用此接口, 传入的音乐 id( 可多个 , 用逗号隔开 ), 可以获取对应的音乐的 url,
|
* 说明 : 使用歌单详情接口后 , 能得到的音乐的 id, 但不能得到的音乐 url, 调用此接口, 传入的音乐 id( 可多个 , 用逗号隔开 ), 可以获取对应的音乐的 url,
|
||||||
* !!!未登录状态返回试听片段(返回字段包含被截取的正常歌曲的开始时间和结束时间)
|
* !!!未登录状态返回试听片段(返回字段包含被截取的正常歌曲的开始时间和结束时间)
|
||||||
* @param {string} id - 音乐的 id,例如 id=405998841,33894312
|
* @param {string} id - 音乐的 id,例如 id=405998841,33894312
|
||||||
* @param {string} sqBr - flac(SQ)的比特率
|
|
||||||
* @param {string} hiResBr - hi-res的比特率
|
|
||||||
*/
|
*/
|
||||||
export function getMP3(id, sqBr, hiResBr) {
|
export function getMP3(id) {
|
||||||
const getBr = (quality, sqBr, hiResBr) => {
|
const getBr = () => {
|
||||||
if (quality === undefined) return 320000;
|
// 当返回的 quality >= 400000时,就会优先返回 hi-res
|
||||||
if (quality === 'flac' && sqBr) return sqBr;
|
const quality = store.state.settings?.musicQuality ?? '320000';
|
||||||
if (quality === '999000' && hiResBr) return hiResBr;
|
return quality === 'flac' ? '350000' : quality;
|
||||||
return quality;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return request({
|
return request({
|
||||||
|
|
@ -29,7 +26,7 @@ export function getMP3(id, sqBr, hiResBr) {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: {
|
params: {
|
||||||
id,
|
id,
|
||||||
br: getBr(store.state.settings?.musicQuality, sqBr, hiResBr),
|
br: getBr(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
src/assets/icons/fullscreen-exit.svg
Normal file
3
src/assets/icons/fullscreen-exit.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" data-darkreader-inline-fill="" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path id="path" d="M1 11L3 11L3 13C3 13.55 3.45 14 4 14C4.55 14 5 13.55 5 13L5 10C5 9.45 4.55 9 4 9L1 9C0.45 9 0 9.45 0 10C0 10.55 0.45 11 1 11ZM11 3L11 1C11 0.45 10.55 0 10 0C9.45 0 9 0.45 9 1L9 4C9 4.54 9.45 5 10 5L13 5C13.55 5 14 4.54 14 4C14 3.45 13.55 3 13 3L11 3Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 396 B |
3
src/assets/icons/fullscreen.svg
Normal file
3
src/assets/icons/fullscreen.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" data-darkreader-inline-fill="" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path id="path" d="M1 9C0.45 9 0 9.45 0 10L0 13C0 13.55 0.45 14 1 14L4 14C4.55 14 5 13.55 5 13C5 12.45 4.55 12 4 12L2 12L2 10C2 9.45 1.55 9 1 9ZM9 1C9 1.54 9.45 2 10 2L12 2L12 4C12 4.54 12.45 5 13 5C13.55 5 14 4.54 14 4L14 1C14 0.45 13.55 0 13 0L10 0C9.45 0 9 0.45 9 1Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 396 B |
|
|
@ -31,7 +31,8 @@ import { EventEmitter } from 'events';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import expressProxy from 'express-http-proxy';
|
import expressProxy from 'express-http-proxy';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import { createMpris } from '@/electron/mpris';
|
import { createMpris, createDbus } from '@/electron/mpris';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
const clc = require('cli-color');
|
const clc = require('cli-color');
|
||||||
const log = text => {
|
const log = text => {
|
||||||
console.log(`${clc.blueBright('[background.js]')} ${text}`);
|
console.log(`${clc.blueBright('[background.js]')} ${text}`);
|
||||||
|
|
@ -420,6 +421,21 @@ class Background {
|
||||||
registerGlobalShortcut(this.window, this.store);
|
registerGlobalShortcut(this.window, this.store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// try to start osdlyrics process on start
|
||||||
|
if (this.store.get('settings.enableOsdlyricsSupport')) {
|
||||||
|
await createDbus(this.window);
|
||||||
|
log('try to start osdlyrics process');
|
||||||
|
const osdlyricsProcess = spawn('osdlyrics');
|
||||||
|
|
||||||
|
osdlyricsProcess.on('error', err => {
|
||||||
|
log(`failed to start osdlyrics: ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
osdlyricsProcess.on('exit', (code, signal) => {
|
||||||
|
log(`osdlyrics process exited with code ${code}, signal ${signal}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// create mpris
|
// create mpris
|
||||||
if (isCreateMpris) {
|
if (isCreateMpris) {
|
||||||
createMpris(this.window);
|
createMpris(this.window);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
><svg-icon icon-class="play" />
|
><svg-icon icon-class="play" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<img :src="imageUrl" :style="imageStyles" />
|
<img :src="imageUrl" :style="imageStyles" loading="lazy" />
|
||||||
<transition v-if="coverHover || alwaysShowShadow" name="fade">
|
<transition v-if="coverHover || alwaysShowShadow" name="fade">
|
||||||
<div
|
<div
|
||||||
v-show="focus || alwaysShowShadow"
|
v-show="focus || alwaysShowShadow"
|
||||||
|
|
@ -135,7 +135,7 @@ img {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
height: 44%;
|
width: 50%;
|
||||||
margin: {
|
margin: {
|
||||||
left: 4px;
|
left: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
items: { type: Array, required: true },
|
items: { type: Array, required: true },
|
||||||
type: { type: String, required: true },
|
type: { type: String, required: true },
|
||||||
subText: { type: String, default: 'null' },
|
subText: { type: String, default: 'none' },
|
||||||
subTextFontSize: { type: String, default: '16px' },
|
subTextFontSize: { type: String, default: '16px' },
|
||||||
showPlayCount: { type: Boolean, default: false },
|
showPlayCount: { type: Boolean, default: false },
|
||||||
columnNumber: { type: Number, default: 5 },
|
columnNumber: { type: Number, default: 5 },
|
||||||
|
|
@ -75,9 +75,9 @@ export default {
|
||||||
return new Date(item.publishTime).getFullYear();
|
return new Date(item.publishTime).getFullYear();
|
||||||
if (this.subText === 'artist') {
|
if (this.subText === 'artist') {
|
||||||
if (item.artist !== undefined)
|
if (item.artist !== undefined)
|
||||||
return `<a href="/#/artist/${item.artist.id}">${item.artist.name}</a>`;
|
return `<a href="/artist/${item.artist.id}">${item.artist.name}</a>`;
|
||||||
if (item.artists !== undefined)
|
if (item.artists !== undefined)
|
||||||
return `<a href="/#/artist/${item.artists[0].id}">${item.artists[0].name}</a>`;
|
return `<a href="/artist/${item.artists[0].id}">${item.artists[0].name}</a>`;
|
||||||
}
|
}
|
||||||
if (this.subText === 'albumType+releaseYear') {
|
if (this.subText === 'albumType+releaseYear') {
|
||||||
let albumType = item.type;
|
let albumType = item.type;
|
||||||
|
|
@ -96,7 +96,7 @@ export default {
|
||||||
return this.type === 'playlist' && item.privacy === 10;
|
return this.type === 'playlist' && item.privacy === 10;
|
||||||
},
|
},
|
||||||
isExplicit(item) {
|
isExplicit(item) {
|
||||||
return this.type === 'album' && item.mark === 1056768;
|
return this.type === 'album' && (item.mark & 1048576) === 1048576;
|
||||||
},
|
},
|
||||||
getTitleLink(item) {
|
getTitleLink(item) {
|
||||||
return `/${this.type}/${item.id}`;
|
return `/${this.type}/${item.id}`;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="daily-recommend-card" @click="goToDailyTracks">
|
<div class="daily-recommend-card" @click="goToDailyTracks">
|
||||||
<img :src="coverUrl" />
|
<img :src="coverUrl" loading="lazy" />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="title-box">
|
<div class="title-box">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="fm" :style="{ background }" data-theme="dark">
|
<div class="fm" :style="{ background }" data-theme="dark">
|
||||||
<img :src="nextTrackCover" style="display: none" />
|
<img :src="nextTrackCover" style="display: none" loading="lazy" />
|
||||||
<img
|
<img
|
||||||
class="cover"
|
class="cover"
|
||||||
:src="track.album && track.album.picUrl | resizeImage(512)"
|
:src="track.album && track.album.picUrl | resizeImage(512)"
|
||||||
|
loading="lazy"
|
||||||
@click="goToAlbum"
|
@click="goToAlbum"
|
||||||
/>
|
/>
|
||||||
<div class="right-part">
|
<div class="right-part">
|
||||||
|
|
@ -13,19 +14,20 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button-icon title="不喜欢" @click.native="moveToFMTrash"
|
<button-icon title="不喜欢" @click.native="moveToFMTrash">
|
||||||
><svg-icon id="thumbs-down" icon-class="thumbs-down"
|
<svg-icon id="thumbs-down" icon-class="thumbs-down" />
|
||||||
/></button-icon>
|
</button-icon>
|
||||||
<button-icon
|
<button-icon
|
||||||
:title="$t(isPlaying ? 'player.pause' : 'player.play')"
|
:title="$t(isPlaying ? 'player.pause' : 'player.play')"
|
||||||
class="play"
|
class="play"
|
||||||
@click.native="play"
|
@click.native="play"
|
||||||
>
|
>
|
||||||
<svg-icon :icon-class="isPlaying ? 'pause' : 'play'"
|
<svg-icon :icon-class="isPlaying ? 'pause' : 'play'" />
|
||||||
/></button-icon>
|
</button-icon>
|
||||||
<button-icon :title="$t('player.next')" @click.native="next"
|
<button-icon :title="$t('player.next')" @click.native="next">
|
||||||
><svg-icon icon-class="next" /></button-icon
|
<svg-icon icon-class="next" />
|
||||||
></div>
|
</button-icon>
|
||||||
|
</div>
|
||||||
<div class="card-name"><svg-icon icon-class="fm" />私人FM</div>
|
<div class="card-name"><svg-icon icon-class="fm" />私人FM</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -36,7 +38,7 @@
|
||||||
import ButtonIcon from '@/components/ButtonIcon.vue';
|
import ButtonIcon from '@/components/ButtonIcon.vue';
|
||||||
import ArtistsInLine from '@/components/ArtistsInLine.vue';
|
import ArtistsInLine from '@/components/ArtistsInLine.vue';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import * as Vibrant from 'node-vibrant';
|
import * as Vibrant from 'node-vibrant/dist/vibrant.worker.min.js';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -65,6 +67,11 @@ export default {
|
||||||
)}?param=512y512`;
|
)}?param=512y512`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
track() {
|
||||||
|
this.getColor();
|
||||||
|
},
|
||||||
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getColor();
|
this.getColor();
|
||||||
window.ok = this.getColor;
|
window.ok = this.getColor;
|
||||||
|
|
@ -74,11 +81,7 @@ export default {
|
||||||
this.player.playPersonalFM();
|
this.player.playPersonalFM();
|
||||||
},
|
},
|
||||||
next() {
|
next() {
|
||||||
this.player.playNextFMTrack().then(result => {
|
this.player.playNextFMTrack();
|
||||||
if (result) {
|
|
||||||
this.getColor();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
goToAlbum() {
|
goToAlbum() {
|
||||||
if (this.track.album.id === 0) return;
|
if (this.track.album.id === 0) return;
|
||||||
|
|
@ -86,7 +89,6 @@ export default {
|
||||||
},
|
},
|
||||||
moveToFMTrash() {
|
moveToFMTrash() {
|
||||||
this.player.moveToFMTrash();
|
this.player.moveToFMTrash();
|
||||||
this.getColor();
|
|
||||||
},
|
},
|
||||||
getColor() {
|
getColor() {
|
||||||
if (!this.player.personalFMTrack?.album?.picUrl) return;
|
if (!this.player.personalFMTrack?.album?.picUrl) return;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
class="playlist"
|
class="playlist"
|
||||||
@click="addTrackToPlaylist(playlist.id)"
|
@click="addTrackToPlaylist(playlist.id)"
|
||||||
>
|
>
|
||||||
<img :src="playlist.coverImgUrl | resizeImage(224)" />
|
<img :src="playlist.coverImgUrl | resizeImage(224)" loading="lazy" />
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="title">{{ playlist.name }}</div>
|
<div class="title">{{ playlist.name }}</div>
|
||||||
<div class="track-count">{{ playlist.trackCount }} 首</div>
|
<div class="track-count">{{ playlist.trackCount }} 首</div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
@mouseleave="hoverVideoID = 0"
|
@mouseleave="hoverVideoID = 0"
|
||||||
@click="goToMv(getID(mv))"
|
@click="goToMv(getID(mv))"
|
||||||
>
|
>
|
||||||
<img :src="getUrl(mv)" />
|
<img :src="getUrl(mv)" loading="lazy" />
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-show="hoverVideoID === getID(mv)"
|
v-show="hoverVideoID === getID(mv)"
|
||||||
|
|
@ -73,7 +73,7 @@ export default {
|
||||||
artistName = mv.creator[0].userName;
|
artistName = mv.creator[0].userName;
|
||||||
artistID = mv.creator[0].userId;
|
artistID = mv.creator[0].userId;
|
||||||
}
|
}
|
||||||
return `<a href="/#/artist/${artistID}">${artistName}</a>`;
|
return `<a href="/artist/${artistID}">${artistName}</a>`;
|
||||||
} else if (this.subtitle === 'publishTime') {
|
} else if (this.subtitle === 'publishTime') {
|
||||||
return mv.publishTime;
|
return mv.publishTime;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img class="avatar" :src="avatarUrl" @click="showUserProfileMenu" />
|
<img
|
||||||
|
class="avatar"
|
||||||
|
:src="avatarUrl"
|
||||||
|
@click="showUserProfileMenu"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
@ -189,7 +194,7 @@ nav {
|
||||||
|
|
||||||
@media (max-width: 1336px) {
|
@media (max-width: 1336px) {
|
||||||
nav {
|
nav {
|
||||||
padding: 0 5vw;
|
padding: 0 max(5vw, 90px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
<div class="container" @click.stop>
|
<div class="container" @click.stop>
|
||||||
<img
|
<img
|
||||||
:src="currentTrack.al && currentTrack.al.picUrl | resizeImage(224)"
|
:src="currentTrack.al && currentTrack.al.picUrl | resizeImage(224)"
|
||||||
|
loading="lazy"
|
||||||
@click="goToAlbum"
|
@click="goToAlbum"
|
||||||
/>
|
/>
|
||||||
<div class="track-info" :title="audioSource">
|
<div class="track-info" :title="audioSource">
|
||||||
|
|
@ -49,7 +50,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="like-button">
|
<div class="like-button">
|
||||||
<button-icon
|
<button-icon
|
||||||
:title="$t('player.like')"
|
:title="
|
||||||
|
player.isCurrentTrackLiked
|
||||||
|
? $t('player.unlike')
|
||||||
|
: $t('player.like')
|
||||||
|
"
|
||||||
@click.native="likeATrack(player.currentTrack.id)"
|
@click.native="likeATrack(player.currentTrack.id)"
|
||||||
>
|
>
|
||||||
<svg-icon
|
<svg-icon
|
||||||
|
|
@ -182,6 +187,7 @@ import '@/assets/css/slider.css';
|
||||||
import ButtonIcon from '@/components/ButtonIcon.vue';
|
import ButtonIcon from '@/components/ButtonIcon.vue';
|
||||||
import VueSlider from 'vue-slider-component';
|
import VueSlider from 'vue-slider-component';
|
||||||
import { goToListSource, hasListSource } from '@/utils/playList';
|
import { goToListSource, hasListSource } from '@/utils/playList';
|
||||||
|
import { formatTrackTime } from '@/utils/common';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Player',
|
name: 'Player',
|
||||||
|
|
@ -234,10 +240,7 @@ export default {
|
||||||
: this.$router.push({ name: 'next' });
|
: this.$router.push({ name: 'next' });
|
||||||
},
|
},
|
||||||
formatTrackTime(value) {
|
formatTrackTime(value) {
|
||||||
if (!value) return '';
|
return formatTrackTime(value);
|
||||||
let min = ~~((value / 60) % 60);
|
|
||||||
let sec = (~~(value % 60)).toString().padStart(2, '0');
|
|
||||||
return `${min}:${sec}`;
|
|
||||||
},
|
},
|
||||||
hasList() {
|
hasList() {
|
||||||
return hasListSource();
|
return hasListSource();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
<div class="track-list">
|
<div class="track-list">
|
||||||
<ContextMenu ref="menu">
|
<ContextMenu ref="menu">
|
||||||
<div v-show="type !== 'cloudDisk'" class="item-info">
|
<div v-show="type !== 'cloudDisk'" class="item-info">
|
||||||
<img :src="rightClickedTrackComputed.al.picUrl | resizeImage(224)" />
|
<img
|
||||||
|
:src="rightClickedTrackComputed.al.picUrl | resizeImage(224)"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="title">{{ rightClickedTrackComputed.name }}</div>
|
<div class="title">{{ rightClickedTrackComputed.name }}</div>
|
||||||
<div class="subtitle">{{ rightClickedTrackComputed.ar[0].name }}</div>
|
<div class="subtitle">{{ rightClickedTrackComputed.ar[0].name }}</div>
|
||||||
|
|
@ -62,6 +65,7 @@
|
||||||
v-for="(track, index) in tracks"
|
v-for="(track, index) in tracks"
|
||||||
:key="itemKey === 'id' ? track.id : `${track.id}${index}`"
|
:key="itemKey === 'id' ? track.id : `${track.id}${index}`"
|
||||||
:track-prop="track"
|
:track-prop="track"
|
||||||
|
:track-no="index + 1"
|
||||||
:highlight-playing-track="highlightPlayingTrack"
|
:highlight-playing-track="highlightPlayingTrack"
|
||||||
@dblclick.native="playThisList(track.id || track.songId)"
|
@dblclick.native="playThisList(track.id || track.songId)"
|
||||||
@click.right.native="openMenu($event, track, index)"
|
@click.right.native="openMenu($event, track, index)"
|
||||||
|
|
@ -269,10 +273,15 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
copyLink() {
|
copyLink() {
|
||||||
navigator.clipboard.writeText(
|
this.$copyText(
|
||||||
`https://music.163.com/song?id=${this.rightClickedTrack.id}`
|
`https://music.163.com/song?id=${this.rightClickedTrack.id}`
|
||||||
);
|
)
|
||||||
|
.then(() => {
|
||||||
this.showToast(locale.t('toast.copied'));
|
this.showToast(locale.t('toast.copied'));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.showToast(`${locale.t('toast.copyFailed')}${err}`);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
removeTrackFromQueue() {
|
removeTrackFromQueue() {
|
||||||
this.$store.state.player.removeTrackFromQueue(
|
this.$store.state.player.removeTrackFromQueue(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<img
|
<img
|
||||||
v-if="!isAlbum"
|
v-if="!isAlbum"
|
||||||
:src="imgUrl"
|
:src="imgUrl"
|
||||||
|
loading="lazy"
|
||||||
:class="{ hover: focus }"
|
:class="{ hover: focus }"
|
||||||
@click="goToAlbum"
|
@click="goToAlbum"
|
||||||
/>
|
/>
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
style="height: 14px; width: 14px"
|
style="height: 14px; width: 14px"
|
||||||
></svg-icon>
|
></svg-icon>
|
||||||
</button>
|
</button>
|
||||||
<span v-show="(!focus || !playable) && !isPlaying">{{ track.no }}</span>
|
<span v-show="(!focus || !playable) && !isPlaying">{{ trackNo }}</span>
|
||||||
<button v-show="isPlaying">
|
<button v-show="isPlaying">
|
||||||
<svg-icon
|
<svg-icon
|
||||||
icon-class="volume"
|
icon-class="volume"
|
||||||
|
|
@ -32,22 +33,24 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{{ track.name }}
|
{{ track.name }}
|
||||||
|
<span v-if="isSubTitle" :title="subTitle" class="sub-title">
|
||||||
|
({{ subTitle }})
|
||||||
|
</span>
|
||||||
<span v-if="isAlbum" class="featured">
|
<span v-if="isAlbum" class="featured">
|
||||||
<ArtistsInLine
|
<ArtistsInLine
|
||||||
:artists="track.ar"
|
:artists="track.ar"
|
||||||
:exclude="$parent.albumObject.artist.name"
|
:exclude="$parent.albumObject.artist.name"
|
||||||
prefix="-"
|
prefix="-"
|
||||||
/></span>
|
/></span>
|
||||||
<span v-if="isAlbum && track.mark === 1318912" class="explicit-symbol"
|
<span
|
||||||
|
v-if="isAlbum && (track.mark & 1048576) === 1048576"
|
||||||
|
class="explicit-symbol"
|
||||||
><ExplicitSymbol
|
><ExplicitSymbol
|
||||||
/></span>
|
/></span>
|
||||||
<span v-if="isSubTitle" :title="subTitle" class="sub-title">
|
|
||||||
({{ subTitle }})
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isAlbum" class="artist">
|
<div v-if="!isAlbum" class="artist">
|
||||||
<span
|
<span
|
||||||
v-if="track.mark === 1318912"
|
v-if="(track.mark & 1048576) === 1048576"
|
||||||
class="explicit-symbol before-artist"
|
class="explicit-symbol before-artist"
|
||||||
><ExplicitSymbol :size="15"
|
><ExplicitSymbol :size="15"
|
||||||
/></span>
|
/></span>
|
||||||
|
|
@ -95,6 +98,7 @@ export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
trackProp: Object,
|
trackProp: Object,
|
||||||
|
trackNo: Number,
|
||||||
highlightPlayingTrack: {
|
highlightPlayingTrack: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
|
@ -316,7 +320,8 @@ button {
|
||||||
opacity: 0.72;
|
opacity: 0.72;
|
||||||
}
|
}
|
||||||
.sub-title {
|
.sub-title {
|
||||||
color: #aeaeae;
|
color: #7a7a7a;
|
||||||
|
opacity: 0.7;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ export function initIpcMain(win, store, trayEventEmitter) {
|
||||||
const sourceList =
|
const sourceList =
|
||||||
typeof sourceListString === 'string'
|
typeof sourceListString === 'string'
|
||||||
? parseSourceStringToList(unmExecutor, sourceListString)
|
? parseSourceStringToList(unmExecutor, sourceListString)
|
||||||
: ['migu', 'ytdl', 'bilibili', 'pyncm', 'kugou'];
|
: ['ytdl', 'bilibili', 'pyncm', 'kugou'];
|
||||||
log(`[UNM] using source: ${sourceList.join(', ')}`);
|
log(`[UNM] using source: ${sourceList.join(', ')}`);
|
||||||
log(`[UNM] using configuration: ${JSON.stringify(context)}`);
|
log(`[UNM] using configuration: ${JSON.stringify(context)}`);
|
||||||
|
|
||||||
|
|
@ -240,7 +240,7 @@ export function initIpcMain(win, store, trayEventEmitter) {
|
||||||
details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','),
|
details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','),
|
||||||
state: track.al.name,
|
state: track.al.name,
|
||||||
endTimestamp: Date.now() + track.dt,
|
endTimestamp: Date.now() + track.dt,
|
||||||
largeImageKey: 'logo',
|
largeImageKey: track.al.picUrl,
|
||||||
largeImageText: 'Listening ' + track.name,
|
largeImageText: 'Listening ' + track.name,
|
||||||
smallImageKey: 'play',
|
smallImageKey: 'play',
|
||||||
smallImageText: 'Playing',
|
smallImageText: 'Playing',
|
||||||
|
|
@ -252,7 +252,7 @@ export function initIpcMain(win, store, trayEventEmitter) {
|
||||||
client.updatePresence({
|
client.updatePresence({
|
||||||
details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','),
|
details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','),
|
||||||
state: track.al.name,
|
state: track.al.name,
|
||||||
largeImageKey: 'logo',
|
largeImageKey: track.al.picUrl,
|
||||||
largeImageText: 'YesPlayMusic',
|
largeImageText: 'YesPlayMusic',
|
||||||
smallImageKey: 'pause',
|
smallImageKey: 'pause',
|
||||||
smallImageText: 'Pause',
|
smallImageText: 'Pause',
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import dbus from 'dbus-next';
|
||||||
import { ipcMain, app } from 'electron';
|
import { ipcMain, app } from 'electron';
|
||||||
|
|
||||||
export function createMpris(window) {
|
export function createMpris(window) {
|
||||||
|
|
@ -28,6 +29,8 @@ export function createMpris(window) {
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('metadata', (e, metadata) => {
|
ipcMain.on('metadata', (e, metadata) => {
|
||||||
|
// 更新 Mpris 状态前将位置设为0, 否则 OSDLyrics 获取到的进度是上首音乐切换时的进度
|
||||||
|
player.getPosition = () => 0;
|
||||||
player.metadata = {
|
player.metadata = {
|
||||||
'mpris:trackid': player.objectPath('track/' + metadata.trackId),
|
'mpris:trackid': player.objectPath('track/' + metadata.trackId),
|
||||||
'mpris:artUrl': metadata.artwork[0].src,
|
'mpris:artUrl': metadata.artwork[0].src,
|
||||||
|
|
@ -35,11 +38,17 @@ export function createMpris(window) {
|
||||||
'xesam:title': metadata.title,
|
'xesam:title': metadata.title,
|
||||||
'xesam:album': metadata.album,
|
'xesam:album': metadata.album,
|
||||||
'xesam:artist': metadata.artist.split(','),
|
'xesam:artist': metadata.artist.split(','),
|
||||||
|
'xesam:url': metadata.url,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('playerCurrentTrackTime', (e, position) => {
|
ipcMain.on('playerCurrentTrackTime', (e, position) => {
|
||||||
player.getPosition = () => position * 1000 * 1000;
|
player.getPosition = () => position * 1000 * 1000;
|
||||||
|
player.seeked(position * 1000 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('seeked', (e, position) => {
|
||||||
|
player.seeked(position * 1000 * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('switchRepeatMode', (e, mode) => {
|
ipcMain.on('switchRepeatMode', (e, mode) => {
|
||||||
|
|
@ -60,3 +69,26 @@ export function createMpris(window) {
|
||||||
player.shuffle = shuffle;
|
player.shuffle = shuffle;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createDbus(window) {
|
||||||
|
const bus = dbus.sessionBus();
|
||||||
|
const Variant = dbus.Variant;
|
||||||
|
|
||||||
|
const osdService = await bus.getProxyObject(
|
||||||
|
'org.osdlyrics.Daemon',
|
||||||
|
'/org/osdlyrics/Lyrics'
|
||||||
|
);
|
||||||
|
|
||||||
|
const osdInterface = osdService.getInterface('org.osdlyrics.Lyrics');
|
||||||
|
|
||||||
|
ipcMain.on('sendLyrics', async (e, { track, lyrics }) => {
|
||||||
|
const metadata = {
|
||||||
|
title: new Variant('s', track.name),
|
||||||
|
artist: new Variant('s', track.ar.map(ar => ar.name).join(', ')),
|
||||||
|
};
|
||||||
|
|
||||||
|
await osdInterface.SetLyricContent(metadata, Buffer.from(lyrics));
|
||||||
|
|
||||||
|
window.webContents.send('saveLyricFinished');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import clc from 'cli-color';
|
import clc from 'cli-color';
|
||||||
|
import checkAuthToken from '../utils/checkAuthToken';
|
||||||
import server from 'NeteaseCloudMusicApi/server';
|
import server from 'NeteaseCloudMusicApi/server';
|
||||||
|
|
||||||
export async function startNeteaseMusicApi() {
|
export async function startNeteaseMusicApi() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* global __static */
|
/* global __static */
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { app, nativeImage, Tray, Menu } from 'electron';
|
import { app, nativeImage, Tray, Menu, nativeTheme } from 'electron';
|
||||||
import { isLinux } from '@/utils/platform';
|
import { isLinux } from '@/utils/platform';
|
||||||
|
|
||||||
function createMenuTemplate(win) {
|
function createMenuTemplate(win) {
|
||||||
|
|
@ -97,6 +97,10 @@ function createMenuTemplate(win) {
|
||||||
// click在默认行为下会弹出一个contextMenu,里面的唯一选项才会调用click事件
|
// click在默认行为下会弹出一个contextMenu,里面的唯一选项才会调用click事件
|
||||||
// setContextMenu应该是目前唯一能在linux下使用托盘菜单api
|
// setContextMenu应该是目前唯一能在linux下使用托盘菜单api
|
||||||
// 但是无法区分鼠标左右键
|
// 但是无法区分鼠标左右键
|
||||||
|
|
||||||
|
// 发现openSUSE KDE环境可以区分鼠标左右键
|
||||||
|
// 添加左键支持
|
||||||
|
// 2022.05.17
|
||||||
class YPMTrayLinuxImpl {
|
class YPMTrayLinuxImpl {
|
||||||
constructor(tray, win, emitter) {
|
constructor(tray, win, emitter) {
|
||||||
this.tray = tray;
|
this.tray = tray;
|
||||||
|
|
@ -127,6 +131,10 @@ class YPMTrayLinuxImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEvents() {
|
handleEvents() {
|
||||||
|
this.tray.on('click', () => {
|
||||||
|
this.win.show();
|
||||||
|
});
|
||||||
|
|
||||||
this.emitter.on('updateTooltip', title => this.tray.setToolTip(title));
|
this.emitter.on('updateTooltip', title => this.tray.setToolTip(title));
|
||||||
this.emitter.on('updatePlayState', isPlaying => {
|
this.emitter.on('updatePlayState', isPlaying => {
|
||||||
this.contextMenu.getMenuItemById('play').visible = !isPlaying;
|
this.contextMenu.getMenuItemById('play').visible = !isPlaying;
|
||||||
|
|
@ -189,8 +197,11 @@ class YPMTrayWindowsImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTray(win, eventEmitter) {
|
export function createTray(win, eventEmitter) {
|
||||||
|
// 感觉图标颜色应该不属于界面主题范畴,只需要跟随系统主题
|
||||||
|
let iconTheme = nativeTheme.shouldUseDarkColors ? 'light' : 'dark';
|
||||||
|
|
||||||
let icon = nativeImage
|
let icon = nativeImage
|
||||||
.createFromPath(path.join(__static, 'img/icons/menu@88.png'))
|
.createFromPath(path.join(__static, `img/icons/menu-${iconTheme}@88.png`))
|
||||||
.resize({
|
.resize({
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ export default {
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
like: 'Like',
|
like: 'Like',
|
||||||
|
unlike: 'Unlike',
|
||||||
previous: 'Previous Song',
|
previous: 'Previous Song',
|
||||||
next: 'Next Song',
|
next: 'Next Song',
|
||||||
repeat: 'Repeat',
|
repeat: 'Repeat',
|
||||||
|
|
@ -112,6 +113,8 @@ export default {
|
||||||
pause: 'Pause',
|
pause: 'Pause',
|
||||||
mute: 'Mute',
|
mute: 'Mute',
|
||||||
nextUp: 'Next Up',
|
nextUp: 'Next Up',
|
||||||
|
translationLyric: 'lyric (trans)',
|
||||||
|
PronunciationLyric: 'lyric (pronounce)',
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
|
|
@ -129,6 +132,17 @@ export default {
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
logout: 'LOGOUT',
|
logout: 'LOGOUT',
|
||||||
language: 'Languages',
|
language: 'Languages',
|
||||||
|
lyric: 'Lyric',
|
||||||
|
others: 'Others',
|
||||||
|
customization: 'Customization',
|
||||||
|
MusicGenrePreference: {
|
||||||
|
text: 'Music Language Preference',
|
||||||
|
none: 'No preferences',
|
||||||
|
mandarin: 'Mandarin',
|
||||||
|
western: 'Europe & America',
|
||||||
|
korean: 'Korean',
|
||||||
|
japanese: 'Japanese',
|
||||||
|
},
|
||||||
musicQuality: {
|
musicQuality: {
|
||||||
text: 'Music Quality',
|
text: 'Music Quality',
|
||||||
low: 'Low',
|
low: 'Low',
|
||||||
|
|
@ -166,6 +180,7 @@ export default {
|
||||||
subTitleDefault: 'Show Alias for Subtitle by default',
|
subTitleDefault: 'Show Alias for Subtitle by default',
|
||||||
enableReversedMode: 'Enable Reversed Mode (Experimental)',
|
enableReversedMode: 'Enable Reversed Mode (Experimental)',
|
||||||
enableCustomTitlebar: 'Enable custom title bar (Need restart)',
|
enableCustomTitlebar: 'Enable custom title bar (Need restart)',
|
||||||
|
showLyricsTime: 'Display current time',
|
||||||
lyricsBackground: {
|
lyricsBackground: {
|
||||||
text: 'Show Lyrics Background',
|
text: 'Show Lyrics Background',
|
||||||
off: 'Off',
|
off: 'Off',
|
||||||
|
|
@ -178,6 +193,13 @@ export default {
|
||||||
exit: 'Exit',
|
exit: 'Exit',
|
||||||
minimizeToTray: 'Minimize to tray',
|
minimizeToTray: 'Minimize to tray',
|
||||||
},
|
},
|
||||||
|
enableOsdlyricsSupport: {
|
||||||
|
title: 'desktop lyrics support',
|
||||||
|
desc1:
|
||||||
|
'Only takes effect under Linux. After enabled, it downloads the lyrics file to the local, and tries to launch OSDLyrics at startup.',
|
||||||
|
desc2:
|
||||||
|
'Please ensure that you have installed OSDLyrics before turning on this.',
|
||||||
|
},
|
||||||
unm: {
|
unm: {
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
audioSource: {
|
audioSource: {
|
||||||
|
|
@ -222,6 +244,8 @@ export default {
|
||||||
minePlaylists: 'My Playlists',
|
minePlaylists: 'My Playlists',
|
||||||
likedPlaylists: 'Liked Playlists',
|
likedPlaylists: 'Liked Playlists',
|
||||||
cardiacMode: 'Cardiac Mode',
|
cardiacMode: 'Cardiac Mode',
|
||||||
|
copyLyric: 'Copy Lyric',
|
||||||
|
copyLyricWithTranslation: 'Copy Lyric With Translation',
|
||||||
},
|
},
|
||||||
toast: {
|
toast: {
|
||||||
savedToPlaylist: 'Saved to playlist',
|
savedToPlaylist: 'Saved to playlist',
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ export default {
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
like: 'Beğen',
|
like: 'Beğen',
|
||||||
|
unlike: 'Aksine',
|
||||||
previous: 'Önceki Müzik',
|
previous: 'Önceki Müzik',
|
||||||
next: 'Sonraki Müzik',
|
next: 'Sonraki Müzik',
|
||||||
repeat: 'Tekrarla',
|
repeat: 'Tekrarla',
|
||||||
|
|
@ -107,6 +108,8 @@ export default {
|
||||||
pause: 'Durdur',
|
pause: 'Durdur',
|
||||||
mute: 'Sesi kapat',
|
mute: 'Sesi kapat',
|
||||||
nextUp: 'Sıradaki',
|
nextUp: 'Sıradaki',
|
||||||
|
translationLyric: 'şarkı sözleri (çeviri)',
|
||||||
|
PronunciationLyric: 'şarkı sözleri (çeviri)',
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
close: 'Kapat',
|
close: 'Kapat',
|
||||||
|
|
@ -124,6 +127,17 @@ export default {
|
||||||
settings: 'Ayarlar',
|
settings: 'Ayarlar',
|
||||||
logout: 'ÇIKIŞ YAP',
|
logout: 'ÇIKIŞ YAP',
|
||||||
language: 'Diller',
|
language: 'Diller',
|
||||||
|
lyric: 'Şarkı Sözleri',
|
||||||
|
others: 'Diğerleri',
|
||||||
|
customization: 'Özelleştirme',
|
||||||
|
MusicGenrePreference: {
|
||||||
|
text: 'Müzik Dili Tercihi',
|
||||||
|
none: 'Tercih yok',
|
||||||
|
mandarin: 'Çince dili',
|
||||||
|
western: 'Avrupa ve Amerika',
|
||||||
|
korean: 'Korece',
|
||||||
|
japanese: 'Japonca',
|
||||||
|
},
|
||||||
musicQuality: {
|
musicQuality: {
|
||||||
text: 'Müzik Kalitesi',
|
text: 'Müzik Kalitesi',
|
||||||
low: 'Düşük',
|
low: 'Düşük',
|
||||||
|
|
@ -216,6 +230,8 @@ export default {
|
||||||
minePlaylists: 'My Playlists',
|
minePlaylists: 'My Playlists',
|
||||||
likedPlaylists: 'Liked Playlists',
|
likedPlaylists: 'Liked Playlists',
|
||||||
cardiacMode: 'Cardiac Mode',
|
cardiacMode: 'Cardiac Mode',
|
||||||
|
copyLyric: 'Copy Lyric',
|
||||||
|
copyLyricWithTranslation: 'Copy Lyric With Translation',
|
||||||
},
|
},
|
||||||
toast: {
|
toast: {
|
||||||
savedToMyLikedSongs: 'Beğendiğim Müziklere Kaydet',
|
savedToMyLikedSongs: 'Beğendiğim Müziklere Kaydet',
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ export default {
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
like: '喜欢',
|
like: '喜欢',
|
||||||
|
unlike: '取消喜欢',
|
||||||
previous: '上一首',
|
previous: '上一首',
|
||||||
next: '下一首',
|
next: '下一首',
|
||||||
repeat: '循环播放',
|
repeat: '循环播放',
|
||||||
|
|
@ -113,6 +114,8 @@ export default {
|
||||||
pause: '暂停',
|
pause: '暂停',
|
||||||
mute: '静音',
|
mute: '静音',
|
||||||
nextUp: '播放列表',
|
nextUp: '播放列表',
|
||||||
|
translationLyric: '歌词(译)',
|
||||||
|
PronunciationLyric: '歌词(音)',
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
|
|
@ -130,6 +133,17 @@ export default {
|
||||||
settings: '设置',
|
settings: '设置',
|
||||||
logout: '登出',
|
logout: '登出',
|
||||||
language: '语言',
|
language: '语言',
|
||||||
|
lyric: '歌词',
|
||||||
|
others: '其他',
|
||||||
|
customization: '自定义',
|
||||||
|
MusicGenrePreference: {
|
||||||
|
text: '音乐语种偏好',
|
||||||
|
none: '无偏好',
|
||||||
|
mandarin: '华语',
|
||||||
|
western: '欧美',
|
||||||
|
korean: '韩语',
|
||||||
|
japanese: '日语',
|
||||||
|
},
|
||||||
musicQuality: {
|
musicQuality: {
|
||||||
text: '音质选择',
|
text: '音质选择',
|
||||||
low: '普通',
|
low: '普通',
|
||||||
|
|
@ -173,12 +187,19 @@ export default {
|
||||||
on: '打开',
|
on: '打开',
|
||||||
dynamic: '动态(GPU 占用较高)',
|
dynamic: '动态(GPU 占用较高)',
|
||||||
},
|
},
|
||||||
|
showLyricsTime: '显示当前时间',
|
||||||
closeAppOption: {
|
closeAppOption: {
|
||||||
text: '关闭主面板时...',
|
text: '关闭主面板时...',
|
||||||
ask: '询问',
|
ask: '询问',
|
||||||
exit: '退出',
|
exit: '退出',
|
||||||
minimizeToTray: '最小化到托盘',
|
minimizeToTray: '最小化到托盘',
|
||||||
},
|
},
|
||||||
|
enableOsdlyricsSupport: {
|
||||||
|
title: '桌面歌词支持',
|
||||||
|
desc1:
|
||||||
|
'仅 Linux 下生效。启用后会将歌词文件下载到本地,并在开启播放器时尝试拉起 OSDLyrics。',
|
||||||
|
desc2: '请在开启之前确保您已经正确安装了 OSDLyrics。',
|
||||||
|
},
|
||||||
unm: {
|
unm: {
|
||||||
enable: '启用',
|
enable: '启用',
|
||||||
audioSource: {
|
audioSource: {
|
||||||
|
|
@ -222,6 +243,8 @@ export default {
|
||||||
minePlaylists: '创建的歌单',
|
minePlaylists: '创建的歌单',
|
||||||
likedPlaylists: '收藏的歌单',
|
likedPlaylists: '收藏的歌单',
|
||||||
cardiacMode: '心动模式',
|
cardiacMode: '心动模式',
|
||||||
|
copyLyric: '复制歌词',
|
||||||
|
copyLyricWithTranslation: '复制歌词(含翻译)',
|
||||||
},
|
},
|
||||||
toast: {
|
toast: {
|
||||||
savedToPlaylist: '已添加到歌单',
|
savedToPlaylist: '已添加到歌单',
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ export default {
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
like: '喜歡',
|
like: '喜歡',
|
||||||
|
unlike: '取消喜歡',
|
||||||
previous: '上一首',
|
previous: '上一首',
|
||||||
next: '下一首',
|
next: '下一首',
|
||||||
repeat: '循環播放',
|
repeat: '循環播放',
|
||||||
|
|
@ -109,6 +110,8 @@ export default {
|
||||||
pause: '暫停',
|
pause: '暫停',
|
||||||
mute: '靜音',
|
mute: '靜音',
|
||||||
nextUp: '播放清單',
|
nextUp: '播放清單',
|
||||||
|
translationLyric: '歌詞(譯)',
|
||||||
|
PronunciationLyric: '歌詞(音)',
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
close: '關閉',
|
close: '關閉',
|
||||||
|
|
@ -126,6 +129,17 @@ export default {
|
||||||
settings: '設定',
|
settings: '設定',
|
||||||
logout: '登出',
|
logout: '登出',
|
||||||
language: '語言',
|
language: '語言',
|
||||||
|
lyric: '歌詞',
|
||||||
|
others: '其他',
|
||||||
|
customization: '自訂',
|
||||||
|
MusicGenrePreference: {
|
||||||
|
text: '音樂語種偏好',
|
||||||
|
none: '無偏好',
|
||||||
|
mandarin: '華語',
|
||||||
|
western: '歐美',
|
||||||
|
korean: '韓語',
|
||||||
|
japanese: '日語',
|
||||||
|
},
|
||||||
musicQuality: {
|
musicQuality: {
|
||||||
text: '音質選擇',
|
text: '音質選擇',
|
||||||
low: '普通',
|
low: '普通',
|
||||||
|
|
@ -164,6 +178,7 @@ export default {
|
||||||
subTitleDefault: '副標題使用別名',
|
subTitleDefault: '副標題使用別名',
|
||||||
enableReversedMode: '啟用倒序播放功能 (實驗性功能)',
|
enableReversedMode: '啟用倒序播放功能 (實驗性功能)',
|
||||||
enableCustomTitlebar: '啟用自訂標題列(重新啟動後生效)',
|
enableCustomTitlebar: '啟用自訂標題列(重新啟動後生效)',
|
||||||
|
showLyricsTime: '顯示目前時間',
|
||||||
lyricsBackground: {
|
lyricsBackground: {
|
||||||
text: '顯示歌詞背景',
|
text: '顯示歌詞背景',
|
||||||
off: '關閉',
|
off: '關閉',
|
||||||
|
|
@ -176,6 +191,12 @@ export default {
|
||||||
exit: '退出',
|
exit: '退出',
|
||||||
minimizeToTray: '最小化到工作列角落',
|
minimizeToTray: '最小化到工作列角落',
|
||||||
},
|
},
|
||||||
|
enableOsdlyricsSupport: {
|
||||||
|
title: '桌面歌詞支援',
|
||||||
|
desc1:
|
||||||
|
'只在 Linux 環境下生效。啟用後會將歌詞檔案下載至本機位置,並在開啟播放器時嘗試連帶啟動 OSDLyrics。',
|
||||||
|
desc2: '請在開啟之前確保您已經正確安裝了 OSDLyrics。',
|
||||||
|
},
|
||||||
unm: {
|
unm: {
|
||||||
enable: '啟用',
|
enable: '啟用',
|
||||||
audioSource: {
|
audioSource: {
|
||||||
|
|
@ -219,6 +240,8 @@ export default {
|
||||||
minePlaylists: '我建立的歌單',
|
minePlaylists: '我建立的歌單',
|
||||||
likedPlaylists: '收藏的歌單',
|
likedPlaylists: '收藏的歌單',
|
||||||
cardiacMode: '心動模式',
|
cardiacMode: '心動模式',
|
||||||
|
copyLyric: '複製歌詞',
|
||||||
|
copyLyricWithTranslation: '複製歌詞(含翻譯)',
|
||||||
},
|
},
|
||||||
toast: {
|
toast: {
|
||||||
savedToPlaylist: '已新增至歌單',
|
savedToPlaylist: '已新增至歌單',
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import i18n from '@/locale';
|
||||||
import '@/assets/icons';
|
import '@/assets/icons';
|
||||||
import '@/utils/filters';
|
import '@/utils/filters';
|
||||||
import './registerServiceWorker';
|
import './registerServiceWorker';
|
||||||
import { dailyTask } from '@/utils/common';
|
|
||||||
import '@/assets/css/global.scss';
|
import '@/assets/css/global.scss';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import '@/assets/css/nprogress.css';
|
import '@/assets/css/nprogress.css';
|
||||||
|
|
@ -38,7 +37,6 @@ Vue.use(
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false, trickleSpeed: 100 });
|
NProgress.configure({ showSpinner: false, trickleSpeed: 100 });
|
||||||
dailyTask();
|
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
i18n,
|
i18n,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ let localStorage = {
|
||||||
nyancatStyle: false,
|
nyancatStyle: false,
|
||||||
showLyricsTranslation: true,
|
showLyricsTranslation: true,
|
||||||
lyricsBackground: true,
|
lyricsBackground: true,
|
||||||
|
enableOsdlyricsSupport: false,
|
||||||
closeAppOption: 'ask',
|
closeAppOption: 'ask',
|
||||||
enableDiscordRichPresence: false,
|
enableDiscordRichPresence: false,
|
||||||
enableGlobalShortcut: true,
|
enableGlobalShortcut: true,
|
||||||
|
|
@ -35,6 +36,8 @@ let localStorage = {
|
||||||
server: '',
|
server: '',
|
||||||
port: null,
|
port: null,
|
||||||
},
|
},
|
||||||
|
enableRealIP: false,
|
||||||
|
realIP: null,
|
||||||
shortcuts: shortcuts,
|
shortcuts: shortcuts,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ export default {
|
||||||
artists: [],
|
artists: [],
|
||||||
mvs: [],
|
mvs: [],
|
||||||
cloudDisk: [],
|
cloudDisk: [],
|
||||||
|
playHistory: {
|
||||||
|
weekData: [],
|
||||||
|
allData: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
clickObjectID: 0,
|
clickObjectID: 0,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { getArtist } from '@/api/artist';
|
||||||
import { trackScrobble, trackUpdateNowPlaying } from '@/api/lastfm';
|
import { trackScrobble, trackUpdateNowPlaying } from '@/api/lastfm';
|
||||||
import { fmTrash, personalFM } from '@/api/others';
|
import { fmTrash, personalFM } from '@/api/others';
|
||||||
import { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist';
|
import { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist';
|
||||||
import { getMP3, getTrackDetail, scrobble } from '@/api/track';
|
import { getLyric, getMP3, getTrackDetail } from '@/api/track';
|
||||||
import store from '@/store';
|
import store from '@/store';
|
||||||
import { isAccountLoggedIn } from '@/utils/auth';
|
import { isAccountLoggedIn } from '@/utils/auth';
|
||||||
import { cacheTrackSource, getTrackSource } from '@/utils/db';
|
import { cacheTrackSource, getTrackSource } from '@/utils/db';
|
||||||
|
|
@ -14,6 +14,17 @@ import { decode as base642Buffer } from '@/utils/base64';
|
||||||
|
|
||||||
const PLAY_PAUSE_FADE_DURATION = 200;
|
const PLAY_PAUSE_FADE_DURATION = 200;
|
||||||
|
|
||||||
|
const INDEX_IN_PLAY_NEXT = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
const UNPLAYABLE_CONDITION = {
|
||||||
|
PLAY_NEXT_TRACK: 'playNextTrack',
|
||||||
|
PLAY_PREV_TRACK: 'playPrevTrack',
|
||||||
|
};
|
||||||
|
|
||||||
const electron =
|
const electron =
|
||||||
process.env.IS_ELECTRON === true ? window.require('electron') : null;
|
process.env.IS_ELECTRON === true ? window.require('electron') : null;
|
||||||
const ipcRenderer =
|
const ipcRenderer =
|
||||||
|
|
@ -35,14 +46,14 @@ function setTitle(track) {
|
||||||
? `${track.name} · ${track.ar[0].name} - YesPlayMusic`
|
? `${track.name} · ${track.ar[0].name} - YesPlayMusic`
|
||||||
: 'YesPlayMusic';
|
: 'YesPlayMusic';
|
||||||
if (isCreateTray) {
|
if (isCreateTray) {
|
||||||
ipcRenderer.send('updateTrayTooltip', document.title);
|
ipcRenderer?.send('updateTrayTooltip', document.title);
|
||||||
}
|
}
|
||||||
store.commit('updateTitle', document.title);
|
store.commit('updateTitle', document.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTrayLikeState(isLiked) {
|
function setTrayLikeState(isLiked) {
|
||||||
if (isCreateTray) {
|
if (isCreateTray) {
|
||||||
ipcRenderer.send('updateTrayLikeState', isLiked);
|
ipcRenderer?.send('updateTrayLikeState', isLiked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,6 +130,8 @@ export default class {
|
||||||
if (shuffle) {
|
if (shuffle) {
|
||||||
this._shuffleTheList();
|
this._shuffleTheList();
|
||||||
}
|
}
|
||||||
|
// 同步当前歌曲在列表中的下标
|
||||||
|
this.current = this.list.indexOf(this.currentTrackID);
|
||||||
}
|
}
|
||||||
get reversed() {
|
get reversed() {
|
||||||
return this._reversed;
|
return this._reversed;
|
||||||
|
|
@ -137,7 +150,7 @@ export default class {
|
||||||
}
|
}
|
||||||
set volume(volume) {
|
set volume(volume) {
|
||||||
this._volume = volume;
|
this._volume = volume;
|
||||||
Howler.volume(volume);
|
this._howler?.volume(volume);
|
||||||
}
|
}
|
||||||
get list() {
|
get list() {
|
||||||
return this.shuffle ? this._shuffledList : this._list;
|
return this.shuffle ? this._shuffledList : this._list;
|
||||||
|
|
@ -164,6 +177,9 @@ export default class {
|
||||||
get currentTrack() {
|
get currentTrack() {
|
||||||
return this._currentTrack;
|
return this._currentTrack;
|
||||||
}
|
}
|
||||||
|
get currentTrackID() {
|
||||||
|
return this._currentTrack?.id ?? 0;
|
||||||
|
}
|
||||||
get playlistSource() {
|
get playlistSource() {
|
||||||
return this._playlistSource;
|
return this._playlistSource;
|
||||||
}
|
}
|
||||||
|
|
@ -187,6 +203,9 @@ export default class {
|
||||||
set progress(value) {
|
set progress(value) {
|
||||||
if (this._howler) {
|
if (this._howler) {
|
||||||
this._howler.seek(value);
|
this._howler.seek(value);
|
||||||
|
if (isCreateMpris) {
|
||||||
|
ipcRenderer?.send('seeked', this._howler.seek());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
get isCurrentTrackLiked() {
|
get isCurrentTrackLiked() {
|
||||||
|
|
@ -195,11 +214,11 @@ export default class {
|
||||||
|
|
||||||
_init() {
|
_init() {
|
||||||
this._loadSelfFromLocalStorage();
|
this._loadSelfFromLocalStorage();
|
||||||
Howler.volume(this.volume);
|
this._howler?.volume(this.volume);
|
||||||
|
|
||||||
if (this._enabled) {
|
if (this._enabled) {
|
||||||
// 恢复当前播放歌曲
|
// 恢复当前播放歌曲
|
||||||
this._replaceCurrentTrack(this._currentTrack.id, false).then(() => {
|
this._replaceCurrentTrack(this.currentTrackID, false).then(() => {
|
||||||
this._howler?.seek(localStorage.getItem('playerCurrentTrackTime') ?? 0);
|
this._howler?.seek(localStorage.getItem('playerCurrentTrackTime') ?? 0);
|
||||||
}); // update audio source and init howler
|
}); // update audio source and init howler
|
||||||
this._initMediaSession();
|
this._initMediaSession();
|
||||||
|
|
@ -223,7 +242,7 @@ export default class {
|
||||||
_setPlaying(isPlaying) {
|
_setPlaying(isPlaying) {
|
||||||
this._playing = isPlaying;
|
this._playing = isPlaying;
|
||||||
if (isCreateTray) {
|
if (isCreateTray) {
|
||||||
ipcRenderer.send('updateTrayPlayState', this._playing);
|
ipcRenderer?.send('updateTrayPlayState', this._playing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_setIntervals() {
|
_setIntervals() {
|
||||||
|
|
@ -235,7 +254,7 @@ export default class {
|
||||||
this._progress = this._howler.seek();
|
this._progress = this._howler.seek();
|
||||||
localStorage.setItem('playerCurrentTrackTime', this._progress);
|
localStorage.setItem('playerCurrentTrackTime', this._progress);
|
||||||
if (isCreateMpris) {
|
if (isCreateMpris) {
|
||||||
ipcRenderer.send('playerCurrentTrackTime', this._progress);
|
ipcRenderer?.send('playerCurrentTrackTime', this._progress);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
@ -243,8 +262,8 @@ export default class {
|
||||||
const next = this._reversed ? this.current - 1 : this.current + 1;
|
const next = this._reversed ? this.current - 1 : this.current + 1;
|
||||||
|
|
||||||
if (this._playNextList.length > 0) {
|
if (this._playNextList.length > 0) {
|
||||||
let trackID = this._playNextList.shift();
|
let trackID = this._playNextList[0];
|
||||||
return [trackID, this.current];
|
return [trackID, INDEX_IN_PLAY_NEXT];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 循环模式开启,则重新播放当前模式下的相对的下一首
|
// 循环模式开启,则重新播放当前模式下的相对的下一首
|
||||||
|
|
@ -278,7 +297,7 @@ export default class {
|
||||||
// 返回 [trackID, index]
|
// 返回 [trackID, index]
|
||||||
return [this.list[next], next];
|
return [this.list[next], next];
|
||||||
}
|
}
|
||||||
async _shuffleTheList(firstTrackID = this._currentTrack.id) {
|
async _shuffleTheList(firstTrackID = this.currentTrackID) {
|
||||||
let list = this._list.filter(tid => tid !== firstTrackID);
|
let list = this._list.filter(tid => tid !== firstTrackID);
|
||||||
if (firstTrackID === 'first') list = this._list;
|
if (firstTrackID === 'first') list = this._list;
|
||||||
this._shuffledList = shuffle(list);
|
this._shuffledList = shuffle(list);
|
||||||
|
|
@ -290,11 +309,6 @@ export default class {
|
||||||
);
|
);
|
||||||
const trackDuration = ~~(track.dt / 1000);
|
const trackDuration = ~~(track.dt / 1000);
|
||||||
time = completed ? trackDuration : ~~time;
|
time = completed ? trackDuration : ~~time;
|
||||||
scrobble({
|
|
||||||
id: track.id,
|
|
||||||
sourceid: this.playlistSource.id,
|
|
||||||
time,
|
|
||||||
});
|
|
||||||
if (
|
if (
|
||||||
store.state.lastfm.key !== undefined &&
|
store.state.lastfm.key !== undefined &&
|
||||||
(time >= trackDuration / 2 || time >= 240)
|
(time >= trackDuration / 2 || time >= 240)
|
||||||
|
|
@ -321,6 +335,29 @@ export default class {
|
||||||
this._nextTrackCallback();
|
this._nextTrackCallback();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this._howler.on('loaderror', (_, errCode) => {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
|
||||||
|
// code 3: MEDIA_ERR_DECODE
|
||||||
|
if (errCode === 3) {
|
||||||
|
this._playNextTrack(this._isPersonalFM);
|
||||||
|
} else if (errCode === 4) {
|
||||||
|
// code 4: MEDIA_ERR_SRC_NOT_SUPPORTED
|
||||||
|
store.dispatch('showToast', `无法播放: 不支持的音频格式`);
|
||||||
|
this._playNextTrack(this._isPersonalFM);
|
||||||
|
} else {
|
||||||
|
const t = this.progress;
|
||||||
|
this._replaceCurrentTrackAudio(this.currentTrack, false, false).then(
|
||||||
|
replaced => {
|
||||||
|
// 如果 replaced 为 false,代表当前的 track 已经不是这里想要替换的track
|
||||||
|
// 此时则不修改当前的歌曲进度
|
||||||
|
if (replaced) {
|
||||||
|
this._howler?.seek(t);
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
if (autoplay) {
|
if (autoplay) {
|
||||||
this.play();
|
this.play();
|
||||||
if (this._currentTrack.name) {
|
if (this._currentTrack.name) {
|
||||||
|
|
@ -355,7 +392,7 @@ export default class {
|
||||||
}
|
}
|
||||||
_getAudioSourceFromNetease(track) {
|
_getAudioSourceFromNetease(track) {
|
||||||
if (isAccountLoggedIn()) {
|
if (isAccountLoggedIn()) {
|
||||||
return getMP3(track.id, track.sq?.br, track.hr?.br).then(result => {
|
return getMP3(track.id).then(result => {
|
||||||
if (!result.data[0]) return null;
|
if (!result.data[0]) return null;
|
||||||
if (!result.data[0].url) return null;
|
if (!result.data[0].url) return null;
|
||||||
if (result.data[0].freeTrialInfo !== null) return null; // 跳过只能试听的歌曲
|
if (result.data[0].freeTrialInfo !== null) return null; // 跳过只能试听的歌曲
|
||||||
|
|
@ -401,12 +438,11 @@ export default class {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {import("@unblockneteasemusic/rust-napi").RetrievedSongInfo | null} */
|
|
||||||
const retrieveSongInfo = await ipcRenderer.invoke(
|
const retrieveSongInfo = await ipcRenderer.invoke(
|
||||||
'unblock-music',
|
'unblock-music',
|
||||||
store.state.settings.unmSource,
|
store.state.settings.unmSource,
|
||||||
track,
|
track,
|
||||||
/** @type {import("@unblockneteasemusic/rust-napi").Context} */ ({
|
{
|
||||||
enableFlac: store.state.settings.unmEnableFlac || null,
|
enableFlac: store.state.settings.unmEnableFlac || null,
|
||||||
proxyUri: store.state.settings.unmProxyUri || null,
|
proxyUri: store.state.settings.unmProxyUri || null,
|
||||||
searchMode: determineSearchMode(store.state.settings.unmSearchMode),
|
searchMode: determineSearchMode(store.state.settings.unmSearchMode),
|
||||||
|
|
@ -415,7 +451,7 @@ export default class {
|
||||||
'qq:cookie': store.state.settings.unmQQCookie || null,
|
'qq:cookie': store.state.settings.unmQQCookie || null,
|
||||||
'ytdl:exe': store.state.settings.unmYtDlExe || null,
|
'ytdl:exe': store.state.settings.unmYtDlExe || null,
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (store.state.settings.automaticallyCacheSongs && retrieveSongInfo?.url) {
|
if (store.state.settings.automaticallyCacheSongs && retrieveSongInfo?.url) {
|
||||||
|
|
@ -452,34 +488,62 @@ export default class {
|
||||||
_replaceCurrentTrack(
|
_replaceCurrentTrack(
|
||||||
id,
|
id,
|
||||||
autoplay = true,
|
autoplay = true,
|
||||||
ifUnplayableThen = 'playNextTrack'
|
ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK
|
||||||
) {
|
) {
|
||||||
if (autoplay && this._currentTrack.name) {
|
if (autoplay && this._currentTrack.name) {
|
||||||
this._scrobble(this.currentTrack, this._howler?.seek());
|
this._scrobble(this.currentTrack, this._howler?.seek());
|
||||||
}
|
}
|
||||||
return getTrackDetail(id).then(data => {
|
return getTrackDetail(id).then(data => {
|
||||||
let track = data.songs[0];
|
const track = data.songs[0];
|
||||||
this._currentTrack = track;
|
this._currentTrack = track;
|
||||||
this._updateMediaSessionMetaData(track);
|
this._updateMediaSessionMetaData(track);
|
||||||
|
return this._replaceCurrentTrackAudio(
|
||||||
|
track,
|
||||||
|
autoplay,
|
||||||
|
true,
|
||||||
|
ifUnplayableThen
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @returns 是否成功加载音频,并使用加载完成的音频替换了howler实例
|
||||||
|
*/
|
||||||
|
_replaceCurrentTrackAudio(
|
||||||
|
track,
|
||||||
|
autoplay,
|
||||||
|
isCacheNextTrack,
|
||||||
|
ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK
|
||||||
|
) {
|
||||||
return this._getAudioSource(track).then(source => {
|
return this._getAudioSource(track).then(source => {
|
||||||
if (source) {
|
if (source) {
|
||||||
|
let replaced = false;
|
||||||
|
if (track.id === this.currentTrackID) {
|
||||||
this._playAudioSource(source, autoplay);
|
this._playAudioSource(source, autoplay);
|
||||||
|
replaced = true;
|
||||||
|
}
|
||||||
|
if (isCacheNextTrack) {
|
||||||
this._cacheNextTrack();
|
this._cacheNextTrack();
|
||||||
return source;
|
}
|
||||||
|
return replaced;
|
||||||
} else {
|
} else {
|
||||||
store.dispatch('showToast', `无法播放 ${track.name}`);
|
store.dispatch('showToast', `无法播放 ${track.name}`);
|
||||||
if (ifUnplayableThen === 'playNextTrack') {
|
switch (ifUnplayableThen) {
|
||||||
if (this.isPersonalFM) {
|
case UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK:
|
||||||
this.playNextFMTrack();
|
this._playNextTrack(this.isPersonalFM);
|
||||||
} else {
|
break;
|
||||||
this.playNextTrack();
|
case UNPLAYABLE_CONDITION.PLAY_PREV_TRACK:
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.playPrevTrack();
|
this.playPrevTrack();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
store.dispatch(
|
||||||
|
'showToast',
|
||||||
|
`undefined Unplayable condition: ${ifUnplayableThen}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
_cacheNextTrack() {
|
_cacheNextTrack() {
|
||||||
let nextTrackID = this._isPersonalFM
|
let nextTrackID = this._isPersonalFM
|
||||||
|
|
@ -511,11 +575,7 @@ export default class {
|
||||||
this.playPrevTrack();
|
this.playPrevTrack();
|
||||||
});
|
});
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||||
if (this.isPersonalFM) {
|
this._playNextTrack(this.isPersonalFM);
|
||||||
this.playNextFMTrack();
|
|
||||||
} else {
|
|
||||||
this.playNextTrack();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
navigator.mediaSession.setActionHandler('stop', () => {
|
navigator.mediaSession.setActionHandler('stop', () => {
|
||||||
this.pause();
|
this.pause();
|
||||||
|
|
@ -557,13 +617,35 @@ export default class {
|
||||||
],
|
],
|
||||||
length: this.currentTrackDuration,
|
length: this.currentTrackDuration,
|
||||||
trackId: this.current,
|
trackId: this.current,
|
||||||
|
url: '/trackid/' + track.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
navigator.mediaSession.metadata = new window.MediaMetadata(metadata);
|
navigator.mediaSession.metadata = new window.MediaMetadata(metadata);
|
||||||
if (isCreateMpris) {
|
if (isCreateMpris) {
|
||||||
ipcRenderer.send('metadata', metadata);
|
this._updateMprisState(track, metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// OSDLyrics 会检测 Mpris 状态并寻找对应歌词文件,所以要在更新 Mpris 状态之前保证歌词下载完成
|
||||||
|
async _updateMprisState(track, metadata) {
|
||||||
|
if (!store.state.settings.enableOsdlyricsSupport) {
|
||||||
|
return ipcRenderer?.send('metadata', metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lyricContent = await getLyric(track.id);
|
||||||
|
|
||||||
|
if (!lyricContent.lrc || !lyricContent.lrc.lyric) {
|
||||||
|
return ipcRenderer?.send('metadata', metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.send('sendLyrics', {
|
||||||
|
track,
|
||||||
|
lyrics: lyricContent.lrc.lyric,
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('saveLyricFinished', () => {
|
||||||
|
ipcRenderer?.send('metadata', metadata);
|
||||||
|
});
|
||||||
|
}
|
||||||
_updateMediaSessionPositionState() {
|
_updateMediaSessionPositionState() {
|
||||||
if ('mediaSession' in navigator === false) {
|
if ('mediaSession' in navigator === false) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -579,11 +661,9 @@ export default class {
|
||||||
_nextTrackCallback() {
|
_nextTrackCallback() {
|
||||||
this._scrobble(this._currentTrack, 0, true);
|
this._scrobble(this._currentTrack, 0, true);
|
||||||
if (!this.isPersonalFM && this.repeatMode === 'one') {
|
if (!this.isPersonalFM && this.repeatMode === 'one') {
|
||||||
this._replaceCurrentTrack(this._currentTrack.id);
|
this._replaceCurrentTrack(this.currentTrackID);
|
||||||
} else if (this.isPersonalFM) {
|
|
||||||
this.playNextFMTrack();
|
|
||||||
} else {
|
} else {
|
||||||
this.playNextTrack();
|
this._playNextTrack(this.isPersonalFM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_loadPersonalFMNextTrack() {
|
_loadPersonalFMNextTrack() {
|
||||||
|
|
@ -617,7 +697,7 @@ export default class {
|
||||||
}
|
}
|
||||||
let copyTrack = { ...track };
|
let copyTrack = { ...track };
|
||||||
copyTrack.dt -= seekTime * 1000;
|
copyTrack.dt -= seekTime * 1000;
|
||||||
ipcRenderer.send('playDiscordPresence', copyTrack);
|
ipcRenderer?.send('playDiscordPresence', copyTrack);
|
||||||
}
|
}
|
||||||
_pauseDiscordPresence(track) {
|
_pauseDiscordPresence(track) {
|
||||||
if (
|
if (
|
||||||
|
|
@ -626,13 +706,16 @@ export default class {
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
ipcRenderer.send('pauseDiscordPresence', track);
|
ipcRenderer?.send('pauseDiscordPresence', track);
|
||||||
|
}
|
||||||
|
_playNextTrack(isPersonal) {
|
||||||
|
if (isPersonal) {
|
||||||
|
this.playNextFMTrack();
|
||||||
|
} else {
|
||||||
|
this.playNextTrack();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTrackID() {
|
|
||||||
const { list, current } = this._getListAndCurrent();
|
|
||||||
return list[current];
|
|
||||||
}
|
|
||||||
appendTrack(trackID) {
|
appendTrack(trackID) {
|
||||||
this.list.append(trackID);
|
this.list.append(trackID);
|
||||||
}
|
}
|
||||||
|
|
@ -644,7 +727,12 @@ export default class {
|
||||||
this._setPlaying(false);
|
this._setPlaying(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.current = index;
|
let next = index;
|
||||||
|
if (index === INDEX_IN_PLAY_NEXT) {
|
||||||
|
this._playNextList.shift();
|
||||||
|
next = this.current;
|
||||||
|
}
|
||||||
|
this.current = next;
|
||||||
this._replaceCurrentTrack(trackID);
|
this._replaceCurrentTrack(trackID);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -697,7 +785,11 @@ export default class {
|
||||||
const [trackID, index] = this._getPrevTrack();
|
const [trackID, index] = this._getPrevTrack();
|
||||||
if (trackID === undefined) return false;
|
if (trackID === undefined) return false;
|
||||||
this.current = index;
|
this.current = index;
|
||||||
this._replaceCurrentTrack(trackID, true, 'playPrevTrack');
|
this._replaceCurrentTrack(
|
||||||
|
trackID,
|
||||||
|
true,
|
||||||
|
UNPLAYABLE_CONDITION.PLAY_PREV_TRACK
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
saveSelfToLocalStorage() {
|
saveSelfToLocalStorage() {
|
||||||
|
|
@ -728,6 +820,9 @@ export default class {
|
||||||
this._howler?.once('play', () => {
|
this._howler?.once('play', () => {
|
||||||
this._howler?.fade(0, this.volume, PLAY_PAUSE_FADE_DURATION);
|
this._howler?.fade(0, this.volume, PLAY_PAUSE_FADE_DURATION);
|
||||||
|
|
||||||
|
// 播放时确保开启player.
|
||||||
|
// 避免因"忘记设置"导致在播放时播放器不显示的Bug
|
||||||
|
this._enabled = true;
|
||||||
this._setPlaying(true);
|
this._setPlaying(true);
|
||||||
if (this._currentTrack.name) {
|
if (this._currentTrack.name) {
|
||||||
setTitle(this._currentTrack);
|
setTitle(this._currentTrack);
|
||||||
|
|
@ -751,11 +846,14 @@ export default class {
|
||||||
this.play();
|
this.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
seek(time = null) {
|
seek(time = null, sendMpris = true) {
|
||||||
|
if (isCreateMpris && sendMpris && time) {
|
||||||
|
ipcRenderer?.send('seeked', time);
|
||||||
|
}
|
||||||
if (time !== null) {
|
if (time !== null) {
|
||||||
this._howler?.seek(time);
|
this._howler?.seek(time);
|
||||||
if (this._playing)
|
if (this._playing)
|
||||||
this._playDiscordPresence(this._currentTrack, this.seek());
|
this._playDiscordPresence(this._currentTrack, this.seek(null, false));
|
||||||
}
|
}
|
||||||
return this._howler === null ? 0 : this._howler.seek();
|
return this._howler === null ? 0 : this._howler.seek();
|
||||||
}
|
}
|
||||||
|
|
@ -781,7 +879,6 @@ export default class {
|
||||||
autoPlayTrackID = 'first'
|
autoPlayTrackID = 'first'
|
||||||
) {
|
) {
|
||||||
this._isPersonalFM = false;
|
this._isPersonalFM = false;
|
||||||
if (!this._enabled) this._enabled = true;
|
|
||||||
this.list = trackIDs;
|
this.list = trackIDs;
|
||||||
this.current = 0;
|
this.current = 0;
|
||||||
this._playlistSource = {
|
this._playlistSource = {
|
||||||
|
|
@ -792,7 +889,7 @@ export default class {
|
||||||
if (autoPlayTrackID === 'first') {
|
if (autoPlayTrackID === 'first') {
|
||||||
this._replaceCurrentTrack(this.list[0]);
|
this._replaceCurrentTrack(this.list[0]);
|
||||||
} else {
|
} else {
|
||||||
this.current = trackIDs.indexOf(autoPlayTrackID);
|
this.current = this.list.indexOf(autoPlayTrackID);
|
||||||
this._replaceCurrentTrack(autoPlayTrackID);
|
this._replaceCurrentTrack(autoPlayTrackID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -838,20 +935,13 @@ export default class {
|
||||||
addTrackToPlayNext(trackID, playNow = false) {
|
addTrackToPlayNext(trackID, playNow = false) {
|
||||||
this._playNextList.push(trackID);
|
this._playNextList.push(trackID);
|
||||||
if (playNow) {
|
if (playNow) {
|
||||||
if (this.isPersonalFM) {
|
|
||||||
this.playNextFMTrack();
|
|
||||||
} else {
|
|
||||||
this.playNextTrack();
|
this.playNextTrack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
playPersonalFM() {
|
playPersonalFM() {
|
||||||
this._isPersonalFM = true;
|
this._isPersonalFM = true;
|
||||||
if (!this._enabled) this._enabled = true;
|
if (this.currentTrackID !== this._personalFMTrack.id) {
|
||||||
if (this._currentTrack.id !== this._personalFMTrack.id) {
|
this._replaceCurrentTrack(this._personalFMTrack.id, true);
|
||||||
this._replaceCurrentTrack(this._personalFMTrack.id).then(() =>
|
|
||||||
this.playOrPause()
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
this.playOrPause();
|
this.playOrPause();
|
||||||
}
|
}
|
||||||
|
|
@ -867,7 +957,7 @@ export default class {
|
||||||
sendSelfToIpcMain() {
|
sendSelfToIpcMain() {
|
||||||
if (process.env.IS_ELECTRON !== true) return false;
|
if (process.env.IS_ELECTRON !== true) return false;
|
||||||
let liked = store.state.liked.songs.includes(this.currentTrack.id);
|
let liked = store.state.liked.songs.includes(this.currentTrack.id);
|
||||||
ipcRenderer.send('player', {
|
ipcRenderer?.send('player', {
|
||||||
playing: this.playing,
|
playing: this.playing,
|
||||||
likedCurrentTrack: liked,
|
likedCurrentTrack: liked,
|
||||||
});
|
});
|
||||||
|
|
@ -883,13 +973,13 @@ export default class {
|
||||||
this.repeatMode = 'on';
|
this.repeatMode = 'on';
|
||||||
}
|
}
|
||||||
if (isCreateMpris) {
|
if (isCreateMpris) {
|
||||||
ipcRenderer.send('switchRepeatMode', this.repeatMode);
|
ipcRenderer?.send('switchRepeatMode', this.repeatMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switchShuffle() {
|
switchShuffle() {
|
||||||
this.shuffle = !this.shuffle;
|
this.shuffle = !this.shuffle;
|
||||||
if (isCreateMpris) {
|
if (isCreateMpris) {
|
||||||
ipcRenderer.send('switchShuffle', this.shuffle);
|
ipcRenderer?.send('switchShuffle', this.shuffle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switchReversed() {
|
switchReversed() {
|
||||||
|
|
|
||||||
8
src/utils/checkAuthToken.js
Normal file
8
src/utils/checkAuthToken.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import os from 'os';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// extract from NeteasyCloudMusicAPI/generateConfig.js and avoid bugs in there (generateConfig require main.js but the main.js has bugs)
|
||||||
|
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
|
||||||
|
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
|
||||||
|
}
|
||||||
|
|
@ -221,7 +221,7 @@ export function bytesToSize(bytes) {
|
||||||
|
|
||||||
export function formatTrackTime(value) {
|
export function formatTrackTime(value) {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
let min = ~~((value / 60) % 60);
|
let min = ~~(value / 60);
|
||||||
let sec = (~~(value % 60)).toString().padStart(2, '0');
|
let sec = (~~(value % 60)).toString().padStart(2, '0');
|
||||||
return `${min}:${sec}`;
|
return `${min}:${sec}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ export function lyricParser(lrc) {
|
||||||
return {
|
return {
|
||||||
lyric: parseLyric(lrc?.lrc?.lyric || ''),
|
lyric: parseLyric(lrc?.lrc?.lyric || ''),
|
||||||
tlyric: parseLyric(lrc?.tlyric?.lyric || ''),
|
tlyric: parseLyric(lrc?.tlyric?.lyric || ''),
|
||||||
|
romalyric: parseLyric(lrc?.romalrc?.lyric || ''),
|
||||||
lyricuser: lrc.lyricUser,
|
lyricuser: lrc.lyricUser,
|
||||||
transuser: lrc.transUser,
|
transuser: lrc.transUser,
|
||||||
};
|
};
|
||||||
|
|
@ -83,3 +84,30 @@ function trimContent(content) {
|
||||||
let t = content.trim();
|
let t = content.trim();
|
||||||
return t.length < 1 ? content : t;
|
return t.length < 1 ? content : t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} lyric
|
||||||
|
*/
|
||||||
|
export async function copyLyric(lyric) {
|
||||||
|
const textToCopy = lyric;
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(textToCopy);
|
||||||
|
} catch (err) {
|
||||||
|
alert('复制失败,请手动复制!');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const tempInput = document.createElement('textarea');
|
||||||
|
tempInput.value = textToCopy;
|
||||||
|
tempInput.style.position = 'absolute';
|
||||||
|
tempInput.style.left = '-9999px';
|
||||||
|
document.body.appendChild(tempInput);
|
||||||
|
tempInput.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
} catch (err) {
|
||||||
|
alert('复制失败,请手动复制!');
|
||||||
|
}
|
||||||
|
document.body.removeChild(tempInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import router from '../router';
|
import router from '../router';
|
||||||
import state from '../store/state';
|
import state from '../store/state';
|
||||||
|
import {
|
||||||
|
recommendPlaylist,
|
||||||
|
dailyRecommendPlaylist,
|
||||||
|
getPlaylistDetail,
|
||||||
|
} from '@/api/playlist';
|
||||||
|
import { isAccountLoggedIn } from '@/utils/auth';
|
||||||
|
|
||||||
export function hasListSource() {
|
export function hasListSource() {
|
||||||
return !state.player.isPersonalFM && state.player.playlistSource.id !== 0;
|
return !state.player.isPersonalFM && state.player.playlistSource.id !== 0;
|
||||||
|
|
@ -20,3 +26,36 @@ export function getListSourcePath() {
|
||||||
return `/${state.player.playlistSource.type}/${state.player.playlistSource.id}`;
|
return `/${state.player.playlistSource.type}/${state.player.playlistSource.id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getRecommendPlayList(limit, removePrivateRecommand) {
|
||||||
|
if (isAccountLoggedIn()) {
|
||||||
|
const playlists = await Promise.all([
|
||||||
|
dailyRecommendPlaylist(),
|
||||||
|
recommendPlaylist({ limit }),
|
||||||
|
]);
|
||||||
|
let recommend = playlists[0].recommend ?? [];
|
||||||
|
if (recommend.length) {
|
||||||
|
if (removePrivateRecommand) recommend = recommend.slice(1);
|
||||||
|
await replaceRecommendResult(recommend);
|
||||||
|
}
|
||||||
|
return recommend.concat(playlists[1].result).slice(0, limit);
|
||||||
|
} else {
|
||||||
|
const response = await recommendPlaylist({ limit });
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replaceRecommendResult(recommend) {
|
||||||
|
for (let r of recommend) {
|
||||||
|
if (specialPlaylist.indexOf(r.id) > -1) {
|
||||||
|
const data = await getPlaylistDetail(r.id, true);
|
||||||
|
const playlist = data.playlist;
|
||||||
|
if (playlist) {
|
||||||
|
r.name = playlist.name;
|
||||||
|
r.picUrl = playlist.coverImgUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const specialPlaylist = [3136952023, 2829883282, 2829816518, 2829896389];
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,11 @@ const service = axios.create({
|
||||||
service.interceptors.request.use(function (config) {
|
service.interceptors.request.use(function (config) {
|
||||||
if (!config.params) config.params = {};
|
if (!config.params) config.params = {};
|
||||||
if (baseURL.length) {
|
if (baseURL.length) {
|
||||||
if (baseURL[0] !== '/' && !process.env.IS_ELECTRON) {
|
if (
|
||||||
|
baseURL[0] !== '/' &&
|
||||||
|
!process.env.IS_ELECTRON &&
|
||||||
|
getCookie('MUSIC_U') !== null
|
||||||
|
) {
|
||||||
config.params.cookie = `MUSIC_U=${getCookie('MUSIC_U')};`;
|
config.params.cookie = `MUSIC_U=${getCookie('MUSIC_U')};`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -34,8 +38,15 @@ service.interceptors.request.use(function (config) {
|
||||||
config.params.realIP = '211.161.244.70';
|
config.params.realIP = '211.161.244.70';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force real_ip
|
||||||
|
const enableRealIP = JSON.parse(
|
||||||
|
localStorage.getItem('settings')
|
||||||
|
).enableRealIP;
|
||||||
|
const realIP = JSON.parse(localStorage.getItem('settings')).realIP;
|
||||||
if (process.env.VUE_APP_REAL_IP) {
|
if (process.env.VUE_APP_REAL_IP) {
|
||||||
config.params.realIP = process.env.VUE_APP_REAL_IP;
|
config.params.realIP = process.env.VUE_APP_REAL_IP;
|
||||||
|
} else if (enableRealIP) {
|
||||||
|
config.params.realIP = realIP;
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxy = JSON.parse(localStorage.getItem('settings')).proxyConfig;
|
const proxy = JSON.parse(localStorage.getItem('settings')).proxyConfig;
|
||||||
|
|
@ -53,8 +64,16 @@ service.interceptors.response.use(
|
||||||
},
|
},
|
||||||
async error => {
|
async error => {
|
||||||
/** @type {import('axios').AxiosResponse | null} */
|
/** @type {import('axios').AxiosResponse | null} */
|
||||||
const response = error.response;
|
let response;
|
||||||
const data = response.data;
|
let data;
|
||||||
|
if (error === 'TypeError: baseURL is undefined') {
|
||||||
|
response = error;
|
||||||
|
data = error;
|
||||||
|
console.error("You must set up the baseURL in the service's config");
|
||||||
|
} else if (error.response) {
|
||||||
|
response = error.response;
|
||||||
|
data = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
response &&
|
response &&
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@
|
||||||
<span v-else>Compilation by Various Artists</span>
|
<span v-else>Compilation by Various Artists</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="date-and-count">
|
<div class="date-and-count">
|
||||||
<span v-if="album.mark === 1056768" class="explicit-symbol"
|
<span
|
||||||
|
v-if="(album.mark & 1048576) === 1048576"
|
||||||
|
class="explicit-symbol"
|
||||||
><ExplicitSymbol
|
><ExplicitSymbol
|
||||||
/></span>
|
/></span>
|
||||||
<span :title="album.publishTime | formatDate">{{
|
<span :title="album.publishTime | formatDate">{{
|
||||||
|
|
@ -71,12 +73,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="Object.keys(tracksByDisc).length !== 1">
|
<div v-if="tracksByDisc.length > 1">
|
||||||
<div v-for="(disc, cd) in tracksByDisc" :key="cd">
|
<div v-for="item in tracksByDisc" :key="item.disc">
|
||||||
<h2 class="disc">Disc {{ cd }}</h2>
|
<h2 class="disc">Disc {{ item.disc }}</h2>
|
||||||
<TrackList
|
<TrackList
|
||||||
:id="album.id"
|
:id="album.id"
|
||||||
:tracks="disc"
|
:tracks="item.tracks"
|
||||||
:type="'album'"
|
:type="'album'"
|
||||||
:album-object="album"
|
:album-object="album"
|
||||||
/>
|
/>
|
||||||
|
|
@ -96,9 +98,7 @@
|
||||||
{{ $t('album.released') }}
|
{{ $t('album.released') }}
|
||||||
{{ album.publishTime | formatDate('MMMM D, YYYY') }}
|
{{ album.publishTime | formatDate('MMMM D, YYYY') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="album.company !== null" class="copyright">
|
<div v-if="album.company" class="copyright"> © {{ album.company }} </div>
|
||||||
© {{ album.company }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="filteredMoreAlbums.length !== 0" class="more-by">
|
<div v-if="filteredMoreAlbums.length !== 0" class="more-by">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
|
|
@ -153,7 +153,7 @@ import locale from '@/locale';
|
||||||
import { splitSoundtrackAlbumTitle, splitAlbumTitle } from '@/utils/common';
|
import { splitSoundtrackAlbumTitle, splitAlbumTitle } from '@/utils/common';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import { isAccountLoggedIn } from '@/utils/auth';
|
import { isAccountLoggedIn } from '@/utils/auth';
|
||||||
import { groupBy } from 'lodash';
|
import { groupBy, toPairs, sortBy } from 'lodash';
|
||||||
|
|
||||||
import ExplicitSymbol from '@/components/ExplicitSymbol.vue';
|
import ExplicitSymbol from '@/components/ExplicitSymbol.vue';
|
||||||
import ButtonTwoTone from '@/components/ButtonTwoTone.vue';
|
import ButtonTwoTone from '@/components/ButtonTwoTone.vue';
|
||||||
|
|
@ -222,7 +222,12 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tracksByDisc() {
|
tracksByDisc() {
|
||||||
return groupBy(this.tracks, 'cd');
|
if (this.tracks.length <= 1) return [];
|
||||||
|
const pairs = toPairs(groupBy(this.tracks, 'cd'));
|
||||||
|
return sortBy(pairs, p => p[0]).map(items => ({
|
||||||
|
disc: items[0],
|
||||||
|
tracks: items[1],
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div v-show="show" class="artist-page">
|
<div v-show="show" class="artist-page">
|
||||||
<div class="artist-info">
|
<div class="artist-info">
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<img :src="artist.img1v1Url | resizeImage(1024)" />
|
<img :src="artist.img1v1Url | resizeImage(1024)" loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="name">{{ artist.name }}</div>
|
<div class="name">{{ artist.name }}</div>
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
@mouseleave="mvHover = false"
|
@mouseleave="mvHover = false"
|
||||||
@click="goToMv(latestMV.id)"
|
@click="goToMv(latestMV.id)"
|
||||||
>
|
>
|
||||||
<img :src="latestMV.coverUrl" />
|
<img :src="latestMV.coverUrl" loading="lazy" />
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-show="mvHover"
|
v-show="mvHover"
|
||||||
|
|
@ -127,7 +127,7 @@
|
||||||
<div v-if="mvs.length !== 0" id="mvs" class="mvs">
|
<div v-if="mvs.length !== 0" id="mvs" class="mvs">
|
||||||
<div class="section-title"
|
<div class="section-title"
|
||||||
>MVs
|
>MVs
|
||||||
<router-link v-show="hasMoreMV" :to="`/artist/${this.artist.id}/mv`">{{
|
<router-link v-show="hasMoreMV" :to="`/artist/${artist.id}/mv`">{{
|
||||||
$t('home.seeMore')
|
$t('home.seeMore')
|
||||||
}}</router-link>
|
}}</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -185,6 +185,7 @@ import {
|
||||||
followAArtist,
|
followAArtist,
|
||||||
similarArtists,
|
similarArtists,
|
||||||
} from '@/api/artist';
|
} from '@/api/artist';
|
||||||
|
import { getTrackDetail } from '@/api/track';
|
||||||
import locale from '@/locale';
|
import locale from '@/locale';
|
||||||
import { isAccountLoggedIn } from '@/utils/auth';
|
import { isAccountLoggedIn } from '@/utils/auth';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
|
|
@ -241,7 +242,9 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['player']),
|
...mapState(['player']),
|
||||||
albums() {
|
albums() {
|
||||||
return this.albumsData.filter(a => a.type === '专辑');
|
return this.albumsData.filter(
|
||||||
|
a => a.type === '专辑' || a.type === '精选集'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
eps() {
|
eps() {
|
||||||
return this.albumsData.filter(a =>
|
return this.albumsData.filter(a =>
|
||||||
|
|
@ -276,7 +279,7 @@ export default {
|
||||||
this.$parent.$refs.main.scrollTo({ top: 0 });
|
this.$parent.$refs.main.scrollTo({ top: 0 });
|
||||||
getArtist(id).then(data => {
|
getArtist(id).then(data => {
|
||||||
this.artist = data.artist;
|
this.artist = data.artist;
|
||||||
this.popularTracks = data.hotSongs;
|
this.setPopularTracks(data.hotSongs);
|
||||||
if (next !== undefined) next();
|
if (next !== undefined) next();
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
this.show = true;
|
this.show = true;
|
||||||
|
|
@ -289,9 +292,17 @@ export default {
|
||||||
this.mvs = data.mvs;
|
this.mvs = data.mvs;
|
||||||
this.hasMoreMV = data.hasMore;
|
this.hasMoreMV = data.hasMore;
|
||||||
});
|
});
|
||||||
|
if (isAccountLoggedIn()) {
|
||||||
similarArtists(id).then(data => {
|
similarArtists(id).then(data => {
|
||||||
this.similarArtists = data.artists;
|
this.similarArtists = data.artists;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setPopularTracks(hotSongs) {
|
||||||
|
const trackIDs = hotSongs.map(t => t.id);
|
||||||
|
getTrackDetail(trackIDs.join(',')).then(data => {
|
||||||
|
this.popularTracks = data.songs;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
goToAlbum(id) {
|
goToAlbum(id) {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-show="show">
|
<div v-show="show">
|
||||||
<h1>
|
<h1>
|
||||||
<img class="avatar" :src="artist.img1v1Url | resizeImage(1024)" />{{
|
<img
|
||||||
artist.name
|
class="avatar"
|
||||||
}}'s Music Videos
|
:src="artist.img1v1Url | resizeImage(1024)"
|
||||||
|
loading="lazy"
|
||||||
|
/>{{ artist.name }}'s Music Videos
|
||||||
</h1>
|
</h1>
|
||||||
<MvRow :mvs="mvs" subtitle="publishTime" />
|
<MvRow :mvs="mvs" subtitle="publishTime" />
|
||||||
<div class="load-more">
|
<div class="load-more">
|
||||||
|
|
|
||||||
|
|
@ -66,13 +66,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapMutations } from 'vuex';
|
import { mapState, mapMutations } from 'vuex';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import {
|
import { topPlaylist, highQualityPlaylist, toplists } from '@/api/playlist';
|
||||||
topPlaylist,
|
|
||||||
highQualityPlaylist,
|
|
||||||
recommendPlaylist,
|
|
||||||
toplists,
|
|
||||||
} from '@/api/playlist';
|
|
||||||
import { playlistCategories } from '@/utils/staticData';
|
import { playlistCategories } from '@/utils/staticData';
|
||||||
|
import { getRecommendPlayList } from '@/utils/playList';
|
||||||
|
|
||||||
import ButtonTwoTone from '@/components/ButtonTwoTone.vue';
|
import ButtonTwoTone from '@/components/ButtonTwoTone.vue';
|
||||||
import CoverRow from '@/components/CoverRow.vue';
|
import CoverRow from '@/components/CoverRow.vue';
|
||||||
|
|
@ -124,10 +120,13 @@ export default {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.show) NProgress.start();
|
if (!this.show) NProgress.start();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
this.activeCategory =
|
const queryCategory = this.$route.query.category;
|
||||||
this.$route.query.category === undefined
|
if (queryCategory === undefined) {
|
||||||
? '全部'
|
this.playlists = [];
|
||||||
: this.$route.query.category;
|
this.activeCategory = '全部';
|
||||||
|
} else {
|
||||||
|
this.activeCategory = queryCategory;
|
||||||
|
}
|
||||||
this.getPlaylist();
|
this.getPlaylist();
|
||||||
},
|
},
|
||||||
goToCategory(Category) {
|
goToCategory(Category) {
|
||||||
|
|
@ -155,9 +154,9 @@ export default {
|
||||||
return this.getTopPlayList();
|
return this.getTopPlayList();
|
||||||
},
|
},
|
||||||
getRecommendPlayList() {
|
getRecommendPlayList() {
|
||||||
recommendPlaylist({ limit: 100 }).then(data => {
|
getRecommendPlayList(100, true).then(list => {
|
||||||
this.playlists = [];
|
this.playlists = [];
|
||||||
this.updatePlaylist(data.result);
|
this.updatePlaylist(list);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getHighQualityPlaylist() {
|
getHighQualityPlaylist() {
|
||||||
|
|
|
||||||
|
|
@ -69,10 +69,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { toplists, recommendPlaylist } from '@/api/playlist';
|
import { toplists } from '@/api/playlist';
|
||||||
import { toplistOfArtists } from '@/api/artist';
|
import { toplistOfArtists } from '@/api/artist';
|
||||||
import { byAppleMusic } from '@/utils/staticData';
|
|
||||||
import { newAlbums } from '@/api/album';
|
import { newAlbums } from '@/api/album';
|
||||||
|
import { byAppleMusic } from '@/utils/staticData';
|
||||||
|
import { getRecommendPlayList } from '@/utils/playList';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import CoverRow from '@/components/CoverRow.vue';
|
import CoverRow from '@/components/CoverRow.vue';
|
||||||
|
|
@ -112,10 +113,8 @@ export default {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.show) NProgress.start();
|
if (!this.show) NProgress.start();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
recommendPlaylist({
|
getRecommendPlayList(10, false).then(items => {
|
||||||
limit: 10,
|
this.recommendPlaylist.items = items;
|
||||||
}).then(data => {
|
|
||||||
this.recommendPlaylist.items = data.result;
|
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
this.show = true;
|
this.show = true;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-show="show" ref="library">
|
<div v-show="show" ref="library">
|
||||||
<h1>
|
<h1>
|
||||||
<img class="avatar" :src="data.user.avatarUrl | resizeImage" />{{
|
<img
|
||||||
data.user.nickname
|
class="avatar"
|
||||||
}}{{ $t('library.sLibrary') }}
|
:src="data.user.avatarUrl | resizeImage"
|
||||||
|
loading="lazy"
|
||||||
|
/>{{ data.user.nickname }}{{ $t('library.sLibrary') }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="section-one">
|
<div class="section-one">
|
||||||
<div class="liked-songs" @click="goToLikedSongsList">
|
<div class="liked-songs" @click="goToLikedSongsList">
|
||||||
|
|
@ -112,7 +114,7 @@
|
||||||
<div v-show="currentTab === 'playlists'">
|
<div v-show="currentTab === 'playlists'">
|
||||||
<div v-if="liked.playlists.length > 1">
|
<div v-if="liked.playlists.length > 1">
|
||||||
<CoverRow
|
<CoverRow
|
||||||
:items="filterPlaylists.slice(1)"
|
:items="filterPlaylists"
|
||||||
type="playlist"
|
type="playlist"
|
||||||
sub-text="creator"
|
sub-text="creator"
|
||||||
:show-play-button="true"
|
:show-play-button="true"
|
||||||
|
|
@ -213,7 +215,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapMutations, mapState } from 'vuex';
|
import { mapActions, mapMutations, mapState } from 'vuex';
|
||||||
import { randomNum, dailyTask } from '@/utils/common';
|
import { randomNum } from '@/utils/common';
|
||||||
import { isAccountLoggedIn } from '@/utils/auth';
|
import { isAccountLoggedIn } from '@/utils/auth';
|
||||||
import { uploadSong } from '@/api/user';
|
import { uploadSong } from '@/api/user';
|
||||||
import { getLyric } from '@/api/track';
|
import { getLyric } from '@/api/track';
|
||||||
|
|
@ -267,7 +269,7 @@ export default {
|
||||||
// Pick 3 or fewer lyrics based on the lyric lines.
|
// Pick 3 or fewer lyrics based on the lyric lines.
|
||||||
const lyricsToPick = Math.min(lyricLine.length, 3);
|
const lyricsToPick = Math.min(lyricLine.length, 3);
|
||||||
|
|
||||||
// The upperbound of the lyric line to pick
|
// The upperBound of the lyric line to pick
|
||||||
const randomUpperBound = lyricLine.length - lyricsToPick;
|
const randomUpperBound = lyricLine.length - lyricsToPick;
|
||||||
const startLyricLineIndex = randomNum(0, randomUpperBound - 1);
|
const startLyricLineIndex = randomNum(0, randomUpperBound - 1);
|
||||||
|
|
||||||
|
|
@ -280,7 +282,7 @@ export default {
|
||||||
return this.data.libraryPlaylistFilter || 'all';
|
return this.data.libraryPlaylistFilter || 'all';
|
||||||
},
|
},
|
||||||
filterPlaylists() {
|
filterPlaylists() {
|
||||||
const playlists = this.liked.playlists;
|
const playlists = this.liked.playlists.slice(1);
|
||||||
const userId = this.data.user.userId;
|
const userId = this.data.user.userId;
|
||||||
if (this.playlistFilter === 'mine') {
|
if (this.playlistFilter === 'mine') {
|
||||||
return playlists.filter(p => p.creator.userId === userId);
|
return playlists.filter(p => p.creator.userId === userId);
|
||||||
|
|
@ -292,7 +294,8 @@ export default {
|
||||||
playHistoryList() {
|
playHistoryList() {
|
||||||
if (this.show && this.playHistoryMode === 'week') {
|
if (this.show && this.playHistoryMode === 'week') {
|
||||||
return this.liked.playHistory.weekData;
|
return this.liked.playHistory.weekData;
|
||||||
} else if (this.show && this.playHistoryMode === 'all') {
|
}
|
||||||
|
if (this.show && this.playHistoryMode === 'all') {
|
||||||
return this.liked.playHistory.allData;
|
return this.liked.playHistory.allData;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -307,7 +310,6 @@ export default {
|
||||||
activated() {
|
activated() {
|
||||||
this.$parent.$refs.scrollbar.restorePosition();
|
this.$parent.$refs.scrollbar.restorePosition();
|
||||||
this.loadData();
|
this.loadData();
|
||||||
dailyTask();
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['showToast']),
|
...mapActions(['showToast']),
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,19 @@
|
||||||
<div class="title">{{ $t('login.loginText') }}</div>
|
<div class="title">{{ $t('login.loginText') }}</div>
|
||||||
<div class="section-2">
|
<div class="section-2">
|
||||||
<div v-show="mode === 'phone'" class="input-box">
|
<div v-show="mode === 'phone'" class="input-box">
|
||||||
<div class="container" :class="{ active: inputFocus === 'phone' }">
|
<div
|
||||||
|
class="container"
|
||||||
|
:class="{ active: ['phone', 'countryCode'].includes(inputFocus) }"
|
||||||
|
>
|
||||||
<svg-icon icon-class="mobile" />
|
<svg-icon icon-class="mobile" />
|
||||||
<div class="inputs">
|
<div class="inputs">
|
||||||
<input
|
<input
|
||||||
id="countryCode"
|
id="countryCode"
|
||||||
v-model="countryCode"
|
v-model="countryCode"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
inputFocus === 'phone' ? '' : $t('login.countryCode')
|
inputFocus === 'countryCode' ? '' : $t('login.countryCode')
|
||||||
"
|
"
|
||||||
@focus="inputFocus = 'phone'"
|
@focus="inputFocus = 'countryCode'"
|
||||||
@blur="inputFocus = ''"
|
@blur="inputFocus = ''"
|
||||||
@keyup.enter="login"
|
@keyup.enter="login"
|
||||||
/>
|
/>
|
||||||
|
|
@ -69,7 +72,7 @@
|
||||||
|
|
||||||
<div v-show="mode == 'qrCode'">
|
<div v-show="mode == 'qrCode'">
|
||||||
<div v-show="qrCodeSvg" class="qr-code-container">
|
<div v-show="qrCodeSvg" class="qr-code-container">
|
||||||
<img :src="qrCodeSvg" />
|
<img :src="qrCodeSvg" loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
<div class="qr-code-info">
|
<div class="qr-code-info">
|
||||||
{{ qrCodeInformation }}
|
{{ qrCodeInformation }}
|
||||||
|
|
@ -87,14 +90,14 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="other-login">
|
<div class="other-login">
|
||||||
<a v-show="mode !== 'email'" @click="changeMode('email')">{{
|
<!-- <a v-show="mode !== 'email'" @click="changeMode('email')">{{
|
||||||
$t('login.loginWithEmail')
|
$t('login.loginWithEmail')
|
||||||
}}</a>
|
}}</a>
|
||||||
<span v-show="mode === 'qrCode'">|</span>
|
<span v-show="mode === 'qrCode'">|</span>
|
||||||
<a v-show="mode !== 'phone'" @click="changeMode('phone')">{{
|
<a v-show="mode !== 'phone'" @click="changeMode('phone')">{{
|
||||||
$t('login.loginWithPhone')
|
$t('login.loginWithPhone')
|
||||||
}}</a>
|
}}</a>
|
||||||
<span v-show="mode !== 'qrCode'">|</span>
|
<span v-show="mode !== 'qrCode'">|</span> -->
|
||||||
<a v-show="mode !== 'qrCode'" @click="changeMode('qrCode')">
|
<a v-show="mode !== 'qrCode'" @click="changeMode('qrCode')">
|
||||||
二维码登录
|
二维码登录
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -261,6 +264,8 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
checkQrCodeLogin() {
|
checkQrCodeLogin() {
|
||||||
|
// 清除二维码检测
|
||||||
|
clearInterval(this.qrCodeCheckInterval);
|
||||||
this.qrCodeCheckInterval = setInterval(() => {
|
this.qrCodeCheckInterval = setInterval(() => {
|
||||||
if (this.qrCodeKey === '') return;
|
if (this.qrCodeKey === '') return;
|
||||||
loginQrCodeCheck(this.qrCodeKey).then(result => {
|
loginQrCodeCheck(this.qrCodeKey).then(result => {
|
||||||
|
|
@ -275,7 +280,7 @@ export default {
|
||||||
clearInterval(this.qrCodeCheckInterval);
|
clearInterval(this.qrCodeCheckInterval);
|
||||||
this.qrCodeInformation = '登录成功,请稍等...';
|
this.qrCodeInformation = '登录成功,请稍等...';
|
||||||
result.code = 200;
|
result.code = 200;
|
||||||
result.cookie = result.cookie.replace('HTTPOnly', '');
|
result.cookie = result.cookie.replaceAll(' HTTPOnly', '');
|
||||||
this.handleLoginResponse(result);
|
this.handleLoginResponse(result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,11 @@
|
||||||
:class="{ active: user.nickname === activeUser.nickname }"
|
:class="{ active: user.nickname === activeUser.nickname }"
|
||||||
@click="activeUser = user"
|
@click="activeUser = user"
|
||||||
>
|
>
|
||||||
<img class="head" :src="user.avatarUrl | resizeImage" />
|
<img
|
||||||
|
class="head"
|
||||||
|
:src="user.avatarUrl | resizeImage"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
<div class="nickname">
|
<div class="nickname">
|
||||||
{{ user.nickname }}
|
{{ user.nickname }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,12 @@
|
||||||
|
|
||||||
<div class="left-side">
|
<div class="left-side">
|
||||||
<div>
|
<div>
|
||||||
|
<div v-if="settings.showLyricsTime" class="date">
|
||||||
|
{{ date }}
|
||||||
|
</div>
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<div class="cover-container">
|
<div class="cover-container">
|
||||||
<img :src="imageUrl" />
|
<img :src="imageUrl" loading="lazy" />
|
||||||
<div
|
<div
|
||||||
class="shadow"
|
class="shadow"
|
||||||
:style="{ backgroundImage: `url(${imageUrl})` }"
|
:style="{ backgroundImage: `url(${imageUrl})` }"
|
||||||
|
|
@ -76,6 +79,29 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="top-right">
|
||||||
|
<div class="volume-control">
|
||||||
|
<button-icon :title="$t('player.mute')" @click.native="mute">
|
||||||
|
<svg-icon v-show="volume > 0.5" icon-class="volume" />
|
||||||
|
<svg-icon v-show="volume === 0" icon-class="volume-mute" />
|
||||||
|
<svg-icon
|
||||||
|
v-show="volume <= 0.5 && volume !== 0"
|
||||||
|
icon-class="volume-half"
|
||||||
|
/>
|
||||||
|
</button-icon>
|
||||||
|
<div class="volume-bar">
|
||||||
|
<vue-slider
|
||||||
|
v-model="volume"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:interval="0.01"
|
||||||
|
:drag-on-click="true"
|
||||||
|
:duration="0"
|
||||||
|
tooltip="none"
|
||||||
|
:dot-size="12"
|
||||||
|
></vue-slider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button-icon
|
<button-icon
|
||||||
:title="$t('player.like')"
|
:title="$t('player.like')"
|
||||||
|
|
@ -87,11 +113,18 @@
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</button-icon>
|
</button-icon>
|
||||||
|
<button-icon
|
||||||
|
:title="$t('contextMenu.addToPlaylist')"
|
||||||
|
@click.native="addToPlaylist"
|
||||||
|
>
|
||||||
|
<svg-icon icon-class="plus" />
|
||||||
|
</button-icon>
|
||||||
<!-- <button-icon @click.native="openMenu" title="Menu"
|
<!-- <button-icon @click.native="openMenu" title="Menu"
|
||||||
><svg-icon icon-class="more"
|
><svg-icon icon-class="more"
|
||||||
/></button-icon> -->
|
/></button-icon> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<span>{{ formatTrackTime(player.progress) || '0:00' }}</span>
|
<span>{{ formatTrackTime(player.progress) || '0:00' }}</span>
|
||||||
<div class="slider">
|
<div class="slider">
|
||||||
|
|
@ -168,6 +201,28 @@
|
||||||
>
|
>
|
||||||
<svg-icon icon-class="shuffle" />
|
<svg-icon icon-class="shuffle" />
|
||||||
</button-icon>
|
</button-icon>
|
||||||
|
<button-icon
|
||||||
|
v-show="
|
||||||
|
isShowLyricTypeSwitch &&
|
||||||
|
$store.state.settings.showLyricsTranslation &&
|
||||||
|
lyricType === 'translation'
|
||||||
|
"
|
||||||
|
:title="$t('player.translationLyric')"
|
||||||
|
@click.native="switchLyricType"
|
||||||
|
>
|
||||||
|
<span class="lyric-switch-icon">译</span>
|
||||||
|
</button-icon>
|
||||||
|
<button-icon
|
||||||
|
v-show="
|
||||||
|
isShowLyricTypeSwitch &&
|
||||||
|
$store.state.settings.showLyricsTranslation &&
|
||||||
|
lyricType === 'romaPronunciation'
|
||||||
|
"
|
||||||
|
:title="$t('player.PronunciationLyric')"
|
||||||
|
@click.native="switchLyricType"
|
||||||
|
>
|
||||||
|
<span class="lyric-switch-icon">音</span>
|
||||||
|
</button-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -182,7 +237,7 @@
|
||||||
>
|
>
|
||||||
<div id="line-1" class="line"></div>
|
<div id="line-1" class="line"></div>
|
||||||
<div
|
<div
|
||||||
v-for="(line, index) in lyricWithTranslation"
|
v-for="(line, index) in lyricToShow"
|
||||||
:id="`line${index}`"
|
:id="`line${index}`"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="line"
|
class="line"
|
||||||
|
|
@ -192,7 +247,12 @@
|
||||||
@click="clickLyricLine(line.time)"
|
@click="clickLyricLine(line.time)"
|
||||||
@dblclick="clickLyricLine(line.time, true)"
|
@dblclick="clickLyricLine(line.time, true)"
|
||||||
>
|
>
|
||||||
<span v-if="line.contents[0]">{{ line.contents[0] }}</span>
|
<div class="content">
|
||||||
|
<span
|
||||||
|
v-if="line.contents[0]"
|
||||||
|
@click.right="openLyricMenu($event, line, 0)"
|
||||||
|
>{{ line.contents[0] }}</span
|
||||||
|
>
|
||||||
<br />
|
<br />
|
||||||
<span
|
<span
|
||||||
v-if="
|
v-if="
|
||||||
|
|
@ -200,10 +260,27 @@
|
||||||
$store.state.settings.showLyricsTranslation
|
$store.state.settings.showLyricsTranslation
|
||||||
"
|
"
|
||||||
class="translation"
|
class="translation"
|
||||||
|
@click.right="openLyricMenu($event, line, 1)"
|
||||||
>{{ line.contents[1] }}</span
|
>{{ line.contents[1] }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ContextMenu v-if="!noLyric" ref="lyricMenu">
|
||||||
|
<div class="item" @click="copyLyric(false)">{{
|
||||||
|
$t('contextMenu.copyLyric')
|
||||||
|
}}</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
rightClickLyric &&
|
||||||
|
rightClickLyric.contents[1] &&
|
||||||
|
$store.state.settings.showLyricsTranslation
|
||||||
|
"
|
||||||
|
class="item"
|
||||||
|
@click="copyLyric(true)"
|
||||||
|
>{{ $t('contextMenu.copyLyricWithTranslation') }}</div
|
||||||
|
>
|
||||||
|
</ContextMenu>
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<div class="close-button" @click="toggleLyrics">
|
<div class="close-button" @click="toggleLyrics">
|
||||||
|
|
@ -211,6 +288,12 @@
|
||||||
<svg-icon icon-class="arrow-down" />
|
<svg-icon icon-class="arrow-down" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="close-button" style="left: 24px" @click="fullscreen">
|
||||||
|
<button>
|
||||||
|
<svg-icon v-if="isFullscreen" icon-class="fullscreen-exit" />
|
||||||
|
<svg-icon v-else icon-class="fullscreen" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -221,28 +304,37 @@
|
||||||
|
|
||||||
import { mapState, mapMutations, mapActions } from 'vuex';
|
import { mapState, mapMutations, mapActions } from 'vuex';
|
||||||
import VueSlider from 'vue-slider-component';
|
import VueSlider from 'vue-slider-component';
|
||||||
|
import ContextMenu from '@/components/ContextMenu.vue';
|
||||||
import { formatTrackTime } from '@/utils/common';
|
import { formatTrackTime } from '@/utils/common';
|
||||||
import { getLyric } from '@/api/track';
|
import { getLyric } from '@/api/track';
|
||||||
import { lyricParser } from '@/utils/lyrics';
|
import { lyricParser, copyLyric } from '@/utils/lyrics';
|
||||||
import ButtonIcon from '@/components/ButtonIcon.vue';
|
import ButtonIcon from '@/components/ButtonIcon.vue';
|
||||||
import * as Vibrant from 'node-vibrant';
|
import * as Vibrant from 'node-vibrant/dist/vibrant.worker.min.js';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
|
import { isAccountLoggedIn } from '@/utils/auth';
|
||||||
import { hasListSource, getListSourcePath } from '@/utils/playList';
|
import { hasListSource, getListSourcePath } from '@/utils/playList';
|
||||||
|
import locale from '@/locale';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Lyrics',
|
name: 'Lyrics',
|
||||||
components: {
|
components: {
|
||||||
VueSlider,
|
VueSlider,
|
||||||
ButtonIcon,
|
ButtonIcon,
|
||||||
|
ContextMenu,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lyricsInterval: null,
|
lyricsInterval: null,
|
||||||
lyric: [],
|
lyric: [],
|
||||||
tlyric: [],
|
tlyric: [],
|
||||||
|
romalyric: [],
|
||||||
|
lyricType: 'translation', // or 'romaPronunciation'
|
||||||
highlightLyricIndex: -1,
|
highlightLyricIndex: -1,
|
||||||
minimize: true,
|
minimize: true,
|
||||||
background: '',
|
background: '',
|
||||||
|
date: this.formatTime(new Date()),
|
||||||
|
isFullscreen: !!document.fullscreenElement,
|
||||||
|
rightClickLyric: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -250,12 +342,28 @@ export default {
|
||||||
currentTrack() {
|
currentTrack() {
|
||||||
return this.player.currentTrack;
|
return this.player.currentTrack;
|
||||||
},
|
},
|
||||||
|
volume: {
|
||||||
|
get() {
|
||||||
|
return this.player.volume;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.player.volume = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
imageUrl() {
|
imageUrl() {
|
||||||
return this.player.currentTrack?.al?.picUrl + '?param=1024y1024';
|
return this.player.currentTrack?.al?.picUrl + '?param=1024y1024';
|
||||||
},
|
},
|
||||||
bgImageUrl() {
|
bgImageUrl() {
|
||||||
return this.player.currentTrack?.al?.picUrl + '?param=512y512';
|
return this.player.currentTrack?.al?.picUrl + '?param=512y512';
|
||||||
},
|
},
|
||||||
|
isShowLyricTypeSwitch() {
|
||||||
|
return this.romalyric.length > 0 && this.tlyric.length > 0;
|
||||||
|
},
|
||||||
|
lyricToShow() {
|
||||||
|
return this.lyricType === 'translation'
|
||||||
|
? this.lyricWithTranslation
|
||||||
|
: this.lyricWithRomaPronunciation;
|
||||||
|
},
|
||||||
lyricWithTranslation() {
|
lyricWithTranslation() {
|
||||||
let ret = [];
|
let ret = [];
|
||||||
// 空内容的去除
|
// 空内容的去除
|
||||||
|
|
@ -287,6 +395,37 @@ export default {
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
lyricWithRomaPronunciation() {
|
||||||
|
let ret = [];
|
||||||
|
// 空内容的去除
|
||||||
|
const lyricFiltered = this.lyric.filter(({ content }) =>
|
||||||
|
Boolean(content)
|
||||||
|
);
|
||||||
|
// content统一转换数组形式
|
||||||
|
if (lyricFiltered.length) {
|
||||||
|
lyricFiltered.forEach(l => {
|
||||||
|
const { rawTime, time, content } = l;
|
||||||
|
const lyricItem = { time, content, contents: [content] };
|
||||||
|
const sameTimeRomaLyric = this.romalyric.find(
|
||||||
|
({ rawTime: tLyricRawTime }) => tLyricRawTime === rawTime
|
||||||
|
);
|
||||||
|
if (sameTimeRomaLyric) {
|
||||||
|
const { content: romaLyricContent } = sameTimeRomaLyric;
|
||||||
|
if (content) {
|
||||||
|
lyricItem.contents.push(romaLyricContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret.push(lyricItem);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ret = lyricFiltered.map(({ time, content }) => ({
|
||||||
|
time,
|
||||||
|
content,
|
||||||
|
contents: [content],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
lyricFontSize() {
|
lyricFontSize() {
|
||||||
return {
|
return {
|
||||||
fontSize: `${this.$store.state.settings.lyricFontSize || 28}px`,
|
fontSize: `${this.$store.state.settings.lyricFontSize || 28}px`,
|
||||||
|
|
@ -325,13 +464,71 @@ export default {
|
||||||
created() {
|
created() {
|
||||||
this.getLyric();
|
this.getLyric();
|
||||||
this.getCoverColor();
|
this.getCoverColor();
|
||||||
|
this.initDate();
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'F11') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.fullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener('fullscreenchange', () => {
|
||||||
|
this.isFullscreen = !!document.fullscreenElement;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeDestroy: function () {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
clearInterval(this.lyricsInterval);
|
clearInterval(this.lyricsInterval);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(['toggleLyrics']),
|
...mapMutations(['toggleLyrics', 'updateModal']),
|
||||||
...mapActions(['likeATrack']),
|
...mapActions(['likeATrack']),
|
||||||
|
initDate() {
|
||||||
|
var _this = this;
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = setInterval(function () {
|
||||||
|
_this.date = _this.formatTime(new Date());
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
formatTime(value) {
|
||||||
|
let hour = value.getHours().toString();
|
||||||
|
let minute = value.getMinutes().toString();
|
||||||
|
let second = value.getSeconds().toString();
|
||||||
|
return (
|
||||||
|
hour.padStart(2, '0') +
|
||||||
|
':' +
|
||||||
|
minute.padStart(2, '0') +
|
||||||
|
':' +
|
||||||
|
second.padStart(2, '0')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
fullscreen() {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
document.documentElement.requestFullscreen();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addToPlaylist() {
|
||||||
|
if (!isAccountLoggedIn()) {
|
||||||
|
this.showToast(locale.t('toast.needToLogin'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$store.dispatch('fetchLikedPlaylist');
|
||||||
|
this.updateModal({
|
||||||
|
modalName: 'addTrackToPlaylistModal',
|
||||||
|
key: 'show',
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
this.updateModal({
|
||||||
|
modalName: 'addTrackToPlaylistModal',
|
||||||
|
key: 'selectedTrackID',
|
||||||
|
value: this.currentTrack?.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
playPrevTrack() {
|
playPrevTrack() {
|
||||||
this.player.playPrevTrack();
|
this.player.playPrevTrack();
|
||||||
},
|
},
|
||||||
|
|
@ -351,9 +548,10 @@ export default {
|
||||||
if (!data?.lrc?.lyric) {
|
if (!data?.lrc?.lyric) {
|
||||||
this.lyric = [];
|
this.lyric = [];
|
||||||
this.tlyric = [];
|
this.tlyric = [];
|
||||||
|
this.romalyric = [];
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
let { lyric, tlyric } = lyricParser(data);
|
let { lyric, tlyric, romalyric } = lyricParser(data);
|
||||||
lyric = lyric.filter(
|
lyric = lyric.filter(
|
||||||
l => !/^作(词|曲)\s*(:|:)\s*无$/.exec(l.content)
|
l => !/^作(词|曲)\s*(:|:)\s*无$/.exec(l.content)
|
||||||
);
|
);
|
||||||
|
|
@ -373,15 +571,27 @@ export default {
|
||||||
if (lyric.length === 1 && includeAM) {
|
if (lyric.length === 1 && includeAM) {
|
||||||
this.lyric = [];
|
this.lyric = [];
|
||||||
this.tlyric = [];
|
this.tlyric = [];
|
||||||
|
this.romalyric = [];
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
this.lyric = lyric;
|
this.lyric = lyric;
|
||||||
this.tlyric = tlyric;
|
this.tlyric = tlyric;
|
||||||
|
this.romalyric = romalyric;
|
||||||
|
if (tlyric.length * romalyric.length > 0) {
|
||||||
|
this.lyricType = 'translation';
|
||||||
|
} else {
|
||||||
|
this.lyricType =
|
||||||
|
lyric.length > 0 ? 'translation' : 'romaPronunciation';
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
switchLyricType() {
|
||||||
|
this.lyricType =
|
||||||
|
this.lyricType === 'translation' ? 'romaPronunciation' : 'translation';
|
||||||
|
},
|
||||||
formatTrackTime(value) {
|
formatTrackTime(value) {
|
||||||
return formatTrackTime(value);
|
return formatTrackTime(value);
|
||||||
},
|
},
|
||||||
|
|
@ -400,9 +610,24 @@ export default {
|
||||||
this.player.play();
|
this.player.play();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
openLyricMenu(e, lyric, idx) {
|
||||||
|
this.rightClickLyric = { ...lyric, idx };
|
||||||
|
this.$refs.lyricMenu.openMenu(e);
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
copyLyric(withTranslation) {
|
||||||
|
if (this.rightClickLyric) {
|
||||||
|
const idx = this.rightClickLyric.idx;
|
||||||
|
if (!withTranslation) {
|
||||||
|
copyLyric(this.rightClickLyric.contents[idx]);
|
||||||
|
} else {
|
||||||
|
copyLyric(this.rightClickLyric.contents.join(' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
setLyricsInterval() {
|
setLyricsInterval() {
|
||||||
this.lyricsInterval = setInterval(() => {
|
this.lyricsInterval = setInterval(() => {
|
||||||
const progress = this.player.seek() ?? 0;
|
const progress = this.player.seek(null, false) ?? 0;
|
||||||
let oldHighlightLyricIndex = this.highlightLyricIndex;
|
let oldHighlightLyricIndex = this.highlightLyricIndex;
|
||||||
this.highlightLyricIndex = this.lyric.findIndex((l, index) => {
|
this.highlightLyricIndex = this.lyric.findIndex((l, index) => {
|
||||||
const nextLyric = this.lyric[index + 1];
|
const nextLyric = this.lyric[index + 1];
|
||||||
|
|
@ -431,13 +656,13 @@ export default {
|
||||||
},
|
},
|
||||||
getCoverColor() {
|
getCoverColor() {
|
||||||
if (this.settings.lyricsBackground !== true) return;
|
if (this.settings.lyricsBackground !== true) return;
|
||||||
const cover = this.currentTrack.al?.picUrl + '?param=1024y1024';
|
const cover = this.currentTrack.al?.picUrl + '?param=256y256';
|
||||||
Vibrant.from(cover, { colorCount: 1 })
|
Vibrant.from(cover, { colorCount: 1 })
|
||||||
.getPalette()
|
.getPalette()
|
||||||
.then(palette => {
|
.then(palette => {
|
||||||
const orignColor = Color.rgb(palette.DarkMuted._rgb);
|
const originColor = Color.rgb(palette.DarkMuted._rgb);
|
||||||
const color = orignColor.darken(0.1).rgb().string();
|
const color = originColor.darken(0.1).rgb().string();
|
||||||
const color2 = orignColor.lighten(0.28).rotate(-30).rgb().string();
|
const color2 = originColor.lighten(0.28).rotate(-30).rgb().string();
|
||||||
this.background = `linear-gradient(to top left, ${color}, ${color2})`;
|
this.background = `linear-gradient(to top left, ${color}, ${color2})`;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -447,6 +672,9 @@ export default {
|
||||||
getListPath() {
|
getListPath() {
|
||||||
return getListSourcePath();
|
return getListSourcePath();
|
||||||
},
|
},
|
||||||
|
mute() {
|
||||||
|
this.player.mute();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -534,6 +762,20 @@ export default {
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
.date {
|
||||||
|
max-width: 54vh;
|
||||||
|
margin: 24px 0;
|
||||||
|
color: var(--color-text);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.88;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
max-width: 54vh;
|
max-width: 54vh;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
|
|
@ -564,6 +806,19 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.top-right {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.volume-control {
|
||||||
|
margin: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.volume-bar {
|
||||||
|
width: 84px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -578,6 +833,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
margin-top: 22px;
|
margin-top: 22px;
|
||||||
|
|
@ -639,6 +895,12 @@ export default {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.lyric-switch-icon {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 14px;
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -697,20 +959,24 @@ export default {
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-secondary-bg-for-transparent);
|
background: var(--color-secondary-bg-for-transparent);
|
||||||
}
|
}
|
||||||
&:active {
|
|
||||||
|
.content {
|
||||||
|
transform-origin: center left;
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
opacity: 0.28;
|
opacity: 0.28;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
font-size: 0.9em;
|
font-size: 1em;
|
||||||
transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
}
|
}
|
||||||
|
|
||||||
span.translation {
|
span.translation {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
font-size: 0.825em;
|
font-size: 0.925em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -722,20 +988,18 @@ export default {
|
||||||
margin-top: 0.1em;
|
margin-top: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight div.content {
|
||||||
transform-origin: center left;
|
transform: scale(1);
|
||||||
transform: scale(1.05);
|
span {
|
||||||
}
|
|
||||||
|
|
||||||
.highlight span {
|
|
||||||
opacity: 0.98;
|
opacity: 0.98;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight span.translation {
|
span.translation {
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,14 @@
|
||||||
}}</router-link>
|
}}</router-link>
|
||||||
-
|
-
|
||||||
{{ mv.data.name }}
|
{{ mv.data.name }}
|
||||||
<div class="like-button">
|
<div class="buttons">
|
||||||
<button-icon @click.native="likeMV">
|
<button-icon class="button" @click.native="likeMV">
|
||||||
<svg-icon v-if="mv.subed" icon-class="heart-solid"></svg-icon>
|
<svg-icon v-if="mv.subed" icon-class="heart-solid"></svg-icon>
|
||||||
<svg-icon v-else icon-class="heart"></svg-icon>
|
<svg-icon v-else icon-class="heart"></svg-icon>
|
||||||
</button-icon>
|
</button-icon>
|
||||||
|
<button-icon class="button" @click.native="openMenu">
|
||||||
|
<svg-icon icon-class="more"></svg-icon>
|
||||||
|
</button-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
|
|
@ -28,6 +31,14 @@
|
||||||
<div class="section-title">{{ $t('mv.moreVideo') }}</div>
|
<div class="section-title">{{ $t('mv.moreVideo') }}</div>
|
||||||
<MvRow :mvs="simiMvs" />
|
<MvRow :mvs="simiMvs" />
|
||||||
</div>
|
</div>
|
||||||
|
<ContextMenu ref="mvMenu">
|
||||||
|
<div class="item" @click="copyUrl(mv.data.id)">{{
|
||||||
|
$t('contextMenu.copyUrl')
|
||||||
|
}}</div>
|
||||||
|
<div class="item" @click="openInBrowser(mv.data.id)">{{
|
||||||
|
$t('contextMenu.openInBrowser')
|
||||||
|
}}</div>
|
||||||
|
</ContextMenu>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -40,6 +51,7 @@ import '@/assets/css/plyr.css';
|
||||||
import Plyr from 'plyr';
|
import Plyr from 'plyr';
|
||||||
|
|
||||||
import ButtonIcon from '@/components/ButtonIcon.vue';
|
import ButtonIcon from '@/components/ButtonIcon.vue';
|
||||||
|
import ContextMenu from '@/components/ContextMenu.vue';
|
||||||
import MvRow from '@/components/MvRow.vue';
|
import MvRow from '@/components/MvRow.vue';
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
|
|
||||||
|
|
@ -48,6 +60,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
MvRow,
|
MvRow,
|
||||||
ButtonIcon,
|
ButtonIcon,
|
||||||
|
ContextMenu,
|
||||||
},
|
},
|
||||||
beforeRouteUpdate(to, from, next) {
|
beforeRouteUpdate(to, from, next) {
|
||||||
this.getData(to.params.id);
|
this.getData(to.params.id);
|
||||||
|
|
@ -127,6 +140,23 @@ export default {
|
||||||
if (data.code === 200) this.mv.subed = !this.mv.subed;
|
if (data.code === 200) this.mv.subed = !this.mv.subed;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
openMenu(e) {
|
||||||
|
this.$refs.mvMenu.openMenu(e);
|
||||||
|
},
|
||||||
|
copyUrl(id) {
|
||||||
|
let showToast = this.showToast;
|
||||||
|
this.$copyText(`https://music.163.com/#/mv?id=${id}`)
|
||||||
|
.then(function () {
|
||||||
|
showToast(locale.t('toast.copied'));
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast(`${locale.t('toast.copyFailed')}${error}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openInBrowser(id) {
|
||||||
|
const url = `https://music.163.com/#/mv?id=${id}`;
|
||||||
|
window.open(url);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -181,8 +211,11 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.like-button {
|
.buttons {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
height: 18px;
|
height: 18px;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,9 @@ export default {
|
||||||
this.player.current + 1,
|
this.player.current + 1,
|
||||||
this.player.current + 100
|
this.player.current + 100
|
||||||
);
|
);
|
||||||
return this.tracks.filter(t => trackIDs.includes(t.id));
|
return trackIDs
|
||||||
|
.map(tid => this.tracks.find(t => t.id === tid))
|
||||||
|
.filter(t => t);
|
||||||
},
|
},
|
||||||
playNextList() {
|
playNextList() {
|
||||||
return this.player.playNextList;
|
return this.player.playNextList;
|
||||||
|
|
|
||||||
|
|
@ -139,9 +139,12 @@
|
||||||
|
|
||||||
<div v-if="isLikeSongsPage" class="user-info">
|
<div v-if="isLikeSongsPage" class="user-info">
|
||||||
<h1>
|
<h1>
|
||||||
<img class="avatar" :src="data.user.avatarUrl | resizeImage" />{{
|
<img
|
||||||
data.user.nickname
|
class="avatar"
|
||||||
}}{{ $t('library.sLikedSongs') }}
|
:src="data.user.avatarUrl | resizeImage"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
{{ data.user.nickname }}{{ $t('library.sLikedSongs') }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="search-box-likepage" @click="searchInPlaylist()">
|
<div class="search-box-likepage" @click="searchInPlaylist()">
|
||||||
<div class="container" :class="{ active: inputFocus }">
|
<div class="container" :class="{ active: inputFocus }">
|
||||||
|
|
@ -315,6 +318,10 @@ const specialPlaylist = {
|
||||||
name: '一周原创发现',
|
name: '一周原创发现',
|
||||||
gradient: 'gradient-blue-purple',
|
gradient: 'gradient-blue-purple',
|
||||||
},
|
},
|
||||||
|
2829883282: {
|
||||||
|
name: '华语私人雷达',
|
||||||
|
gradient: 'gradient-yellow-red',
|
||||||
|
},
|
||||||
3136952023: {
|
3136952023: {
|
||||||
name: '私人雷达',
|
name: '私人雷达',
|
||||||
gradient: 'gradient-radar',
|
gradient: 'gradient-radar',
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -63,6 +63,16 @@ module.exports = {
|
||||||
.loader('node-loader')
|
.loader('node-loader')
|
||||||
.end();
|
.end();
|
||||||
|
|
||||||
|
config.module
|
||||||
|
.rule('webpack4_es_fallback')
|
||||||
|
.test(/\.js$/)
|
||||||
|
.include.add(/node_modules/)
|
||||||
|
.end()
|
||||||
|
.use('esbuild-loader')
|
||||||
|
.loader('esbuild-loader')
|
||||||
|
.options({ target: 'es2015', format: "cjs" })
|
||||||
|
.end();
|
||||||
|
|
||||||
// LimitChunkCountPlugin 可以通过合并块来对块进行后期处理。用以解决 chunk 包太多的问题
|
// LimitChunkCountPlugin 可以通过合并块来对块进行后期处理。用以解决 chunk 包太多的问题
|
||||||
config.plugin('chunkPlugin').use(webpack.optimize.LimitChunkCountPlugin, [
|
config.plugin('chunkPlugin').use(webpack.optimize.LimitChunkCountPlugin, [
|
||||||
{
|
{
|
||||||
|
|
@ -169,6 +179,16 @@ module.exports = {
|
||||||
'jsbi',
|
'jsbi',
|
||||||
path.join(__dirname, 'node_modules/jsbi/dist/jsbi-cjs.js')
|
path.join(__dirname, 'node_modules/jsbi/dist/jsbi-cjs.js')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
config.module
|
||||||
|
.rule('webpack4_es_fallback')
|
||||||
|
.test(/\.js$/)
|
||||||
|
.include.add(/node_modules/)
|
||||||
|
.end()
|
||||||
|
.use('esbuild-loader')
|
||||||
|
.loader('esbuild-loader')
|
||||||
|
.options({ target: 'es2015', format: "cjs" })
|
||||||
|
.end();
|
||||||
},
|
},
|
||||||
// 渲染线程的配置文件
|
// 渲染线程的配置文件
|
||||||
chainWebpackRendererProcess: config => {
|
chainWebpackRendererProcess: config => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue