mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 19:37:35 +08:00
Compare commits
88 Commits
v3.0.0-alp
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc49b7ad00 | ||
|
|
fcf78cdd08 | ||
|
|
98fbc81d2f | ||
|
|
5ad562ab1c | ||
|
|
10dc011f9a | ||
|
|
3df6e91f95 | ||
|
|
31ae15d242 | ||
|
|
a2cfbb5e52 | ||
|
|
c4bd94daae | ||
|
|
e18828cd08 | ||
|
|
82f6b4607b | ||
|
|
3874ab3483 | ||
|
|
fc297cb198 | ||
|
|
aec06c5a55 | ||
|
|
87ce076f26 | ||
|
|
45b1bf130d | ||
|
|
3966578015 | ||
|
|
fed7b3678b | ||
|
|
ac6ce257b8 | ||
|
|
b8b8f747d3 | ||
|
|
53468b2e3a | ||
|
|
ecadf6ade7 | ||
|
|
8187b09fcb | ||
|
|
89117d4198 | ||
|
|
6158dd2750 | ||
|
|
86f33d054a | ||
|
|
52e8458590 | ||
|
|
554cf45500 | ||
|
|
60f751713a | ||
|
|
edd9b38cfc | ||
|
|
9a87d73289 | ||
|
|
317763e2c3 | ||
|
|
aa3f9d2ca8 | ||
|
|
e64a5ba1bc | ||
|
|
00e6f7bb60 | ||
|
|
883ef05ab4 | ||
|
|
d0b5eb3371 | ||
|
|
590ef96aa7 | ||
|
|
172ca5a2f3 | ||
|
|
6e1e56c1bd | ||
|
|
105fed4bd0 | ||
|
|
8bd3dc56d8 | ||
|
|
8e88bf64b1 | ||
|
|
244a832c52 | ||
|
|
146af3aeba | ||
|
|
143e8e29d7 | ||
|
|
c702e6e01a | ||
|
|
b2ddb9f4e2 | ||
|
|
201186bab2 | ||
|
|
bfcd59daca | ||
|
|
d5c3843c3f | ||
|
|
d3f307eac5 | ||
|
|
675a52b8d1 | ||
|
|
aee90e9c4e | ||
|
|
eb39b81d8d | ||
|
|
436df47104 | ||
|
|
e04e5e34c6 | ||
|
|
b57d685c03 | ||
|
|
6684172592 | ||
|
|
0257e74ff0 | ||
|
|
16c8865651 | ||
|
|
1a61aa2458 | ||
|
|
e543f07d8e | ||
|
|
96a0495a88 | ||
|
|
02befcd8a4 | ||
|
|
15b500806a | ||
|
|
9accf5d27d | ||
|
|
191ab29a44 | ||
|
|
a4d4cd5f70 | ||
|
|
3b07f7346f | ||
|
|
1edceeebdd | ||
|
|
fbf261f80b | ||
|
|
432fa18299 | ||
|
|
b2dcec840b | ||
|
|
6f93d65f02 | ||
|
|
852b901353 | ||
|
|
71a282157d | ||
|
|
49f462cdf5 | ||
|
|
94c0ca70e1 | ||
|
|
813637762c | ||
|
|
eb39b85a8b | ||
|
|
201fd8d687 | ||
|
|
02116d8f0f | ||
|
|
551a190edf | ||
|
|
f5015a4028 | ||
|
|
ea55616bd6 | ||
|
|
2b8eb93404 | ||
|
|
18113d94e9 |
100
.github/workflows/build.yml
vendored
100
.github/workflows/build.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# Dev 分支推送部署预览
|
||||
## 仅部署 Win 端
|
||||
## 部署 Windows x64 和 ARM64 版本
|
||||
name: Build Dev
|
||||
|
||||
on:
|
||||
@@ -8,8 +8,9 @@ on:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and electron app
|
||||
# Windows x64 架构
|
||||
release-x64:
|
||||
name: Build Electron App for Windows (x64)
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
@@ -20,7 +21,10 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
node-version: "22.x"
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
@@ -31,31 +35,77 @@ jobs:
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App
|
||||
run: npm run build:win
|
||||
run: pnpm install
|
||||
# 清理旧的构建产物
|
||||
- name: Clean dist folder
|
||||
run: |
|
||||
if (Test-Path dist) {
|
||||
Remove-Item -Recurse -Force dist
|
||||
}
|
||||
# 构建 Electron App (x64)
|
||||
- name: Build Electron App for Windows x64
|
||||
run: pnpm run build:win -- --x64 || true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 清理不必要的构建产物
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 清理不必要的构建产物(保留 .exe 和 .blockmap 文件)
|
||||
- name: Cleanup Artifacts
|
||||
run: npx del-cli "dist/**/*" "!dist/*.exe"
|
||||
# 上传构建产物
|
||||
run: npx del-cli "dist/**/*.yaml" "dist/**/*.yml"
|
||||
# 上传构建产物(仅上传 x64 架构的 .exe 文件)
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlayer-dev
|
||||
path: dist
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
name: SPlayer-dev-x64
|
||||
path: |
|
||||
dist/*x64*.exe
|
||||
dist/*x64*.exe.blockmap
|
||||
|
||||
# Windows ARM64 架构
|
||||
release-arm64:
|
||||
name: Build Electron App for Windows (ARM64)
|
||||
runs-on: windows-11-arm
|
||||
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
name: ${{ github.ref }}-rc
|
||||
body: This version is still under development, currently only provides windows version, non-developers please do not use!
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: dist/*.exe
|
||||
node-version: "22.x"
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
# 清理旧的构建产物
|
||||
- name: Clean dist folder
|
||||
run: |
|
||||
if (Test-Path dist) {
|
||||
Remove-Item -Recurse -Force dist
|
||||
}
|
||||
# 构建 Electron App (ARM64)
|
||||
- name: Build Electron App for Windows ARM64
|
||||
run: pnpm run build:win -- --arm64 || true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 清理不必要的构建产物(保留 .exe 和 .blockmap 文件)
|
||||
- name: Cleanup Artifacts
|
||||
run: npx del-cli "dist/**/*.yaml" "dist/**/*.yml"
|
||||
# 上传构建产物(仅上传 ARM64 架构的 .exe 文件)
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlayer-dev-arm64
|
||||
path: |
|
||||
dist/*arm64*.exe
|
||||
dist/*arm64*.exe.blockmap
|
||||
|
||||
246
.github/workflows/release.yml
vendored
246
.github/workflows/release.yml
vendored
@@ -1,159 +1,169 @@
|
||||
# Release 发行版本部署
|
||||
## 多端部署
|
||||
name: Build Release
|
||||
name: Build & Release SPlayer
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
- v* # 只在 tag v* 时触发
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.x
|
||||
PNPM_VERSION: 8
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
|
||||
jobs:
|
||||
# Windows
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
# ===================================================================
|
||||
# 并行构建所有平台和架构
|
||||
# ===================================================================
|
||||
build:
|
||||
name: Build on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
# 矩阵策略
|
||||
# 即使一个矩阵任务失败,其他任务也会继续运行
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
# 开始步骤
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
node-version: "20.x"
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
# 安装 Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
# 清理旧的构建产物
|
||||
- name: Clean workspace on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
Write-Host "🧹 Cleaning workspace, node_modules, and Electron caches..."
|
||||
if (Test-Path dist) { Remove-Item -Recurse -Force dist }
|
||||
if (Test-Path out) { Remove-Item -Recurse -Force out }
|
||||
if (Test-Path node_modules) { Remove-Item -Recurse -Force node_modules }
|
||||
|
||||
if (Test-Path "$env:LOCALAPPDATA\electron-builder") {
|
||||
Remove-Item "$env:LOCALAPPDATA\electron-builder" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if (Test-Path "$env:LOCALAPPDATA\electron") {
|
||||
Remove-Item "$env:LOCALAPPDATA\electron" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
- name: Clean workspace on macOS & Linux
|
||||
if: runner.os == 'macOS' || runner.os == 'Linux'
|
||||
run: |
|
||||
echo "🧹 Cleaning workspace, node_modules, and Electron caches..."
|
||||
rm -rf dist out node_modules ~/.cache/electron-builder ~/.cache/electron
|
||||
# 安装项目依赖
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
- name: Copy .env file on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for Windows
|
||||
run: npm run build:win || true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlarer-Win
|
||||
if-no-files-found: ignore
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: false
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Mac
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
- name: Copy .env file on macOS & Linux
|
||||
if: runner.os == 'macOS' || runner.os == 'Linux'
|
||||
run: |
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
else
|
||||
echo ".env file already exists. Skipping the copy step."
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for macOS
|
||||
run: npm run build:mac || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlarer-Macos
|
||||
if-no-files-found: ignore
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: false
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Linux
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 更新 Ubuntu 软件源
|
||||
- name: Ubuntu Update with sudo
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update
|
||||
# 安装依赖
|
||||
- name: Install RPM & Pacman
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get install --no-install-recommends -y rpm &&
|
||||
sudo apt-get install --no-install-recommends -y libarchive-tools &&
|
||||
sudo apt-get install --no-install-recommends -y libopenjp2-tools
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
else
|
||||
echo ".env file already exists. Skipping the copy step."
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 安裝 Snapcraft
|
||||
- name: Install Snapcraft
|
||||
if: runner.os == 'Linux'
|
||||
uses: samuelmeuli/action-snapcraft@v2
|
||||
with:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for Linux
|
||||
run: npm run build:linux || true
|
||||
- name: Build Windows x64 & ARM64 App
|
||||
if: runner.os == 'Windows'
|
||||
run: pnpm build:win || true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build macOS Universal App
|
||||
if: runner.os == 'macOS'
|
||||
run: pnpm build:mac || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload Linux artifact
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Linux x64 & ARM64 App
|
||||
if: runner.os == 'Linux'
|
||||
run: pnpm build:linux || true
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 上传 Snap 包到 Snapcraft 商店
|
||||
- name: Publish Snap to Snap Store
|
||||
if: runner.os == 'Linux'
|
||||
run: snapcraft upload dist/*.snap --release stable
|
||||
continue-on-error: true
|
||||
# 合并所有构建
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlarer-Linux
|
||||
if-no-files-found: ignore
|
||||
name: SPlayer-${{ runner.os }}
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
# ===================================================================
|
||||
# 收集并发布 Release
|
||||
# ===================================================================
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
# 需要写入权限
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
# 将所有产物下载到 artifacts 文件夹
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
# 创建 GitHub Release 并上传所有产物
|
||||
- name: Create GitHub Release and Upload Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 自动生成 Release 说明
|
||||
generate_release_notes: true
|
||||
# 发布为草稿
|
||||
draft: false
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 发布为预发布
|
||||
prerelease: false
|
||||
# 全部上传
|
||||
files: |
|
||||
!artifacts/**/*-unpacked/**
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.rpm
|
||||
artifacts/**/*.pacman
|
||||
artifacts/**/*.snap
|
||||
artifacts/**/*.tar.gz
|
||||
artifacts/**/*.yml
|
||||
artifacts/**/*.blockmap
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -28,3 +28,5 @@ components.d.ts
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env.development
|
||||
.env.production
|
||||
14
Dockerfile
14
Dockerfile
@@ -23,8 +23,16 @@ COPY --from=builder /app/out/renderer /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
RUN apk add --no-cache npm
|
||||
COPY --from=builder /app/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
RUN npm install -g NeteaseCloudMusicApi
|
||||
RUN apk add --no-cache npm python3 youtube-dl \
|
||||
&& npm install -g @unblockneteasemusic/server NeteaseCloudMusicApi \
|
||||
&& wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp \
|
||||
&& chmod +x /usr/local/bin/yt-dlp \
|
||||
&& chmod +x /docker-entrypoint.sh
|
||||
|
||||
CMD nginx && npx NeteaseCloudMusicApi
|
||||
ENV NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["npx", "NeteaseCloudMusicApi"]
|
||||
12
README.md
12
README.md
@@ -1,6 +1,12 @@
|
||||
# SPlayer
|
||||
|
||||
> 一个简约的音乐播放器
|
||||
> A simple music player
|
||||
|
||||

|
||||

|
||||
[](https://github.com/imsyy/SPlayer/actions/workflows/release.yml)
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
@@ -117,6 +123,10 @@
|
||||
|
||||
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
|
||||
|
||||
## Snap Store
|
||||
|
||||
[](https://snapcraft.io/splayer)
|
||||
|
||||
## ⚙️ Docker 部署
|
||||
|
||||
> 安装及配置 `Docker` 将不在此处说明,请自行解决
|
||||
|
||||
@@ -13,6 +13,9 @@ export default {
|
||||
"MaybeRefOrGetter": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"ShallowRef": true,
|
||||
"Slot": true,
|
||||
"Slots": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"asyncComputed": true,
|
||||
@@ -29,6 +32,7 @@ export default {
|
||||
"createGlobalState": true,
|
||||
"createInjectionState": true,
|
||||
"createReactiveFn": true,
|
||||
"createRef": true,
|
||||
"createReusableTemplate": true,
|
||||
"createSharedComposable": true,
|
||||
"createTemplatePromise": true,
|
||||
@@ -43,6 +47,7 @@ export default {
|
||||
"extendRef": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"getCurrentWatcher": true,
|
||||
"h": true,
|
||||
"ignorableWatch": true,
|
||||
"inject": true,
|
||||
@@ -52,6 +57,7 @@ export default {
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"isShallow": true,
|
||||
"makeDestructurable": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
@@ -63,6 +69,7 @@ export default {
|
||||
"onBeforeUpdate": true,
|
||||
"onClickOutside": true,
|
||||
"onDeactivated": true,
|
||||
"onElementRemoval": true,
|
||||
"onErrorCaptured": true,
|
||||
"onKeyStroke": true,
|
||||
"onLongPress": true,
|
||||
@@ -145,6 +152,7 @@ export default {
|
||||
"useCloned": true,
|
||||
"useColorMode": true,
|
||||
"useConfirmDialog": true,
|
||||
"useCountdown": true,
|
||||
"useCounter": true,
|
||||
"useCssModule": true,
|
||||
"useCssVar": true,
|
||||
@@ -229,12 +237,14 @@ export default {
|
||||
"usePreferredDark": true,
|
||||
"usePreferredLanguages": true,
|
||||
"usePreferredReducedMotion": true,
|
||||
"usePreferredReducedTransparency": true,
|
||||
"usePrevious": true,
|
||||
"useRafFn": true,
|
||||
"useRefHistory": true,
|
||||
"useResizeObserver": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSSRWidth": true,
|
||||
"useScreenOrientation": true,
|
||||
"useScreenSafeArea": true,
|
||||
"useScriptTag": true,
|
||||
@@ -261,6 +271,7 @@ export default {
|
||||
"useThrottleFn": true,
|
||||
"useThrottledRefHistory": true,
|
||||
"useTimeAgo": true,
|
||||
"useTimeAgoIntl": true,
|
||||
"useTimeout": true,
|
||||
"useTimeoutFn": true,
|
||||
"useTimeoutPoll": true,
|
||||
|
||||
10
auto-imports.d.ts
vendored
10
auto-imports.d.ts
vendored
@@ -21,6 +21,7 @@ declare global {
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createRef: typeof import('@vueuse/core')['createRef']
|
||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||
@@ -35,6 +36,7 @@ declare global {
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
||||
const h: typeof import('vue')['h']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
@@ -44,6 +46,7 @@ declare global {
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const isShallow: typeof import('vue')['isShallow']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
@@ -55,6 +58,7 @@ declare global {
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
@@ -137,6 +141,7 @@ declare global {
|
||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCountdown: typeof import('@vueuse/core')['useCountdown']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
@@ -221,12 +226,14 @@ declare global {
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
|
||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
@@ -253,6 +260,7 @@ declare global {
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
@@ -296,6 +304,6 @@ declare global {
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
@@ -17,6 +18,7 @@ declare module 'vue' {
|
||||
CoverMenu: typeof import('./src/components/Menu/CoverMenu.vue')['default']
|
||||
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
|
||||
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
|
||||
ExcludeKeywords: typeof import('./src/components/Modal/ExcludeKeywords.vue')['default']
|
||||
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
|
||||
GeneralSetting: typeof import('./src/components/Setting/GeneralSetting.vue')['default']
|
||||
JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default']
|
||||
@@ -54,6 +56,7 @@ declare module 'vue' {
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
|
||||
@@ -10,3 +10,21 @@ services:
|
||||
ports:
|
||||
- 25884:25884
|
||||
restart: always
|
||||
environment:
|
||||
# 所有变量都不是必填项
|
||||
# 网易云服务端 IP, 可在宿主机通过 ping music.163.com 获得
|
||||
- NETEASE_SERVER_IP=220.197.30.65
|
||||
# UnblockNeteaseMusic 使用的音源, 支持列表见 https://github.com/UnblockNeteaseMusic/server?tab=readme-ov-file#%E9%9F%B3%E6%BA%90%E6%B8%85%E5%8D%95
|
||||
- UNBLOCK_SOURCES=kugou kuwo bilibili
|
||||
# 可添加 UnblockNeteaseMusic 支持的任何环境变量, 支持列表见 https://github.com/UnblockNeteaseMusic/server?tab=readme-ov-file#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F
|
||||
- ENABLE_FLAC=false
|
||||
- ENABLE_HTTPDNS=false
|
||||
- BLOCK_ADS=true
|
||||
- DISABLE_UPGRADE_CHECK=false
|
||||
- DEVELOPMENT=false
|
||||
- FOLLOW_SOURCE_ORDER=true
|
||||
- JSON_LOG=false
|
||||
- NO_CACHE=false
|
||||
- SELECT_MAX_BR=true
|
||||
- LOG_LEVEL=info
|
||||
- SEARCH_ALBUM=true
|
||||
|
||||
29
docker-entrypoint.sh
Normal file
29
docker-entrypoint.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# start unblock service in the background
|
||||
npx unblockneteasemusic -p 80:443 -s -f ${NETEASE_SERVER_IP:-220.197.30.65} -o ${UNBLOCK_SOURCES:-kugou kuwo bilibili} 2>&1 &
|
||||
|
||||
# point the neteasemusic address to the unblock service
|
||||
if ! grep -q "music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface.music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface.music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface3.music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface3.music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface.music.163.com.163jiasu.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface.music.163.com.163jiasu.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface3.music.163.com.163jiasu.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface3.music.163.com.163jiasu.com" >> /etc/hosts
|
||||
fi
|
||||
|
||||
# start the nginx daemon
|
||||
nginx
|
||||
|
||||
# start the main process
|
||||
exec "$@"
|
||||
170
electron-builder.config.ts
Normal file
170
electron-builder.config.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import type { Configuration } from "electron-builder";
|
||||
|
||||
const config: Configuration = {
|
||||
// 应用程序的唯一标识符
|
||||
appId: "com.imsyy.splayer",
|
||||
// 应用程序的产品名称
|
||||
productName: "SPlayer",
|
||||
copyright: "Copyright © imsyy 2023",
|
||||
// 构建资源所在的目录
|
||||
directories: {
|
||||
buildResources: "build",
|
||||
},
|
||||
// 包含在最终应用程序构建中的文件列表
|
||||
// 使用通配符 ! 表示排除不需要的文件
|
||||
files: [
|
||||
"public/**",
|
||||
"out/**",
|
||||
"!**/.vscode/*",
|
||||
"!src/*",
|
||||
"!electron.vite.config.{js,ts,mjs,cjs}",
|
||||
"!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}",
|
||||
"!{.env,.env.*,.npmrc,pnpm-lock.yaml}",
|
||||
],
|
||||
// 哪些文件将不会被压缩,而是解压到构建目录
|
||||
asarUnpack: ["public/**"],
|
||||
win: {
|
||||
// 可执行文件名
|
||||
executableName: "SPlayer",
|
||||
// 应用程序的图标文件路径
|
||||
icon: "public/icons/logo.ico",
|
||||
// Windows 平台全局文件名模板
|
||||
artifactName: "${productName}-${version}-${arch}.${ext}",
|
||||
// 是否对可执行文件进行签名和编辑
|
||||
// signAndEditExecutable: false,
|
||||
// 构建类型(架构由命令行参数 --x64 或 --arm64 指定)
|
||||
target: [
|
||||
// 安装版
|
||||
{
|
||||
target: "nsis",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// 打包版
|
||||
{
|
||||
target: "portable",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
// NSIS 安装器配置
|
||||
nsis: {
|
||||
// 是否一键式安装
|
||||
oneClick: false,
|
||||
// 安装程序的生成名称
|
||||
artifactName: "${productName}-${version}-${arch}-setup.${ext}",
|
||||
// 创建的桌面快捷方式名称
|
||||
shortcutName: "${productName}",
|
||||
// 卸载时显示的名称
|
||||
uninstallDisplayName: "${productName}",
|
||||
// 创建桌面图标
|
||||
createDesktopShortcut: "always",
|
||||
// 是否允许 UAC 提升权限
|
||||
allowElevation: true,
|
||||
// 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true,
|
||||
// 安装包图标
|
||||
installerIcon: "public/icons/favicon.ico",
|
||||
// 卸载命令图标
|
||||
uninstallerIcon: "public/icons/favicon.ico",
|
||||
},
|
||||
// Portable 便携版配置
|
||||
portable: {
|
||||
// 便携版文件名
|
||||
artifactName: "${productName}-${version}-${arch}-portable.${ext}",
|
||||
},
|
||||
// macOS 平台配置
|
||||
mac: {
|
||||
// 可执行文件名
|
||||
executableName: "SPlayer",
|
||||
// 应用程序的图标文件路径
|
||||
icon: "public/icons/favicon-512x512.png",
|
||||
// 权限继承的文件路径
|
||||
entitlementsInherit: "build/entitlements.mac.plist",
|
||||
// macOS 平台全局文件名模板
|
||||
artifactName: "${productName}-${version}-${arch}.${ext}",
|
||||
// 扩展信息,如权限描述
|
||||
extendInfo: {
|
||||
NSCameraUsageDescription: "Application requests access to the device's camera.",
|
||||
NSMicrophoneUsageDescription: "Application requests access to the device's microphone.",
|
||||
NSDocumentsFolderUsageDescription:
|
||||
"Application requests access to the user's Documents folder.",
|
||||
NSDownloadsFolderUsageDescription:
|
||||
"Application requests access to the user's Downloads folder.",
|
||||
},
|
||||
// 是否启用应用程序的 Notarization(苹果的安全审核)
|
||||
notarize: false,
|
||||
darkModeSupport: true,
|
||||
category: "public.app-category.music",
|
||||
target: [
|
||||
// DMG 安装版
|
||||
{
|
||||
target: "dmg",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// 压缩包安装版
|
||||
{
|
||||
target: "zip",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Linux 平台配置
|
||||
linux: {
|
||||
// 可执行文件名
|
||||
executableName: "SPlayer",
|
||||
// 应用程序的图标文件路径
|
||||
icon: "public/icons/favicon-512x512.png",
|
||||
// Linux 所有格式的统一文件名模板
|
||||
artifactName: "${name}-${version}-${arch}.${ext}",
|
||||
// 构建类型 - 支持 x64 和 ARM64 架构
|
||||
target: [
|
||||
// Pacman 包管理器
|
||||
{
|
||||
target: "pacman",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// AppImage 格式
|
||||
{
|
||||
target: "AppImage",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// Debian 包管理器
|
||||
{
|
||||
target: "deb",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// RPM 包管理器
|
||||
{
|
||||
target: "rpm",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// Snap 包管理器(仅支持 x64 架构)
|
||||
{
|
||||
target: "snap",
|
||||
arch: ["x64"],
|
||||
},
|
||||
// 压缩包格式
|
||||
{
|
||||
target: "tar.gz",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
// 维护者信息
|
||||
maintainer: "imsyy.top",
|
||||
// 应用程序类别
|
||||
category: "Audio;Music",
|
||||
},
|
||||
// AppImage 特定配置
|
||||
appImage: {
|
||||
// AppImage 文件的生成名称
|
||||
artifactName: "${name}-${version}-${arch}.${ext}",
|
||||
},
|
||||
// 是否在构建之前重新编译原生模块
|
||||
npmRebuild: false,
|
||||
// Electron 下载镜像配置
|
||||
electronDownload: {
|
||||
mirror: "https://npmmirror.com/mirrors/electron/",
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,114 +0,0 @@
|
||||
# 应用程序的唯一标识符
|
||||
appId: com.imsyy.splayer
|
||||
# 应用程序的产品名称
|
||||
productName: SPlayer
|
||||
copyright: Copyright © imsyy 2023
|
||||
# 构建资源所在的目录
|
||||
directories:
|
||||
buildResources: public
|
||||
# 包含在最终应用程序构建中的文件列表
|
||||
# 使用通配符 ! 表示排除不需要的文件
|
||||
files:
|
||||
- "public/**"
|
||||
- "out/**"
|
||||
- "!**/.vscode/*"
|
||||
- "!src/*"
|
||||
- "!electron.vite.config.{js,ts,mjs,cjs}"
|
||||
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
|
||||
- "!{.env,.env.*,.npmrc,pnpm-lock.yaml}"
|
||||
# 哪些文件将不会被压缩,而是解压到构建目录
|
||||
asarUnpack:
|
||||
- public/**
|
||||
win:
|
||||
# 可执行文件名
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
# 安装版
|
||||
- nsis
|
||||
# 打包版
|
||||
- portable
|
||||
# NSIS 安装器配置
|
||||
nsis:
|
||||
# 是否一键式安装
|
||||
oneClick: false
|
||||
# 安装程序的生成名称
|
||||
artifactName: ${productName}-${version}-setup.${ext}
|
||||
# 创建的桌面快捷方式名称
|
||||
shortcutName: ${productName}
|
||||
# 卸载时显示的名称
|
||||
uninstallDisplayName: ${productName}
|
||||
# 创建桌面图标
|
||||
createDesktopShortcut: always
|
||||
# 是否允许 UAC 提升权限
|
||||
allowElevation: true
|
||||
# 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true
|
||||
# 安装包图标
|
||||
installerIcon: public/icons/favicon.ico
|
||||
# 卸载命令图标
|
||||
uninstallerIcon: public/icons/favicon.ico
|
||||
# macOS 平台配置
|
||||
mac:
|
||||
# 可执行文件名
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/icons/favicon-512x512.png
|
||||
# 权限继承的文件路径
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
# 扩展信息,如权限描述
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
# 是否启用应用程序的 Notarization(苹果的安全审核)
|
||||
notarize: false
|
||||
darkModeSupport: true
|
||||
category: public.app-category.music
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: zip
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
# macOS 平台的 DMG 配置
|
||||
dmg:
|
||||
# DMG 文件的生成名称
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
# Linux 平台配置
|
||||
linux:
|
||||
# 可执行文件名
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
- pacman
|
||||
- AppImage
|
||||
- deb
|
||||
- rpm
|
||||
- tar.gz
|
||||
# 维护者信息
|
||||
maintainer: imsyy.top
|
||||
# 应用程序类别
|
||||
category: Audio;Music
|
||||
# AppImage 配置
|
||||
appImage:
|
||||
# AppImage 文件的生成名称
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
# 是否在构建之前重新编译原生模块
|
||||
npmRebuild: false
|
||||
# 自动更新的配置
|
||||
publish:
|
||||
# 更新提供商
|
||||
provider: github
|
||||
# 自动更新检查的 URL
|
||||
# url: https://example.com/auto-updates
|
||||
owner: "imsyy"
|
||||
repo: "SPlayer"
|
||||
@@ -88,7 +88,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
"/api": {
|
||||
target: `http://127.0.0.1:${servePort}`,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, "/api/"),
|
||||
rewrite: (path) => path.replace(/^\/api/, "/api"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { app, shell, BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { electronApp } from "@electron-toolkit/utils";
|
||||
import { join } from "path";
|
||||
import { release, type } from "os";
|
||||
import { isDev, isMac, appName } from "./utils";
|
||||
import { registerAllShortcuts, unregisterShortcuts } from "./shortcut";
|
||||
import { unregisterShortcuts } from "./shortcut";
|
||||
import { initTray, MainTray } from "./tray";
|
||||
import { initThumbar, Thumbar } from "./thumbar";
|
||||
import { type StoreType, initStore } from "./store";
|
||||
import Store from "electron-store";
|
||||
import initAppServer from "../server";
|
||||
import initIpcMain from "./ipcMain";
|
||||
import log from "./logger";
|
||||
import store from "./store";
|
||||
// icon
|
||||
import icon from "../../public/icons/favicon.png?asset";
|
||||
|
||||
@@ -29,6 +30,8 @@ class MainProcess {
|
||||
mainWindow: BrowserWindow | null = null;
|
||||
lyricWindow: BrowserWindow | null = null;
|
||||
loadingWindow: BrowserWindow | null = null;
|
||||
// store
|
||||
store: Store<StoreType> | null = null;
|
||||
// 托盘
|
||||
mainTray: MainTray | null = null;
|
||||
// 工具栏
|
||||
@@ -38,7 +41,7 @@ class MainProcess {
|
||||
constructor() {
|
||||
log.info("🚀 Main process startup");
|
||||
// 禁用 Windows 7 的 GPU 加速功能
|
||||
if (release().startsWith("6.1") && type() == 'Windows_NT') app.disableHardwareAcceleration();
|
||||
if (release().startsWith("6.1") && type() == "Windows_NT") app.disableHardwareAcceleration();
|
||||
// 单例锁
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
log.error("❌ There is already a program running and this process is terminated");
|
||||
@@ -46,10 +49,12 @@ class MainProcess {
|
||||
process.exit(0);
|
||||
} else this.showWindow();
|
||||
// 准备就绪
|
||||
app.whenReady().then(async () => {
|
||||
app.on("ready", async () => {
|
||||
log.info("🚀 Application Process Startup");
|
||||
// 设置应用程序名称
|
||||
electronApp.setAppUserModelId(app.getName());
|
||||
electronApp.setAppUserModelId("com.imsyy.splayer");
|
||||
// 初始化 store
|
||||
this.store = initStore();
|
||||
// 启动主服务进程
|
||||
await initAppServer();
|
||||
// 启动进程
|
||||
@@ -68,10 +73,8 @@ class MainProcess {
|
||||
this.loadingWindow,
|
||||
this.mainTray,
|
||||
this.thumbar,
|
||||
store,
|
||||
this.store,
|
||||
);
|
||||
// 注册快捷键
|
||||
registerAllShortcuts(this.mainWindow!);
|
||||
});
|
||||
}
|
||||
// 创建窗口
|
||||
@@ -111,10 +114,10 @@ class MainProcess {
|
||||
createMainWindow() {
|
||||
// 窗口配置项
|
||||
const options: BrowserWindowConstructorOptions = {
|
||||
width: store.get("window").width,
|
||||
height: store.get("window").height,
|
||||
minHeight: 800,
|
||||
minWidth: 1280,
|
||||
width: this.store?.get("window").width,
|
||||
height: this.store?.get("window").height,
|
||||
minHeight: 600,
|
||||
minWidth: 800,
|
||||
// 菜单栏
|
||||
titleBarStyle: "customButtonsOnHover",
|
||||
// 立即显示窗口
|
||||
@@ -132,8 +135,8 @@ class MainProcess {
|
||||
}
|
||||
|
||||
// 配置网络代理
|
||||
if (store.get("proxy")) {
|
||||
this.mainWindow.webContents.session.setProxy({ proxyRules: store.get("proxy") });
|
||||
if (this.store?.get("proxy")) {
|
||||
this.mainWindow.webContents.session.setProxy({ proxyRules: this.store?.get("proxy") });
|
||||
}
|
||||
|
||||
// 窗口打开处理程序
|
||||
@@ -162,15 +165,15 @@ class MainProcess {
|
||||
createLyricsWindow() {
|
||||
// 初始化窗口
|
||||
this.lyricWindow = this.createWindow({
|
||||
width: store.get("lyric").width || 800,
|
||||
height: store.get("lyric").height || 180,
|
||||
width: this.store?.get("lyric").width || 800,
|
||||
height: this.store?.get("lyric").height || 180,
|
||||
minWidth: 440,
|
||||
minHeight: 120,
|
||||
maxWidth: 1600,
|
||||
maxHeight: 300,
|
||||
// 窗口位置
|
||||
x: store.get("lyric").x,
|
||||
y: store.get("lyric").y,
|
||||
x: this.store?.get("lyric").x,
|
||||
y: this.store?.get("lyric").y,
|
||||
transparent: true,
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
alwaysOnTop: true,
|
||||
@@ -213,11 +216,6 @@ class MainProcess {
|
||||
this.showWindow();
|
||||
});
|
||||
|
||||
// 开发环境控制台
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
console.log("Received custom protocol URL:", url);
|
||||
@@ -236,6 +234,10 @@ class MainProcess {
|
||||
}
|
||||
// 窗口事件
|
||||
handleWindowEvents() {
|
||||
this.mainWindow?.on("ready-to-show", () => {
|
||||
if (!this.mainWindow) return;
|
||||
this.thumbar = initThumbar(this.mainWindow);
|
||||
});
|
||||
this.mainWindow?.on("show", () => {
|
||||
// this.mainWindow?.webContents.send("lyricsScroll");
|
||||
});
|
||||
@@ -257,7 +259,7 @@ class MainProcess {
|
||||
const bounds = this.lyricWindow?.getBounds();
|
||||
if (bounds) {
|
||||
const { width, height } = bounds;
|
||||
store.set("lyric", { ...store.get("lyric"), width, height });
|
||||
this.store?.set("lyric", { ...this.store?.get("lyric"), width, height });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -275,7 +277,7 @@ class MainProcess {
|
||||
saveBounds() {
|
||||
if (this.mainWindow?.isFullScreen()) return;
|
||||
const bounds = this.mainWindow?.getBounds();
|
||||
if (bounds) store.set("window", bounds);
|
||||
if (bounds) this.store?.set("window", bounds);
|
||||
}
|
||||
// 显示窗口
|
||||
showWindow() {
|
||||
|
||||
@@ -17,13 +17,14 @@ import { Thumbar } from "./thumbar";
|
||||
import { StoreType } from "./store";
|
||||
import { isDev, getFileID, getFileMD5 } from "./utils";
|
||||
import { isShortcutRegistered, registerShortcut, unregisterShortcuts } from "./shortcut";
|
||||
import { join, basename, resolve } from "path";
|
||||
import { join, basename, resolve, relative, isAbsolute } from "path";
|
||||
import { download } from "electron-dl";
|
||||
import { checkUpdate, startDownloadUpdate } from "./update";
|
||||
import fs from "fs/promises";
|
||||
import log from "../main/logger";
|
||||
import Store from "electron-store";
|
||||
import fg from "fast-glob";
|
||||
import openLoginWin from "./loginWin";
|
||||
|
||||
// 注册 ipcMain
|
||||
const initIpcMain = (
|
||||
@@ -172,8 +173,11 @@ const initWinIpcMain = (
|
||||
// 遍历音乐文件
|
||||
ipcMain.handle("get-music-files", async (_, dirPath: string) => {
|
||||
try {
|
||||
// 规范化路径
|
||||
const filePath = resolve(dirPath).replace(/\\/g, "/");
|
||||
console.info(`📂 Fetching music files from: ${filePath}`);
|
||||
// 查找指定目录下的所有音乐文件
|
||||
const musicFiles = await fg("**/*.{mp3,wav,flac}", { cwd: dirPath });
|
||||
const musicFiles = await fg("**/*.{mp3,wav,flac}", { cwd: filePath });
|
||||
// 解析元信息
|
||||
const metadataPromises = musicFiles.map(async (file) => {
|
||||
const filePath = join(dirPath, file);
|
||||
@@ -213,18 +217,19 @@ const initWinIpcMain = (
|
||||
// 获取音乐元信息
|
||||
ipcMain.handle("get-music-metadata", async (_, path: string) => {
|
||||
try {
|
||||
const { common, format } = await parseFile(path);
|
||||
const filePath = resolve(path).replace(/\\/g, "/");
|
||||
const { common, format } = await parseFile(filePath);
|
||||
return {
|
||||
// 文件名称
|
||||
fileName: basename(path),
|
||||
fileName: basename(filePath),
|
||||
// 文件大小
|
||||
fileSize: (await fs.stat(path)).size / (1024 * 1024),
|
||||
fileSize: (await fs.stat(filePath)).size / (1024 * 1024),
|
||||
// 元信息
|
||||
common,
|
||||
// 音质信息
|
||||
format,
|
||||
// md5
|
||||
md5: await getFileMD5(path),
|
||||
md5: await getFileMD5(filePath),
|
||||
};
|
||||
} catch (error) {
|
||||
log.error("❌ Error fetching music metadata:", error);
|
||||
@@ -235,17 +240,13 @@ const initWinIpcMain = (
|
||||
// 获取音乐歌词
|
||||
ipcMain.handle("get-music-lyric", async (_, path: string): Promise<string> => {
|
||||
try {
|
||||
const { common, native } = await parseFile(path);
|
||||
const filePath = resolve(path).replace(/\\/g, "/");
|
||||
const { common } = await parseFile(filePath);
|
||||
const lyric = common?.lyrics;
|
||||
if (lyric && lyric.length > 0) return String(lyric[0]);
|
||||
else {
|
||||
// 尝试读取 UNSYNCEDLYRICS
|
||||
const nativeTags = native["ID3v2.3"] || native["ID3v2.4"];
|
||||
const usltTag = nativeTags?.find((tag) => tag.id === "USLT");
|
||||
if (usltTag) return String(usltTag.value.text);
|
||||
// 如果歌词数据不存在,尝试读取同名的 lrc 文件
|
||||
else {
|
||||
const lrcFilePath = path.replace(/\.[^.]+$/, ".lrc");
|
||||
const lrcFilePath = filePath.replace(/\.[^.]+$/, ".lrc");
|
||||
try {
|
||||
await fs.access(lrcFilePath);
|
||||
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
|
||||
@@ -254,7 +255,6 @@ const initWinIpcMain = (
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("❌ Error fetching music lyric:", error);
|
||||
throw error;
|
||||
@@ -535,6 +535,9 @@ const initWinIpcMain = (
|
||||
|
||||
// 开始下载更新
|
||||
ipcMain.on("start-download-update", () => startDownloadUpdate());
|
||||
|
||||
// 新建窗口
|
||||
ipcMain.on("open-login-web", () => openLoginWin(win!));
|
||||
};
|
||||
|
||||
// lyric
|
||||
@@ -619,6 +622,16 @@ const initLyricIpcMain = (
|
||||
lyricWin.setIgnoreMouseEvents(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否是子文件夹
|
||||
ipcMain.handle("check-if-subfolder", (_, localFilesPath: string[], selectedDir: string) => {
|
||||
const resolvedSelectedDir = resolve(selectedDir);
|
||||
const allPaths = localFilesPath.map((p) => resolve(p));
|
||||
return allPaths.some((existingPath) => {
|
||||
const relativePath = relative(existingPath, resolvedSelectedDir);
|
||||
return relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// tray
|
||||
@@ -666,6 +679,10 @@ const initTrayIpcMain = (
|
||||
// thumbar
|
||||
const initThumbarIpcMain = (thumbar: Thumbar | null): void => {
|
||||
if (!thumbar) return;
|
||||
// 更新工具栏
|
||||
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
|
||||
thumbar?.updateThumbar(playStatus);
|
||||
});
|
||||
};
|
||||
|
||||
// store
|
||||
|
||||
72
electron/main/loginWin.ts
Normal file
72
electron/main/loginWin.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { BrowserWindow, session } from "electron";
|
||||
import icon from "../../public/icons/favicon.png?asset";
|
||||
import { join } from "path";
|
||||
|
||||
const openLoginWin = async (mainWin: BrowserWindow) => {
|
||||
let loginTimer: NodeJS.Timeout;
|
||||
const loginSession = session.fromPartition("persist:login");
|
||||
// 清除 Cookie
|
||||
await loginSession.clearStorageData({
|
||||
storages: ["cookies", "localstorage"],
|
||||
});
|
||||
const loginWin = new BrowserWindow({
|
||||
parent: mainWin,
|
||||
title: "登录网易云音乐( 若遇到无响应请关闭后重试 )",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
center: true,
|
||||
autoHideMenuBar: true,
|
||||
icon,
|
||||
// resizable: false,
|
||||
// movable: false,
|
||||
// minimizable: false,
|
||||
// maximizable: false,
|
||||
webPreferences: {
|
||||
session: loginSession,
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
},
|
||||
});
|
||||
|
||||
// 打开网易云
|
||||
loginWin.loadURL("https://music.163.com/#/login/");
|
||||
|
||||
// 阻止新窗口创建
|
||||
loginWin.webContents.setWindowOpenHandler(() => {
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// 检查是否登录
|
||||
const checkLogin = async () => {
|
||||
try {
|
||||
loginWin.webContents.executeJavaScript(
|
||||
"document.title = '登录网易云音乐( 若遇到无响应请关闭后重试 )'",
|
||||
);
|
||||
// 是否登录?判断 MUSIC_U
|
||||
const MUSIC_U = await loginSession.cookies.get({
|
||||
name: "MUSIC_U",
|
||||
});
|
||||
if (MUSIC_U && MUSIC_U?.length > 0) {
|
||||
if (loginTimer) clearInterval(loginTimer);
|
||||
const value = `MUSIC_U=${MUSIC_U[0].value};`;
|
||||
// 发送回主进程
|
||||
mainWin?.webContents.send("send-cookies", value);
|
||||
loginWin.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 循环检查
|
||||
loginWin.webContents.once("did-finish-load", () => {
|
||||
loginWin.show();
|
||||
loginTimer = setInterval(checkLogin, 1000);
|
||||
loginWin.on("closed", () => {
|
||||
clearInterval(loginTimer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default openLoginWin;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BrowserWindow, globalShortcut } from "electron";
|
||||
import { isDev } from "./utils";
|
||||
import { globalShortcut } from "electron";
|
||||
import log from "../main/logger";
|
||||
|
||||
// 注册快捷键并检查
|
||||
@@ -29,15 +28,3 @@ export const unregisterShortcuts = () => {
|
||||
globalShortcut.unregisterAll();
|
||||
log.info("🚫 All shortcuts unregistered.");
|
||||
};
|
||||
|
||||
// 注册所有快捷键
|
||||
export const registerAllShortcuts = (win: BrowserWindow) => {
|
||||
// 开启控制台
|
||||
registerShortcut("CmdOrCtrl+Shift+I", () => {
|
||||
win.webContents.openDevTools({
|
||||
title: "SPlayer DevTools",
|
||||
// 客户端分离
|
||||
mode: isDev ? "right" : "detach",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Store from "electron-store";
|
||||
import { screen } from "electron";
|
||||
import log from "./logger";
|
||||
|
||||
log.info("🌱 Store init");
|
||||
@@ -24,7 +25,8 @@ export interface StoreType {
|
||||
}
|
||||
|
||||
// 初始化仓库
|
||||
const store = new Store<StoreType>({
|
||||
export const initStore = () => {
|
||||
return new Store<StoreType>({
|
||||
defaults: {
|
||||
window: {
|
||||
width: 1280,
|
||||
@@ -34,13 +36,12 @@ const store = new Store<StoreType>({
|
||||
fontSize: 30,
|
||||
mainColor: "#fff",
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x: screen.getPrimaryDisplay().workAreaSize.width / 2 - 400,
|
||||
y: screen.getPrimaryDisplay().workAreaSize.height - 90,
|
||||
width: 800,
|
||||
height: 180,
|
||||
},
|
||||
proxy: "",
|
||||
},
|
||||
});
|
||||
|
||||
export default store;
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ type ThumbarMap = Map<ThumbarKeys, ThumbarButton>;
|
||||
|
||||
export interface Thumbar {
|
||||
clearThumbar(): void;
|
||||
updateThumbar(playing: boolean, clean?: boolean): void;
|
||||
}
|
||||
|
||||
// 工具栏图标
|
||||
@@ -32,12 +33,12 @@ const createThumbarButtons = (win: BrowserWindow): ThumbarMap => {
|
||||
.set(ThumbarKeys.Prev, {
|
||||
tooltip: "上一曲",
|
||||
icon: thumbarIcon("prev"),
|
||||
click: () => win.webContents.send("play-prev"),
|
||||
click: () => win.webContents.send("playPrev"),
|
||||
})
|
||||
.set(ThumbarKeys.Next, {
|
||||
tooltip: "下一曲",
|
||||
icon: thumbarIcon("next"),
|
||||
click: () => win.webContents.send("play-next"),
|
||||
click: () => win.webContents.send("playNext"),
|
||||
})
|
||||
.set(ThumbarKeys.Play, {
|
||||
tooltip: "播放",
|
||||
@@ -47,7 +48,7 @@ const createThumbarButtons = (win: BrowserWindow): ThumbarMap => {
|
||||
.set(ThumbarKeys.Pause, {
|
||||
tooltip: "暂停",
|
||||
icon: thumbarIcon("pause"),
|
||||
click: () => win.webContents.send("play-pause"),
|
||||
click: () => win.webContents.send("pause"),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -75,7 +76,7 @@ class createThumbar implements Thumbar {
|
||||
this.updateThumbar();
|
||||
}
|
||||
// 更新工具栏
|
||||
private updateThumbar(playing: boolean = false, clean: boolean = false) {
|
||||
updateThumbar(playing: boolean = false, clean: boolean = false) {
|
||||
if (clean) return this.clearThumbar();
|
||||
this._win.setThumbarButtons([this._prev, playing ? this._pause : this._play, this._next]);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
} from "electron";
|
||||
import { isWin, isLinux, isDev, appName } from "./utils";
|
||||
import { isWin, appName } from "./utils";
|
||||
import { join } from "path";
|
||||
import log from "./logger";
|
||||
|
||||
@@ -269,12 +269,8 @@ class CreateTray implements MainTray {
|
||||
|
||||
export const initTray = (win: BrowserWindow, lyricWin: BrowserWindow) => {
|
||||
try {
|
||||
// 若为 MacOS
|
||||
if (isWin || isLinux || isDev) {
|
||||
log.info("🚀 Tray Process Startup");
|
||||
return new CreateTray(win, lyricWin);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
log.error("❌ Tray Process Error", error);
|
||||
return null;
|
||||
|
||||
@@ -11,8 +11,10 @@ import log from "../main/logger";
|
||||
const initAppServer = async () => {
|
||||
try {
|
||||
const server = fastify({
|
||||
routerOptions: {
|
||||
// 忽略尾随斜杠
|
||||
ignoreTrailingSlash: true,
|
||||
},
|
||||
});
|
||||
// 注册插件
|
||||
server.register(fastifyCookie);
|
||||
@@ -46,7 +48,7 @@ const initAppServer = async () => {
|
||||
server.register(initNcmAPI, { prefix: "/api" });
|
||||
server.register(initUnblockAPI, { prefix: "/api" });
|
||||
// 启动端口
|
||||
const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884);
|
||||
const port = Number(process.env["VITE_SERVER_PORT"] || 25884);
|
||||
await server.listen({ port });
|
||||
log.info(`🌐 Starting AppServer on port ${port}`);
|
||||
return server;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import { pathCase } from "change-case";
|
||||
import NeteaseCloudMusicApi from "NeteaseCloudMusicApi";
|
||||
import NeteaseCloudMusicApi from "@neteaseapireborn/api";
|
||||
import log from "../../main/logger";
|
||||
|
||||
// 获取数据
|
||||
@@ -33,12 +33,12 @@ const initNcmAPI = async (fastify: FastifyInstance) => {
|
||||
// 主信息
|
||||
fastify.get("/netease", (_, reply) => {
|
||||
reply.send({
|
||||
name: "NeteaseCloudMusicApi",
|
||||
version: "4.20.0",
|
||||
description: "网易云音乐 Node.js API service",
|
||||
author: "@binaryify",
|
||||
name: "@neteaseapireborn/api",
|
||||
version: "4.29.2",
|
||||
description: "网易云音乐 API Enhanced",
|
||||
author: "@MoeFurina",
|
||||
license: "MIT",
|
||||
url: "https://gitlab.com/Binaryify/neteasecloudmusicapi",
|
||||
url: "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
14
electron/server/port.ts
Normal file
14
electron/server/port.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import getPort from "get-port";
|
||||
|
||||
// 默认端口
|
||||
let webPort: number;
|
||||
let servePort: number;
|
||||
|
||||
const getSafePort = async () => {
|
||||
if (webPort && servePort) return { webPort, servePort };
|
||||
webPort = await getPort({ port: 14558 });
|
||||
servePort = await getPort({ port: 25884 });
|
||||
return { webPort, servePort };
|
||||
};
|
||||
|
||||
export default getSafePort;
|
||||
@@ -26,11 +26,7 @@ export default [
|
||||
"**/components.d.ts",
|
||||
],
|
||||
},
|
||||
...compat.extends(
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:vue/vue3-essential",
|
||||
),
|
||||
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
|
||||
{
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
|
||||
22
nginx.conf
22
nginx.conf
@@ -15,6 +15,22 @@ server {
|
||||
rewrite ^(.*)$ /index.html last;
|
||||
}
|
||||
|
||||
location /api/netease/song/url/v1 {
|
||||
proxy_buffers 16 64k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
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/song/url/v1;
|
||||
|
||||
sub_filter '"url":"https://music.163.com' '"url":"/music/unblock';
|
||||
sub_filter_types application/json;
|
||||
sub_filter_once off;
|
||||
}
|
||||
|
||||
location /api/netease/ {
|
||||
proxy_buffers 16 64k;
|
||||
proxy_buffer_size 128k;
|
||||
@@ -26,4 +42,10 @@ server {
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
|
||||
location /music/unblock/ {
|
||||
proxy_pass https://music.163.com/;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
|
||||
134
package.json
134
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"productName": "SPlayer",
|
||||
"version": "3.0.0-alpha.4",
|
||||
"version": "3.0.0-beta.3",
|
||||
"description": "A minimalist music player",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "imsyy",
|
||||
@@ -24,91 +24,109 @@
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"build": "npx rimraf dist && npm run typecheck && electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:web": "npm run build",
|
||||
"build:unpack": "npm run build && electron-builder --dir",
|
||||
"build:win": "npm run build && electron-builder --win",
|
||||
"build:mac": "npm run build && electron-builder --mac",
|
||||
"build:linux": "npm run build && electron-builder --linux"
|
||||
"build:win": "npm run build && electron-builder --win --config electron-builder.config.ts",
|
||||
"build:mac": "npm run build && electron-builder --mac --config electron-builder.config.ts",
|
||||
"build:linux": "npm run build && electron-builder --linux --config electron-builder.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@applemusic-like-lyrics/core": "^0.1.3",
|
||||
"@applemusic-like-lyrics/lyric": "^0.2.4",
|
||||
"@applemusic-like-lyrics/vue": "^0.1.5",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@imsyy/color-utils": "^1.0.2",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@pixi/app": "^7.4.2",
|
||||
"@pixi/core": "^7.4.2",
|
||||
"@pixi/display": "^7.4.2",
|
||||
"@pixi/filter-blur": "^7.4.2",
|
||||
"@neteaseapireborn/api": "^4.29.9",
|
||||
"@pixi/app": "^7.4.3",
|
||||
"@pixi/core": "^7.4.3",
|
||||
"@pixi/display": "^7.4.3",
|
||||
"@pixi/filter-blur": "^7.4.3",
|
||||
"@pixi/filter-bulge-pinch": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.2",
|
||||
"@pixi/sprite": "^7.4.2",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"NeteaseCloudMusicApi": "^4.23.1",
|
||||
"axios": "^1.7.7",
|
||||
"@pixi/filter-color-matrix": "^7.4.3",
|
||||
"@pixi/sprite": "^7.4.3",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"axios": "^1.12.2",
|
||||
"change-case": "^5.4.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"electron-dl": "^3.5.2",
|
||||
"dayjs": "^1.11.18",
|
||||
"electron-dl": "^4.0.0",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-updater": "^6.6.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"font-list": "^1.5.1",
|
||||
"github-markdown-css": "^5.7.0",
|
||||
"font-list": "^2.0.1",
|
||||
"get-port": "^7.1.0",
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"howler": "^2.2.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jss": "^10.10.0",
|
||||
"jss-preset-default": "^10.10.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^14.1.2",
|
||||
"music-metadata": "7.14.0",
|
||||
"pinia": "^2.2.4",
|
||||
"pinia-plugin-persistedstate": "^3.2.3",
|
||||
"plyr": "^3.7.8",
|
||||
"vue-virt-list": "^1.5.2"
|
||||
"marked": "^16.4.0",
|
||||
"md5": "^2.3.0",
|
||||
"music-metadata": "^11.9.0",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"plyr": "^3.8.3",
|
||||
"vue-virt-list": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@fastify/cookie": "^9.4.0",
|
||||
"@fastify/http-proxy": "^9.5.0",
|
||||
"@fastify/multipart": "^8.3.0",
|
||||
"@fastify/static": "^7.0.4",
|
||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^4",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/http-proxy": "^11.3.0",
|
||||
"@fastify/multipart": "^9.2.1",
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.7.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
||||
"@typescript-eslint/parser": "^8.8.1",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^24.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"ajv": "^8.17.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"electron": "^28.3.3",
|
||||
"electron-builder": "^25.1.7",
|
||||
"electron-log": "^5.2.0",
|
||||
"electron-vite": "^2.3.0",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-plugin-vue": "^9.29.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fastify": "^4.28.1",
|
||||
"naive-ui": "^2.40.1",
|
||||
"node-taglib-sharp": "^5.2.3",
|
||||
"prettier": "^3.3.3",
|
||||
"sass": "^1.79.5",
|
||||
"terser": "^5.34.1",
|
||||
"typescript": "^5.6.3",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.8",
|
||||
"electron": "^38.2.2",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-vite": "^4.0.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-plugin-vue": "^10.5.0",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fastify": "^5.6.1",
|
||||
"naive-ui": "^2.43.1",
|
||||
"node-taglib-sharp": "^6.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.93.2",
|
||||
"terser": "^5.44.0",
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.9",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"vue": "3.5.10",
|
||||
"vue-router": "^4.4.5",
|
||||
"vue-tsc": "^2.1.6"
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-tsc": "^3.1.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"dmg-builder": "26.0.12",
|
||||
"electron-builder-squirrel-windows": "26.0.12"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@applemusic-like-lyrics/lyric",
|
||||
"@parcel/watcher",
|
||||
"core-js",
|
||||
"electron",
|
||||
"esbuild",
|
||||
"sharp",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
5586
pnpm-lock.yaml
generated
5586
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/icons/logo-icon.png
Normal file
BIN
public/icons/logo-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/icons/logo.ico
Normal file
BIN
public/icons/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
public/logo.ico
Normal file
BIN
public/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -55,9 +55,10 @@
|
||||
<!-- 路由页面 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive :max="20" :exclude="['layout']">
|
||||
<KeepAlive v-if="settingStore.useKeepAlive" :max="20" :exclude="['layout']">
|
||||
<component :is="Component" class="router-view" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" class="router-view" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<!-- 回顶 -->
|
||||
|
||||
@@ -65,6 +65,23 @@ export const songLyric = (id: number) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 获取格式TTML的歌词
|
||||
export const songLyricTTML = async (id: number) => {
|
||||
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response === null || response.status !== 200) {
|
||||
console.error(`TTML API请求失败或TTML仓库没有歌词, 将会使用默认歌词`);
|
||||
return null;
|
||||
}
|
||||
const data = await response.text();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('TTML API请求出错:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌曲下载链接
|
||||
* @param id 音乐 id
|
||||
@@ -114,10 +131,20 @@ export const matchSong = (
|
||||
* 歌曲动态封面
|
||||
* @param {number} id - 歌曲 id
|
||||
*/
|
||||
|
||||
export const songDynamicCover = (id: number) => {
|
||||
return request({
|
||||
url: "/song/dynamic/cover",
|
||||
params: { id },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 副歌时间
|
||||
* @param {number} id - 歌曲 id
|
||||
*/
|
||||
export const songChorus = (id: number) => {
|
||||
return request({
|
||||
url: "/song/chorus",
|
||||
params: { id },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
"id": 3136952023,
|
||||
"name": "私人雷达"
|
||||
},
|
||||
{
|
||||
"id": 8402996200,
|
||||
"name": "会员雷达"
|
||||
},
|
||||
{
|
||||
"id": 5320167908,
|
||||
"name": "时光雷达"
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<n-text class="ar"> {{ song.artists || "未知艺术家" }} </n-text>
|
||||
</div>
|
||||
<!-- 别名 -->
|
||||
<n-text v-if="song.alia" class="alia" depth="3">{{ song.alia }}</n-text>
|
||||
<n-text v-if="song.alia" class="alia text-hidden" depth="3">{{ song.alia }}</n-text>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 专辑 -->
|
||||
@@ -204,10 +204,10 @@ const localCover = async (show: boolean) => {
|
||||
border-radius: 12px;
|
||||
border: 2px solid rgba(var(--primary), 0.12);
|
||||
background-color: var(--surface-container-hex);
|
||||
transition:
|
||||
transform 0.1s,
|
||||
background-color 0.3s var(--n-bezier),
|
||||
border-color 0.3s var(--n-bezier);
|
||||
// transition:
|
||||
// transform 0.1s,
|
||||
// background-color 0.3s var(--n-bezier),
|
||||
// border-color 0.3s var(--n-bezier);
|
||||
&.play {
|
||||
border-color: rgba(var(--primary), 0.58);
|
||||
background-color: rgba(var(--primary), 0.28);
|
||||
@@ -253,6 +253,9 @@ const localCover = async (show: boolean) => {
|
||||
transition:
|
||||
opacity 0.3s,
|
||||
transform 0.3s;
|
||||
:deep(.svg-container) {
|
||||
color: var(--primary-hex);
|
||||
}
|
||||
}
|
||||
.status,
|
||||
.play {
|
||||
|
||||
@@ -122,7 +122,7 @@ const menuOptions = computed<MenuOption[] | MenuGroupOption[]>(() => {
|
||||
key: "cloud",
|
||||
link: "cloud",
|
||||
label: "我的云盘",
|
||||
show: isElectron && dataStore.loginType !== "uid",
|
||||
show: isLogin() === 1,
|
||||
icon: renderIcon("Cloud"),
|
||||
},
|
||||
{
|
||||
@@ -176,11 +176,20 @@ const menuOptions = computed<MenuOption[] | MenuGroupOption[]>(() => {
|
||||
children: [...likedPlaylist.value],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
: [
|
||||
{
|
||||
key: "local",
|
||||
link: "local",
|
||||
label: "本地歌曲",
|
||||
show: isElectron,
|
||||
icon: renderIcon("FolderMusic"),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// 生成歌单列表
|
||||
const renderPlaylist = (playlist: CoverType[], showCover: boolean) => {
|
||||
if (!isLogin()) return [];
|
||||
return playlist.map((playlist) => ({
|
||||
key: playlist.id,
|
||||
label: () =>
|
||||
|
||||
@@ -363,7 +363,6 @@ const getListData = async (id: number): Promise<SongType[]> => {
|
||||
&:hover {
|
||||
background-color: rgba(var(--primary), 0.12);
|
||||
.cover {
|
||||
border-radius: 16px 16px 0 0;
|
||||
.cover-img {
|
||||
transform: scale(1.1);
|
||||
filter: brightness(0.8);
|
||||
|
||||
@@ -281,9 +281,11 @@ onBeforeUnmount(() => {
|
||||
<style lang="scss" scoped>
|
||||
.song-list {
|
||||
height: 100%;
|
||||
border-radius: 12px 0 0 12px;
|
||||
overflow: hidden;
|
||||
.song-card {
|
||||
padding-bottom: 12px;
|
||||
padding-right: 4px;
|
||||
// padding-right: 4px;
|
||||
}
|
||||
// 悬浮顶栏
|
||||
.list-header {
|
||||
@@ -293,8 +295,8 @@ onBeforeUnmount(() => {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 18px 8px 12px;
|
||||
margin-right: 4px;
|
||||
padding: 8px 12px;
|
||||
// margin-right: 4px;
|
||||
border: 1px solid transparent;
|
||||
background-color: var(--background-hex);
|
||||
.n-text {
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
openPlaylistAdd,
|
||||
openSongInfoEditor,
|
||||
} from "@/utils/modal";
|
||||
import { deleteSongs } from "@/utils/auth";
|
||||
import { deleteSongs, isLogin } from "@/utils/auth";
|
||||
import { songUrl } from "@/api/song";
|
||||
import player from "@/utils/player";
|
||||
|
||||
@@ -64,6 +64,7 @@ const openDropdown = (
|
||||
const isHasMv = !!song?.mv && song.mv !== 0;
|
||||
const isCloud = router.currentRoute.value.name === "cloud";
|
||||
const isLocal = !!song?.path;
|
||||
const isLoginNormal = isLogin() === 1;
|
||||
// 是否当前播放
|
||||
const isCurrent = statusStore.playIndex === index;
|
||||
// 是否为用户歌单
|
||||
@@ -169,7 +170,7 @@ const openDropdown = (
|
||||
{
|
||||
key: "cloud-import",
|
||||
label: "导入至云盘",
|
||||
show: !isCloud && type === "song" && !isLocal,
|
||||
show: !isCloud && isLoginNormal && type === "song" && !isLocal,
|
||||
props: {
|
||||
onClick: () => importSongToCloud(song),
|
||||
},
|
||||
@@ -178,7 +179,7 @@ const openDropdown = (
|
||||
{
|
||||
key: "delete",
|
||||
label: "从歌单中删除",
|
||||
show: isUserPlaylist && !isCloud,
|
||||
show: isUserPlaylist && isLoginNormal && !isCloud,
|
||||
props: {
|
||||
onClick: () => deleteSongs(playListId!, [song.id], () => emit("removeSong", [song.id])),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="create-playlist">
|
||||
<n-tabs v-model:value="playlistType" type="segment" animated>
|
||||
<n-tab-pane name="online" tab="在线歌单">
|
||||
<n-tab-pane :disabled="isLogin() !== 1" name="online" tab="在线歌单">
|
||||
<n-form ref="onlineFormRef" :model="onlineFormData" :rules="onlineFormRules">
|
||||
<n-form-item label="歌单名称" path="name">
|
||||
<n-input v-model:value="onlineFormData.name" placeholder="请输入歌单名称" />
|
||||
@@ -28,7 +28,7 @@ import { useDataStore } from "@/stores";
|
||||
import { textRule } from "@/utils/rules";
|
||||
import { debounce } from "lodash-es";
|
||||
import { createPlaylist } from "@/api/playlist";
|
||||
import { updateUserLikePlaylist } from "@/utils/auth";
|
||||
import { isLogin, updateUserLikePlaylist } from "@/utils/auth";
|
||||
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
|
||||
@@ -42,7 +42,7 @@ interface OnlineFormType {
|
||||
const dataStore = useDataStore();
|
||||
|
||||
// 歌单类别
|
||||
const playlistType = ref<"online" | "local">("online");
|
||||
const playlistType = ref<"online" | "local">(isLogin() === 1 ? "online" : "local");
|
||||
|
||||
// 在线歌单数据
|
||||
const onlineFormRef = ref<FormInst | null>(null);
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</n-flex>
|
||||
</n-radio-group>
|
||||
</n-collapse-item>
|
||||
<n-collapse-item v-if="isElectron" title="下载路径" name="path">
|
||||
<n-collapse-item v-if="isElectron" title="本次下载路径" name="path">
|
||||
<n-input-group>
|
||||
<n-input :value="downloadPath || '未配置下载目录'" disabled>
|
||||
<template #prefix>
|
||||
@@ -137,7 +137,7 @@ const changeDownloadPath = async () => {
|
||||
const download = async () => {
|
||||
if (!songData.value) return;
|
||||
loading.value = true;
|
||||
downloadPath.value = settingStore.downloadPath;
|
||||
if (settingStore.downloadPath) downloadPath.value = settingStore.downloadPath;
|
||||
try {
|
||||
// 获取下载链接
|
||||
const result = await songDownloadUrl(props.id, songLevelChoosed.value);
|
||||
@@ -184,7 +184,7 @@ const electronDownload = async (url: string, songName: string, fileType: string)
|
||||
}
|
||||
// 下载歌曲
|
||||
const config = {
|
||||
fileName: songName,
|
||||
fileName: songName.replace(/[/:*?"<>|]/g, "&"),
|
||||
fileType,
|
||||
path: downloadPath.value,
|
||||
downloadMeta,
|
||||
|
||||
20
src/components/Modal/ExcludeKeywords.vue
Normal file
20
src/components/Modal/ExcludeKeywords.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="exclude">
|
||||
<n-alert :show-icon="false">请勿添加过多,以免影响歌词的正常显示</n-alert>
|
||||
<n-dynamic-tags v-model:value="settingStore.excludeKeywords" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from "@/stores";
|
||||
|
||||
const settingStore = useSettingStore();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.exclude {
|
||||
.n-alert {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
</template>
|
||||
可在官方的
|
||||
<n-a href="https://music.163.com/" target="_blank">网页端</n-a>
|
||||
和客户端的控制台中获取,只需要 Cookie 中的 <code>MUSIC_U</code> 字段即可,例如:
|
||||
或点击下方的自动获取,只需要 Cookie 中的 <code>MUSIC_U</code> 字段即可,例如:
|
||||
<code>MUSIC_U=00C7...;</code><br />请注意:必须以 <code>;</code> 结束
|
||||
</n-alert>
|
||||
<n-input
|
||||
@@ -15,12 +15,16 @@
|
||||
type="textarea"
|
||||
placeholder="请输入 Cookie"
|
||||
/>
|
||||
<n-flex class="menu">
|
||||
<n-button v-if="isElectron" type="primary" @click="openWeb">自动获取</n-button>
|
||||
<n-button type="primary" @click="login">登录</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LoginType } from "@/types/main";
|
||||
import { isElectron } from "@/utils/helper";
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
@@ -29,12 +33,32 @@ const emit = defineEmits<{
|
||||
|
||||
const cookie = ref<string>();
|
||||
|
||||
// 开启窗口
|
||||
const openWeb = () => {
|
||||
window.$dialog.info({
|
||||
title: "使用前告知",
|
||||
content:
|
||||
"请知悉,该功能仍旧无法确保账号的安全性!请自行决定是否使用!如遇打开窗口后页面出现白屏或者无法点击等情况,请关闭后重试",
|
||||
positiveText: "我已了解",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => window.electron.ipcRenderer.send("open-login-web"),
|
||||
});
|
||||
};
|
||||
|
||||
// Cookie 登录
|
||||
const login = async () => {
|
||||
if (!cookie.value) {
|
||||
window.$message.warning("请输入 Cookie");
|
||||
return;
|
||||
}
|
||||
cookie.value = cookie.value.trim();
|
||||
console.log(cookie.value.endsWith(";"));
|
||||
|
||||
// 是否为有效 Cookie
|
||||
if (!cookie.value.includes("MUSIC_U") || !cookie.value.endsWith(";")) {
|
||||
window.$message.warning("请输入有效的 Cookie");
|
||||
return;
|
||||
}
|
||||
// 写入 Cookie
|
||||
try {
|
||||
window.$message.success("登录成功");
|
||||
@@ -53,6 +77,16 @@ const login = async () => {
|
||||
console.error("Cookie 登录出错:", error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.on("send-cookies", (_, value) => {
|
||||
if (!value) return;
|
||||
cookie.value = value;
|
||||
login();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -73,5 +107,13 @@ const login = async () => {
|
||||
margin: 4px 0;
|
||||
font-family: auto;
|
||||
}
|
||||
.menu {
|
||||
margin-top: 20px;
|
||||
.n-button {
|
||||
width: auto;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -57,7 +57,7 @@ import { useDataStore } from "@/stores";
|
||||
import { coverLoaded } from "@/utils/helper";
|
||||
import { playlistTracks } from "@/api/playlist";
|
||||
import { debounce } from "lodash-es";
|
||||
import { updateUserLikePlaylist, updateUserLikeSongs } from "@/utils/auth";
|
||||
import { isLogin, updateUserLikePlaylist, updateUserLikeSongs } from "@/utils/auth";
|
||||
import { openCreatePlaylist } from "@/utils/modal";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -86,6 +86,10 @@ const onlinePlaylists = computed(() => {
|
||||
// 添加到歌单
|
||||
const addPlaylist = debounce(
|
||||
async (id: number, index: number) => {
|
||||
if (isLogin() === 2) {
|
||||
window.$message.warning("该登录模式暂不支持该操作");
|
||||
return;
|
||||
}
|
||||
loadingMsg.value = window.$message.loading("正在添加歌曲至歌单", { duration: 0 });
|
||||
const ids = props.data.map((item) => item.id).filter((item) => item !== 0);
|
||||
const result = await playlistTracks(id, ids);
|
||||
|
||||
@@ -200,8 +200,8 @@ const getSongInfo = async () => {
|
||||
name: common.title || "",
|
||||
artist: common.artist || "",
|
||||
album: common.album || "",
|
||||
alia: common.comment?.[0] || "",
|
||||
lyric: common.lyrics?.[0] || "",
|
||||
alia: (common.comment?.[0] as string) || "",
|
||||
lyric: (common.lyrics?.[0] as unknown as string) || "",
|
||||
type: format.codec,
|
||||
duration: format.duration ? Number(format.duration.toFixed(2)) : 0,
|
||||
size: fileSize,
|
||||
@@ -212,7 +212,7 @@ const getSongInfo = async () => {
|
||||
// 获取封面
|
||||
const coverBuff = common.picture?.[0]?.data || "";
|
||||
const coverType = common.picture?.[0]?.format || "";
|
||||
if (coverBuff) coverData.value = blob.createBlobURL(coverBuff, coverType, path);
|
||||
if (coverBuff) coverData.value = blob.createBlobURL(coverBuff as Buffer, coverType, path);
|
||||
};
|
||||
|
||||
// 在线匹配
|
||||
@@ -262,7 +262,7 @@ const onlineMatch = debounce(
|
||||
const changeCover = async () => {
|
||||
const newPath = await window.electron.ipcRenderer.invoke("choose-image");
|
||||
if (!newPath) return;
|
||||
coverData.value = newPath;
|
||||
coverData.value = `file://${newPath}`;
|
||||
};
|
||||
|
||||
// 实时修改列表
|
||||
@@ -300,6 +300,8 @@ const saveSongInfo = debounce(async (song: SongType) => {
|
||||
cover:
|
||||
coverData.value.startsWith("blob:") || coverData.value === "/images/song.jpg?assest"
|
||||
? null
|
||||
: coverData.value.startsWith("file://")
|
||||
? coverData.value.replace(/^file:\/\//, "")
|
||||
: coverData.value,
|
||||
};
|
||||
console.log(song.path, metadata);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
v-show="statusStore.showFullPlayer"
|
||||
:style="{
|
||||
'--main-color': mainColor,
|
||||
'--main-color': statusStore.mainColor,
|
||||
cursor: statusStore.playerMetaShow ? 'auto' : 'none',
|
||||
}"
|
||||
class="full-player"
|
||||
@@ -66,16 +66,7 @@
|
||||
<!-- 封面 -->
|
||||
<PlayerCover />
|
||||
<!-- 数据 -->
|
||||
<PlayerData
|
||||
v-if="settingStore.playerType === 'cover' || !musicStore.isHasLrc || isShowComment"
|
||||
:center="
|
||||
statusStore.pureLyricMode ||
|
||||
musicStore.playSong.type === 'radio' ||
|
||||
!musicStore.isHasLrc ||
|
||||
isShowComment
|
||||
"
|
||||
:theme="mainColor"
|
||||
/>
|
||||
<PlayerData :center="playerDataCenter" :theme="statusStore.mainColor" />
|
||||
</div>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<!-- 评论 -->
|
||||
@@ -83,14 +74,14 @@
|
||||
<!-- 歌词 -->
|
||||
<div v-else-if="musicStore.isHasLrc" class="content-right">
|
||||
<!-- 数据 -->
|
||||
<PlayerData
|
||||
<!-- <PlayerData
|
||||
v-if="
|
||||
(statusStore.pureLyricMode && musicStore.isHasLrc) ||
|
||||
(settingStore.playerType === 'record' && musicStore.isHasLrc)
|
||||
"
|
||||
:center="statusStore.pureLyricMode"
|
||||
:theme="mainColor"
|
||||
/>
|
||||
/> -->
|
||||
<!-- 歌词 -->
|
||||
<MainAMLyric v-if="settingStore.useAMLyrics" />
|
||||
<MainLyric v-else />
|
||||
@@ -103,7 +94,7 @@
|
||||
<!-- 音乐频谱 -->
|
||||
<PlayerSpectrum
|
||||
v-if="settingStore.showSpectrums"
|
||||
:color="mainColor ? `rgb(${mainColor})` : 'rgb(239 239 239)'"
|
||||
:color="statusStore.mainColor ? `rgb(${statusStore.mainColor})` : 'rgb(239 239 239)'"
|
||||
:show="!statusStore.playerMetaShow"
|
||||
:height="60"
|
||||
/>
|
||||
@@ -120,6 +111,11 @@ const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
// 是否显示评论
|
||||
const isShowComment = computed<boolean>(
|
||||
() => !musicStore.playSong.path && statusStore.showPlayerComment,
|
||||
);
|
||||
|
||||
// 主内容 key
|
||||
const playerContentKey = computed(() => {
|
||||
return `
|
||||
@@ -129,8 +125,15 @@ const playerContentKey = computed(() => {
|
||||
${isShowComment.value}`;
|
||||
});
|
||||
|
||||
// 是否显示评论
|
||||
const isShowComment = computed(() => !musicStore.playSong.path && statusStore.showPlayerComment);
|
||||
// 数据是否居中
|
||||
const playerDataCenter = computed<boolean>(
|
||||
() =>
|
||||
!musicStore.isHasLrc ||
|
||||
statusStore.pureLyricMode ||
|
||||
settingStore.playerType === "record" ||
|
||||
musicStore.playSong.type === "radio" ||
|
||||
isShowComment.value,
|
||||
);
|
||||
|
||||
// 当前实时歌词
|
||||
const instantLyrics = computed(() => {
|
||||
@@ -138,14 +141,7 @@ const instantLyrics = computed(() => {
|
||||
const content = isYrc
|
||||
? musicStore.songLyric.yrcData[statusStore.lyricIndex]
|
||||
: musicStore.songLyric.lrcData[statusStore.lyricIndex];
|
||||
return { content: content?.content, tran: content?.tran };
|
||||
});
|
||||
|
||||
// 播放器主色
|
||||
const mainColor = computed(() => {
|
||||
const mainColor = statusStore.songCoverTheme?.main;
|
||||
if (!mainColor) return "239, 239, 239";
|
||||
return `${mainColor.r}, ${mainColor.g}, ${mainColor.b}`;
|
||||
return { content: content?.content, tran: settingStore.showTran && content?.tran };
|
||||
});
|
||||
|
||||
// 隐藏播放元素
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
<template>
|
||||
<Transition>
|
||||
<div
|
||||
:key="amLyricsData?.[0]?.startTime"
|
||||
:class="['lyric-am', { pure: statusStore.pureLyricMode }]"
|
||||
>
|
||||
<LyricPlayer
|
||||
ref="lyricPlayerRef"
|
||||
:lyricLines="amLyricsData"
|
||||
:currentTime="playSeek"
|
||||
:playing="statusStore.playStatus"
|
||||
:enableSpring="settingStore.useAMSpring"
|
||||
<div :key="amLyricsData?.[0]?.startTime" :class="['lyric-am', { pure: statusStore.pureLyricMode }]">
|
||||
<LyricPlayer ref="lyricPlayerRef" :lyricLines="amLyricsData" :currentTime="playSeek"
|
||||
:playing="statusStore.playStatus" :enableSpring="settingStore.useAMSpring"
|
||||
:enableScale="settingStore.useAMSpring"
|
||||
:alignPosition="settingStore.lyricsScrollPosition === 'center' ? 0.5 : 0.2"
|
||||
:enableBlur="settingStore.lyricsBlur"
|
||||
:style="{
|
||||
:enableBlur="settingStore.lyricsBlur" :style="{
|
||||
'--amll-lyric-view-color': mainColor,
|
||||
'--amll-lyric-player-font-size': settingStore.lyricFontSize + 'px',
|
||||
'--ja-font-family': settingStore.japaneseLyricFont !== 'follow' ? settingStore.japaneseLyricFont : '',
|
||||
'font-weight': settingStore.lyricFontBold ? 'bold' : 'normal',
|
||||
'font-family': settingStore.LyricFont !== 'follow' ? settingStore.LyricFont : '',
|
||||
}"
|
||||
class="am-lyric"
|
||||
@line-click="jumpSeek"
|
||||
/>
|
||||
}" class="am-lyric" @line-click="jumpSeek" />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
@@ -31,7 +21,9 @@ import { LyricPlayer } from "@applemusic-like-lyrics/vue";
|
||||
import { LyricLine } from "@applemusic-like-lyrics/core";
|
||||
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
import { msToS } from "@/utils/time";
|
||||
import { getLyricLanguage } from "@/utils/lyric";
|
||||
import player from "@/utils/player";
|
||||
import { watch } from "vue";
|
||||
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
@@ -50,15 +42,43 @@ const { pause: pauseSeek, resume: resumeSeek } = useRafFn(() => {
|
||||
|
||||
// 歌词主色
|
||||
const mainColor = computed(() => {
|
||||
const mainColor = statusStore.songCoverTheme?.main;
|
||||
if (!mainColor) return "rgb(239, 239, 239)";
|
||||
return `rgb(${mainColor.r}, ${mainColor.g}, ${mainColor.b})`;
|
||||
if (!statusStore.mainColor) return "rgb(239, 239, 239)";
|
||||
return `rgb(${statusStore.mainColor})`;
|
||||
});
|
||||
|
||||
// 检查是否为纯音乐歌词
|
||||
const isPureInstrumental = (lyrics: LyricLine[]): boolean => {
|
||||
if (!lyrics || lyrics.length === 0) return false;
|
||||
const instrumentalKeywords = ['纯音乐', 'instrumental', '请欣赏'];
|
||||
|
||||
if (lyrics.length === 1) {
|
||||
const content = lyrics[0].words?.[0]?.word || '';
|
||||
return instrumentalKeywords.some(keyword => content.toLowerCase().includes(keyword.toLowerCase()));
|
||||
}
|
||||
|
||||
if (lyrics.length <= 3) {
|
||||
const allContent = lyrics.map(line => line.words?.[0]?.word || '').join('');
|
||||
return instrumentalKeywords.some(keyword => allContent.toLowerCase().includes(keyword.toLowerCase()));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 当前歌词
|
||||
const amLyricsData = computed<LyricLine[]>(() => {
|
||||
const isYrc = musicStore.songLyric.yrcData?.length && settingStore.showYrc;
|
||||
return isYrc ? musicStore.songLyric.yrcAMData : musicStore.songLyric.lrcAMData;
|
||||
const { songLyric } = musicStore;
|
||||
if (!songLyric) return [];
|
||||
|
||||
// 优先使用逐字歌词(YRC/TTML)
|
||||
const useYrc = songLyric.yrcAMData?.length && settingStore.showYrc;
|
||||
const lyrics = useYrc ? songLyric.yrcAMData : songLyric.lrcAMData;
|
||||
|
||||
// 简单检查歌词有效性
|
||||
if (!Array.isArray(lyrics) || lyrics.length === 0) return [];
|
||||
|
||||
// 检查是否为纯音乐
|
||||
if (isPureInstrumental(lyrics)) return [];
|
||||
|
||||
return lyrics;
|
||||
});
|
||||
|
||||
// 进度跳转
|
||||
@@ -69,9 +89,30 @@ const jumpSeek = (line: any) => {
|
||||
player.play();
|
||||
};
|
||||
|
||||
// 处理歌词语言
|
||||
const processLyricLanguage = () => {
|
||||
const lyricLinesEl = lyricPlayerRef.value?.lyricPlayer?.lyricLinesEl ?? [];
|
||||
// 遍历歌词行
|
||||
for (let e of lyricLinesEl) {
|
||||
// 获取歌词行内容 (合并逐字歌词为一句)
|
||||
const content = e.lyricLine.words.map((word: any) => word.word).join("");
|
||||
// 获取歌词语言
|
||||
const lang = getLyricLanguage(content);
|
||||
// 为主歌词设置 lang 属性 (firstChild 获取主歌词 不为翻译和音译设置属性)
|
||||
e.element.firstChild.setAttribute("lang", lang);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换歌曲时处理歌词语言
|
||||
watch(amLyricsData, () => {
|
||||
nextTick(() => processLyricLanguage());
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 恢复进度
|
||||
resumeSeek();
|
||||
// 处理歌词语言
|
||||
nextTick(() => processLyricLanguage());
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -86,15 +127,14 @@ onBeforeUnmount(() => {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
filter: drop-shadow(0px 4px 6px rgba(0, 0, 0, 0.2));
|
||||
mask: linear-gradient(
|
||||
180deg,
|
||||
mask: linear-gradient(180deg,
|
||||
hsla(0, 0%, 100%, 0) 0,
|
||||
hsla(0, 0%, 100%, 0.6) 5%,
|
||||
#fff 10%,
|
||||
#fff 75%,
|
||||
hsla(0, 0%, 100%, 0.6) 85%,
|
||||
hsla(0, 0%, 100%, 0)
|
||||
);
|
||||
hsla(0, 0%, 100%, 0));
|
||||
|
||||
:deep(.am-lyric) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -105,15 +145,22 @@ onBeforeUnmount(() => {
|
||||
padding-right: 80px;
|
||||
margin-left: -2rem;
|
||||
}
|
||||
|
||||
&.pure {
|
||||
text-align: center;
|
||||
|
||||
:deep(.am-lyric) {
|
||||
margin: 0;
|
||||
padding: 0 80px;
|
||||
|
||||
div {
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:lang(ja) {
|
||||
font-family: var(--ja-font-family);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
'--lrc-tran-size': settingStore.lyricTranFontSize + 'px',
|
||||
'--lrc-roma-size': settingStore.lyricRomaFontSize + 'px',
|
||||
'--lrc-bold': settingStore.lyricFontBold ? 'bold' : 'normal',
|
||||
'--ja-font-family':
|
||||
settingStore.japaneseLyricFont !== 'follow' ? settingStore.japaneseLyricFont : '',
|
||||
'font-family': settingStore.LyricFont !== 'follow' ? settingStore.LyricFont : '',
|
||||
cursor: statusStore.playerMetaShow ? 'auto' : 'none',
|
||||
}"
|
||||
@@ -41,7 +43,19 @@
|
||||
v-for="(item, index) in musicStore.songLyric.yrcData"
|
||||
:key="index"
|
||||
:id="`lrc-${index}`"
|
||||
:class="['lrc-line', 'is-yrc', { on: statusStore.lyricIndex === index }]"
|
||||
:class="[
|
||||
'lrc-line',
|
||||
'is-yrc',
|
||||
{
|
||||
// on: statusStore.lyricIndex === index,
|
||||
// 当播放时间大于等于当前歌词的开始时间
|
||||
on:
|
||||
(playSeek >= item.time && playSeek < item.endTime) ||
|
||||
statusStore.lyricIndex === index,
|
||||
'is-bg': item.isBG,
|
||||
'is-duet': item.isDuet,
|
||||
},
|
||||
]"
|
||||
:style="{
|
||||
filter: settingStore.lyricsBlur
|
||||
? `blur(${Math.min(Math.abs(statusStore.lyricIndex - index) * 1.8, 10)}px)`
|
||||
@@ -60,16 +74,26 @@
|
||||
'end-with-space': text.endsWithSpace,
|
||||
}"
|
||||
>
|
||||
<span class="word">{{ text.content }}</span>
|
||||
<span class="filler" :style="getYrcStyle(text, index)">
|
||||
<span class="word" :lang="getLyricLanguage(text.content)">
|
||||
{{ text.content }}
|
||||
</span>
|
||||
<span
|
||||
class="filler"
|
||||
:style="getYrcStyle(text, index)"
|
||||
:lang="getLyricLanguage(text.content)"
|
||||
>
|
||||
{{ text.content }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 翻译 -->
|
||||
<span v-if="item.tran" class="tran">{{ item.tran }}</span>
|
||||
<span v-if="item.tran && settingStore.showTran" class="tran" lang="en">
|
||||
{{ item.tran }}
|
||||
</span>
|
||||
<!-- 音译 -->
|
||||
<span v-if="item.roma" class="roma">{{ item.roma }}</span>
|
||||
<span v-if="item.roma && settingStore.showRoma" class="roma" lang="en">
|
||||
{{ item.roma }}
|
||||
</span>
|
||||
<!-- 倒计时 -->
|
||||
<div
|
||||
v-if="
|
||||
@@ -113,11 +137,15 @@
|
||||
@click="jumpSeek(item.time)"
|
||||
>
|
||||
<!-- 歌词 -->
|
||||
<span class="content">{{ item.content }}</span>
|
||||
<span class="content" :lang="getLyricLanguage(item.content)">{{ item.content }}</span>
|
||||
<!-- 翻译 -->
|
||||
<span v-if="item.tran" class="tran">{{ item.tran }}</span>
|
||||
<span v-if="item.tran && settingStore.showTran" class="tran" lang="en">
|
||||
{{ item.tran }}
|
||||
</span>
|
||||
<!-- 音译 -->
|
||||
<span v-if="item.roma" class="roma">{{ item.roma }}</span>
|
||||
<span v-if="item.roma && settingStore.showRoma" class="roma" lang="en">
|
||||
{{ item.roma }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="placeholder" />
|
||||
</template>
|
||||
@@ -151,6 +179,7 @@ import { NScrollbar } from "naive-ui";
|
||||
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
import { openSetting } from "@/utils/modal";
|
||||
import player from "@/utils/player";
|
||||
import { getLyricLanguage } from "@/utils/lyric";
|
||||
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
@@ -200,12 +229,12 @@ const lyricsScroll = (index: number) => {
|
||||
const getYrcStyle = (wordData: LyricContentType, lyricIndex: number) => {
|
||||
if (settingStore.showYrcAnimation) {
|
||||
// 如果当前歌词索引与播放歌曲的歌词索引不匹配
|
||||
if (statusStore.lyricIndex !== lyricIndex) {
|
||||
return {
|
||||
transitionDuration: `0ms, 0ms, 0.35s`,
|
||||
transitionDelay: `0ms`,
|
||||
};
|
||||
}
|
||||
// if (statusStore.lyricIndex !== lyricIndex) {
|
||||
// return {
|
||||
// transitionDuration: `0ms, 0ms, 0.35s`,
|
||||
// transitionDelay: `0ms`,
|
||||
// };
|
||||
// }
|
||||
// 如果播放状态不是加载中,且当前单词的时间加上持续时间减去播放进度大于 0
|
||||
if (
|
||||
statusStore.playLoading === false &&
|
||||
@@ -217,11 +246,6 @@ const getYrcStyle = (wordData: LyricContentType, lyricIndex: number) => {
|
||||
WebkitMaskPositionX: `${
|
||||
100 - Math.max(((playSeek.value - wordData.time) / wordData.duration) * 100, 0)
|
||||
}%`,
|
||||
// 最大上移2px
|
||||
// transform: `translateY(-${Math.min(
|
||||
// ((playSeek.value - wordData.time) / wordData.duration) * 2,
|
||||
// 2,
|
||||
// )}px)`,
|
||||
};
|
||||
}
|
||||
// 如果以上条件都不满足
|
||||
@@ -230,7 +254,6 @@ const getYrcStyle = (wordData: LyricContentType, lyricIndex: number) => {
|
||||
transitionDelay: `${wordData.time - playSeek.value}ms, ${
|
||||
wordData.time - playSeek.value + wordData.duration * 0.5
|
||||
}ms, 0ms`,
|
||||
// transform: "translateY(-2px)",
|
||||
};
|
||||
} else {
|
||||
// 如果当前歌词索引与播放歌曲的歌词索引不匹配,或者播放状态不是加载中且当前单词的时间大于等于播放进度
|
||||
@@ -299,6 +322,8 @@ onBeforeUnmount(() => {
|
||||
:deep(.n-scrollbar-content) {
|
||||
padding-left: 10px;
|
||||
padding-right: 80px;
|
||||
max-width: 100%; /* 新增:防止宽度溢出 */
|
||||
box-sizing: border-box; /* 新增:确保 padding 不影响宽度 */
|
||||
}
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
@@ -315,6 +340,7 @@ onBeforeUnmount(() => {
|
||||
.lyric-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box; /* 新增:确保宽度计算正确 */
|
||||
}
|
||||
.lrc-line {
|
||||
position: relative;
|
||||
@@ -324,21 +350,29 @@ onBeforeUnmount(() => {
|
||||
padding: 10px 16px;
|
||||
transform: scale(0.86);
|
||||
transform-origin: left center;
|
||||
will-change: filter, opacity, transform;
|
||||
transition:
|
||||
filter 0.35s,
|
||||
opacity 0.35s,
|
||||
transform 0.5s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
cursor: pointer;
|
||||
// 歌词
|
||||
width: 100%;
|
||||
box-sizing: border-box; /* 新增:确保 padding 不影响宽度 */
|
||||
.content {
|
||||
display: block;
|
||||
font-size: var(--lrc-size);
|
||||
font-weight: var(--lrc-bold);
|
||||
word-wrap: break-word;
|
||||
// 逐字歌词
|
||||
width: 100%;
|
||||
overflow-wrap: anywhere; /* 支持超长单词换行 */
|
||||
word-break: break-word; /* 优先空格或连字符换行,超长单词强制换行 */
|
||||
white-space: normal; /* 新增:明确文本换行行为 */
|
||||
hyphens: auto; /* 英文自动连字符 */
|
||||
.content-text {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow-wrap: anywhere; /* 新增:逐字歌词单词支持换行 */
|
||||
word-break: break-word; /* 新增:单词内换行 */
|
||||
white-space: normal; /* 新增:确保逐字歌词换行 */
|
||||
.word {
|
||||
opacity: 0.3;
|
||||
display: inline-block;
|
||||
@@ -384,22 +418,32 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
&:lang(ja) {
|
||||
font-family: var(--ja-font-family);
|
||||
}
|
||||
}
|
||||
// 翻译
|
||||
.tran {
|
||||
margin-top: 8px;
|
||||
opacity: 0.6;
|
||||
font-size: var(--lrc-tran-size);
|
||||
transition: opacity 0.35s;
|
||||
width: 100%;
|
||||
overflow-wrap: anywhere; /* 支持超长单词换行 */
|
||||
word-break: break-word; /* 优先空格或连字符换行,超长单词强制换行 */
|
||||
white-space: normal; /* 新增:明确文本换行行为 */
|
||||
hyphens: auto; /* 英文自动连字符 */
|
||||
}
|
||||
// 音译
|
||||
.roma {
|
||||
margin-top: 4px;
|
||||
opacity: 0.5;
|
||||
font-size: var(--lrc-roma-size);
|
||||
transition: opacity 0.35s;
|
||||
width: 100%;
|
||||
overflow-wrap: anywhere; /* 支持超长单词换行 */
|
||||
word-break: break-word; /* 优先空格或连字符换行,超长单词强制换行 */
|
||||
white-space: normal; /* 新增:明确文本换行行为 */
|
||||
hyphens: auto; /* 英文自动连字符 */
|
||||
}
|
||||
// 倒计时
|
||||
.count-down-content {
|
||||
height: 50px;
|
||||
margin-top: 40px;
|
||||
@@ -415,16 +459,33 @@ onBeforeUnmount(() => {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
overflow-wrap: anywhere; /* 逐字歌词支持超长单词换行 */
|
||||
word-break: break-word; /* 优先空格或连字符换行 */
|
||||
white-space: normal; /* 确保换行行为 */
|
||||
}
|
||||
.tran,
|
||||
.roma {
|
||||
opacity: 0.3;
|
||||
}
|
||||
&.is-bg {
|
||||
opacity: 0.4;
|
||||
transform: scale(0.5);
|
||||
padding: 0px 32px;
|
||||
}
|
||||
&.is-duet {
|
||||
transform-origin: right;
|
||||
.content,
|
||||
.tran,
|
||||
.roma {
|
||||
text-align: right;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.on {
|
||||
opacity: 1;
|
||||
opacity: 1 !important;
|
||||
transform: scale(1);
|
||||
// 逐字歌词
|
||||
.content-text {
|
||||
.filler {
|
||||
opacity: 1;
|
||||
@@ -437,6 +498,9 @@ onBeforeUnmount(() => {
|
||||
.roma {
|
||||
opacity: 0.6;
|
||||
}
|
||||
&.is-bg {
|
||||
opacity: 0.6 !important;
|
||||
}
|
||||
}
|
||||
&::before {
|
||||
content: "";
|
||||
@@ -541,7 +605,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.lrc-line {
|
||||
transform-origin: right;
|
||||
align-items: flex-end;
|
||||
.content {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -561,7 +624,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.lrc-line {
|
||||
transform-origin: center !important;
|
||||
align-items: center !important;
|
||||
.content {
|
||||
text-align: center !important;
|
||||
}
|
||||
@@ -571,8 +633,12 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
&.record,
|
||||
&.pure {
|
||||
:deep(.n-scrollbar-content) {
|
||||
padding: 0 80px;
|
||||
max-width: 100%; /* 新增:防止宽度溢出 */
|
||||
box-sizing: border-box; /* 新增:确保 padding 不影响宽度 */
|
||||
}
|
||||
.lyric-content {
|
||||
.placeholder {
|
||||
&:first-child {
|
||||
@@ -588,11 +654,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
&.pure {
|
||||
:deep(.n-scrollbar-content) {
|
||||
padding: 0 80px;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.lrc-line {
|
||||
filter: blur(0) !important;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<n-drawer
|
||||
v-model:show="statusStore.playListShow"
|
||||
:class="{ 'full-player': statusStore.showFullPlayer }"
|
||||
:style="{ '--main-color': mainColor }"
|
||||
:style="{ '--main-color': statusStore.mainColor }"
|
||||
:auto-focus="false"
|
||||
id="main-playlist"
|
||||
style="width: 400px"
|
||||
@@ -124,13 +124,6 @@ const statusStore = useStatusStore();
|
||||
|
||||
const playListRef = ref<VirtualListInst | null>(null);
|
||||
|
||||
// 列表主色
|
||||
const mainColor = computed(() => {
|
||||
const mainColor = statusStore.songCoverTheme?.main;
|
||||
if (!mainColor) return "239, 239, 239";
|
||||
return `${mainColor.r}, ${mainColor.g}, ${mainColor.b}`;
|
||||
});
|
||||
|
||||
// 播放列表数据
|
||||
const playListData = computed(() => {
|
||||
return dataStore.playList.map((item, index) => {
|
||||
@@ -290,6 +283,9 @@ const scrollToItem = (index: number, behavior: "smooth" | "auto" = "smooth") =>
|
||||
&.on {
|
||||
border-color: rgb(var(--main-color));
|
||||
}
|
||||
&:hover {
|
||||
border-color: rgb(var(--main-color));
|
||||
}
|
||||
.num {
|
||||
color: rgba(var(--main-color), 0.52);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
:max="100"
|
||||
:tooltip="false"
|
||||
:keyboard="false"
|
||||
:marks="
|
||||
statusStore.chorus && statusStore.progress <= statusStore.chorus
|
||||
? { [statusStore.chorus]: '' }
|
||||
: undefined
|
||||
"
|
||||
class="player-slider"
|
||||
@dragstart="player.pause(false)"
|
||||
@dragend="sliderDragend"
|
||||
@@ -168,17 +173,17 @@
|
||||
@select="(mode) => player.togglePlayMode(mode)"
|
||||
>
|
||||
<div class="menu-icon" @click.stop="player.togglePlayMode(false)">
|
||||
<SvgIcon :name="playModeIcon" />
|
||||
<SvgIcon :name="statusStore.playModeIcon" />
|
||||
</div>
|
||||
</n-dropdown>
|
||||
<!-- 音量调节 -->
|
||||
<n-popover :show-arrow="false" :style="{ padding: 0 }">
|
||||
<template #trigger>
|
||||
<div class="menu-icon" @click.stop="player.toggleMute" @wheel="changeVolume">
|
||||
<SvgIcon :name="playVolumeIcon" />
|
||||
<div class="menu-icon" @click.stop="player.toggleMute" @wheel="player.setVolume">
|
||||
<SvgIcon :name="statusStore.playVolumeIcon" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="volume-change" @wheel="changeVolume">
|
||||
<div class="volume-change" @wheel="player.setVolume">
|
||||
<n-slider
|
||||
v-model:value="statusStore.playVolume"
|
||||
:tooltip="false"
|
||||
@@ -188,7 +193,7 @@
|
||||
vertical
|
||||
@update:value="(val) => player.setVolume(val)"
|
||||
/>
|
||||
<n-text class="slider-num">{{ playVolumePercentage }}%</n-text>
|
||||
<n-text class="slider-num">{{ statusStore.playVolumePercent }}%</n-text>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 播放列表 -->
|
||||
@@ -214,7 +219,7 @@
|
||||
import type { DropdownOption } from "naive-ui";
|
||||
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||
import { secondsToTime, calculateCurrentTime } from "@/utils/time";
|
||||
import { renderIcon, isElectron } from "@/utils/helper";
|
||||
import { renderIcon, isElectron, coverLoaded } from "@/utils/helper";
|
||||
import { toLikeSong } from "@/utils/auth";
|
||||
import { openDownloadSong, openJumpArtist, openPlaylistAdd } from "@/utils/modal";
|
||||
import player from "@/utils/player";
|
||||
@@ -277,6 +282,20 @@ const songMoreOptions = computed<DropdownOption[]>(() => {
|
||||
props: { onClick: () => openDownloadSong(musicStore.playSong) },
|
||||
icon: renderIcon("Download"),
|
||||
},
|
||||
{
|
||||
key: "comment",
|
||||
label: "查看评论",
|
||||
show: !isLocal,
|
||||
props: {
|
||||
onClick: () => {
|
||||
statusStore.$patch({
|
||||
showFullPlayer: true,
|
||||
showPlayerComment: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
icon: renderIcon("Message"),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -289,43 +308,6 @@ const sliderDragend = () => {
|
||||
player.play();
|
||||
};
|
||||
|
||||
// 封面加载完成
|
||||
const coverLoaded = (e: Event) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target && target.nodeType === Node.ELEMENT_NODE) {
|
||||
target.style.opacity = "1";
|
||||
}
|
||||
};
|
||||
|
||||
// 当前音量百分比
|
||||
const playVolumePercentage = computed(() => {
|
||||
return Math.round(statusStore.playVolume * 100);
|
||||
});
|
||||
|
||||
// 当前音量图标
|
||||
const playVolumeIcon = computed(() => {
|
||||
const volume = statusStore.playVolume;
|
||||
return volume === 0
|
||||
? "VolumeOff"
|
||||
: volume < 0.4
|
||||
? "VolumeMute"
|
||||
: volume < 0.7
|
||||
? "VolumeDown"
|
||||
: "VolumeUp";
|
||||
});
|
||||
|
||||
// 当前播放模式图标
|
||||
const playModeIcon = computed(() => {
|
||||
const mode = statusStore.playSongMode;
|
||||
return statusStore.playHeartbeatMode
|
||||
? "HeartBit"
|
||||
: mode === "repeat"
|
||||
? "Repeat"
|
||||
: mode === "repeat-once"
|
||||
? "RepeatSong"
|
||||
: "Shuffle";
|
||||
});
|
||||
|
||||
// 是否展示歌词
|
||||
const isShowLyrics = computed(() => {
|
||||
const isHasLrc = musicStore.isHasLrc;
|
||||
@@ -344,14 +326,10 @@ const instantLyrics = computed(() => {
|
||||
const content = isYrc
|
||||
? musicStore.songLyric.yrcData[statusStore.lyricIndex]
|
||||
: musicStore.songLyric.lrcData[statusStore.lyricIndex];
|
||||
return content?.tran ? `${content?.content}( ${content?.tran} )` : content?.content;
|
||||
return content?.tran && settingStore.showTran
|
||||
? `${content?.content}( ${content?.tran} )`
|
||||
: content?.content;
|
||||
});
|
||||
|
||||
// 音量条鼠标滚动
|
||||
const changeVolume = (e: WheelEvent) => {
|
||||
const deltaY = e.deltaY;
|
||||
player.setVolume(deltaY > 0 ? "down" : "up");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -378,6 +356,7 @@ const changeVolume = (e: WheelEvent) => {
|
||||
height: 16px;
|
||||
top: -8px;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
--n-rail-height: 3px;
|
||||
--n-handle-size: 14px;
|
||||
}
|
||||
@@ -553,6 +532,8 @@ const changeVolume = (e: WheelEvent) => {
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
.n-text {
|
||||
color: var(--primary-hex);
|
||||
opacity: 0.8;
|
||||
&:nth-of-type(1) {
|
||||
&::after {
|
||||
content: "/";
|
||||
@@ -573,6 +554,7 @@ const changeVolume = (e: WheelEvent) => {
|
||||
cursor: pointer;
|
||||
.n-icon {
|
||||
font-size: 22px;
|
||||
color: var(--primary-hex);
|
||||
}
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
|
||||
@@ -195,6 +195,7 @@ onMounted(player.initPersonalFM);
|
||||
--n-width: 46px;
|
||||
--n-height: 46px;
|
||||
.n-icon {
|
||||
color: var(--primary-hex);
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
}
|
||||
@@ -205,6 +206,7 @@ onMounted(player.initPersonalFM);
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
color: var(--primary-hex);
|
||||
transition:
|
||||
background-color 0.3s,
|
||||
transform 0.3s;
|
||||
|
||||
@@ -129,6 +129,9 @@ onMounted(() => {
|
||||
:deep(.n-scrollbar-content) {
|
||||
padding-right: 60px;
|
||||
}
|
||||
:deep(.n-skeleton) {
|
||||
background-color: rgba(var(--main-color), 0.08);
|
||||
}
|
||||
.comment-list {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -94,8 +94,28 @@
|
||||
</div>
|
||||
<!-- 播放模式 -->
|
||||
<div class="menu-icon" @click.stop="player.togglePlayMode(false)">
|
||||
<SvgIcon :name="playModeIcon" />
|
||||
<SvgIcon :name="statusStore.playModeIcon" />
|
||||
</div>
|
||||
<!-- 音量调节 -->
|
||||
<n-popover :show-arrow="false" :style="{ '--main-color': statusStore.mainColor }" raw>
|
||||
<template #trigger>
|
||||
<div class="menu-icon" @click.stop="player.toggleMute" @wheel="player.setVolume">
|
||||
<SvgIcon :name="statusStore.playVolumeIcon" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="volume-change" @wheel="player.setVolume">
|
||||
<n-slider
|
||||
v-model:value="statusStore.playVolume"
|
||||
:tooltip="false"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
vertical
|
||||
@update:value="(val) => player.setVolume(val)"
|
||||
/>
|
||||
<n-text class="slider-num">{{ statusStore.playVolumePercent }}%</n-text>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 播放列表 -->
|
||||
<div
|
||||
v-if="!statusStore.personalFmMode"
|
||||
@@ -121,18 +141,6 @@ const dataStore = useDataStore();
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
|
||||
// 当前播放模式图标
|
||||
const playModeIcon = computed(() => {
|
||||
const mode = statusStore.playSongMode;
|
||||
return statusStore.playHeartbeatMode
|
||||
? "HeartBit"
|
||||
: mode === "repeat"
|
||||
? "Repeat"
|
||||
: mode === "repeat-once"
|
||||
? "RepeatSong"
|
||||
: "Shuffle";
|
||||
});
|
||||
|
||||
// 进度条拖拽结束
|
||||
const sliderDragend = () => {
|
||||
const seek = calculateCurrentTime(statusStore.progress, statusStore.duration);
|
||||
@@ -259,11 +267,6 @@ const sliderDragend = () => {
|
||||
margin: 6px 8px;
|
||||
--n-handle-size: 12px;
|
||||
--n-rail-height: 4px;
|
||||
--n-rail-color: rgba(var(--main-color), 0.14);
|
||||
--n-rail-color-hover: rgba(var(--main-color), 0.3);
|
||||
--n-fill-color: rgb(var(--main-color));
|
||||
--n-handle-color: rgb(var(--main-color));
|
||||
--n-fill-color-hover: rgb(var(--main-color));
|
||||
}
|
||||
span {
|
||||
opacity: 0.6;
|
||||
@@ -277,4 +280,28 @@ const sliderDragend = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// volume
|
||||
.volume-change {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 64px;
|
||||
height: 200px;
|
||||
padding: 12px 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(var(--main-color), 0.14);
|
||||
.slider-num {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: rgb(var(--main-color));
|
||||
}
|
||||
}
|
||||
// slider
|
||||
.n-slider {
|
||||
--n-rail-color: rgba(var(--main-color), 0.14);
|
||||
--n-rail-color-hover: rgba(var(--main-color), 0.3);
|
||||
--n-fill-color: rgb(var(--main-color));
|
||||
--n-handle-color: rgb(var(--main-color));
|
||||
--n-fill-color-hover: rgb(var(--main-color));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -59,7 +59,8 @@ const { start: dynamicCoverStart, stop: dynamicCoverStop } = useTimeoutFn(
|
||||
// 获取动态封面
|
||||
const getDynamicCover = async () => {
|
||||
if (
|
||||
!isLogin() ||
|
||||
isLogin() !== 1 ||
|
||||
musicStore.playSong.path ||
|
||||
!musicStore.playSong.id ||
|
||||
!settingStore.dynamicCover ||
|
||||
settingStore.playerType !== "cover"
|
||||
|
||||
@@ -27,7 +27,12 @@
|
||||
<div v-if="musicStore.playSong.type !== 'radio'" class="artists">
|
||||
<SvgIcon :depth="3" name="Artist" size="20" />
|
||||
<div v-if="Array.isArray(musicStore.playSong.artists)" class="ar-list">
|
||||
<span v-for="ar in musicStore.playSong.artists" :key="ar.id" class="ar">
|
||||
<span
|
||||
v-for="ar in musicStore.playSong.artists"
|
||||
:key="ar.id"
|
||||
class="ar"
|
||||
@click="jumpPage({ name: 'artist', query: { id: ar.id } })"
|
||||
>
|
||||
{{ ar.name }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -44,16 +49,23 @@
|
||||
<!-- 专辑 -->
|
||||
<div v-if="musicStore.playSong.type !== 'radio'" class="album">
|
||||
<SvgIcon :depth="3" name="Album" size="20" />
|
||||
<span class="name-text text-hidden">
|
||||
{{
|
||||
typeof musicStore.playSong.album === "string"
|
||||
? musicStore.playSong.album || "未知专辑"
|
||||
: musicStore.playSong.album?.name || "未知专辑"
|
||||
}}
|
||||
<span
|
||||
v-if="isObject(musicStore.playSong.album)"
|
||||
class="name-text text-hidden"
|
||||
@click="jumpPage({ name: 'album', query: { id: musicStore.playSong.album.id } })"
|
||||
>
|
||||
{{ musicStore.playSong.album?.name || "未知专辑" }}
|
||||
</span>
|
||||
<span v-else class="name-text text-hidden">
|
||||
{{ musicStore.playSong.album || "未知专辑" }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 电台 -->
|
||||
<div v-if="musicStore.playSong.type === 'radio'" class="dj">
|
||||
<div
|
||||
v-if="musicStore.playSong.type === 'radio'"
|
||||
class="dj"
|
||||
@click="jumpPage({ name: 'dj', query: { id: musicStore.playSong.dj?.id } })"
|
||||
>
|
||||
<SvgIcon :depth="3" name="Podcast" size="20" />
|
||||
<span class="name-text text-hidden">{{ musicStore.playSong.dj?.name || "播客电台" }}</span>
|
||||
</div>
|
||||
@@ -61,16 +73,32 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import { useMusicStore, useStatusStore, useSettingStore } from "@/stores";
|
||||
import { debounce, isObject } from "lodash-es";
|
||||
|
||||
defineProps<{
|
||||
center?: boolean;
|
||||
theme?: string;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
const jumpPage = debounce(
|
||||
(go: RouteLocationRaw) => {
|
||||
if (!go) return;
|
||||
statusStore.showFullPlayer = false;
|
||||
router.push(go);
|
||||
},
|
||||
300,
|
||||
{
|
||||
leading: true,
|
||||
trailing: false,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
/>
|
||||
</Transition>
|
||||
<!-- 默认内容 -->
|
||||
<SearchDefault @to-search="toSearch" />
|
||||
<SearchDefault v-if="settingStore.useOnlineService" @to-search="toSearch" />
|
||||
<!-- 搜索结果 -->
|
||||
<SearchSuggest @to-search="toSearch" />
|
||||
<!-- 右键菜单 -->
|
||||
@@ -37,7 +37,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStatusStore, useDataStore } from "@/stores";
|
||||
import { useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||
import { searchDefault } from "@/api/search";
|
||||
import SearchInpMenu from "@/components/Menu/SearchInpMenu.vue";
|
||||
import player from "@/utils/player";
|
||||
@@ -47,13 +47,16 @@ import { formatSongsList } from "@/utils/format";
|
||||
const router = useRouter();
|
||||
const dataStore = useDataStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
// 右键菜单
|
||||
const searchInpMenuRef = ref<InstanceType<typeof SearchInpMenu> | null>(null);
|
||||
|
||||
// 搜索框数据
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null);
|
||||
const searchPlaceholder = ref<string>("搜索音乐 / 视频");
|
||||
const searchPlaceholder = ref<string>(
|
||||
settingStore.useOnlineService ? "搜索音乐 / 视频" : "搜索本地音乐",
|
||||
);
|
||||
const searchRealkeyword = ref<string>("");
|
||||
|
||||
// 搜索框输入限制
|
||||
@@ -94,14 +97,23 @@ const updatePlaceholder = async () => {
|
||||
|
||||
// 前往搜索
|
||||
const toSearch = async (key: any, type: string = "keyword") => {
|
||||
// 关闭搜索框
|
||||
statusStore.searchFocus = false;
|
||||
searchInputRef.value?.blur();
|
||||
// 未输入内容且不存在推荐
|
||||
if (!key && searchPlaceholder.value === "搜索音乐 / 视频") return;
|
||||
if (!key && searchPlaceholder.value !== "搜索音乐 / 视频" && searchRealkeyword.value) {
|
||||
key = searchRealkeyword.value?.trim();
|
||||
}
|
||||
// 关闭搜索框
|
||||
statusStore.searchFocus = false;
|
||||
searchInputRef.value?.blur();
|
||||
// 本地搜索
|
||||
if (!settingStore.useOnlineService) {
|
||||
// 跳转本地搜索页面
|
||||
router.push({
|
||||
name: "search",
|
||||
query: { keyword: key },
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 更新推荐
|
||||
updatePlaceholder();
|
||||
// 前往搜索
|
||||
@@ -143,9 +155,10 @@ const toSearch = async (key: any, type: string = "keyword") => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updatePlaceholder();
|
||||
// 每分钟更新
|
||||
useIntervalFn(updatePlaceholder, 60 * 1000);
|
||||
if (settingStore.useOnlineService) {
|
||||
useIntervalFn(updatePlaceholder, 60 * 1000, { immediate: true });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
<!-- 搜索建议 -->
|
||||
<Transition name="fade" mode="out-in" @after-leave="calcSearchSuggestHeights">
|
||||
<div
|
||||
v-if="Object.keys(searchSuggestData)?.length && searchSuggestData?.order"
|
||||
v-if="
|
||||
Object.keys(searchSuggestData)?.length &&
|
||||
searchSuggestData?.order &&
|
||||
settingStore.useOnlineService
|
||||
"
|
||||
ref="searchSuggestRef"
|
||||
class="all-suggest"
|
||||
>
|
||||
@@ -60,13 +64,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { searchSuggest } from "@/api/search";
|
||||
import { useStatusStore } from "@/stores";
|
||||
import { useStatusStore, useSettingStore } from "@/stores";
|
||||
|
||||
const emit = defineEmits<{
|
||||
toSearch: [key: number | string, type: string];
|
||||
}>();
|
||||
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
// 搜索建议数据
|
||||
const searchSuggestData = ref<any>({});
|
||||
@@ -125,7 +130,7 @@ const calcSearchSuggestHeights = () => {
|
||||
watchDebounced(
|
||||
() => statusStore.searchInputValue,
|
||||
(val) => {
|
||||
if (!val || val === "") return;
|
||||
if (!val || val === "" || !settingStore.useOnlineService) return;
|
||||
getSearchSuggest(val);
|
||||
},
|
||||
{ debounce: 300 },
|
||||
|
||||
@@ -10,7 +10,15 @@
|
||||
{{ packageJson.version }}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
<n-button type="primary" strong secondary @click="checkUpdate"> 检查更新 </n-button>
|
||||
<n-button
|
||||
:loading="statusStore.updateCheck"
|
||||
type="primary"
|
||||
strong
|
||||
secondary
|
||||
@click="checkUpdate"
|
||||
>
|
||||
{{ statusStore.updateCheck ? "检查更新中" : "检查更新" }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
<n-collapse-transition :show="!!updateData">
|
||||
<n-card class="set-item update-data">
|
||||
@@ -24,7 +32,7 @@
|
||||
</n-tag>
|
||||
<n-text :depth="3" class="time">{{ newVersion?.time }}</n-text>
|
||||
</n-flex>
|
||||
<div class="markdown-body" v-html="newVersion?.changelog" />
|
||||
<div class="markdown-body" v-html="newVersion?.changelog" @click="jumpLink" />
|
||||
</n-card>
|
||||
</n-collapse-transition>
|
||||
</div>
|
||||
@@ -45,7 +53,7 @@
|
||||
</n-tag>
|
||||
<n-text :depth="3" class="time">{{ item?.time }}</n-text>
|
||||
</n-flex>
|
||||
<div class="markdown-body" v-html="item?.changelog" />
|
||||
<div class="markdown-body" v-html="item?.changelog" @click="jumpLink" />
|
||||
</n-card>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
@@ -73,8 +81,11 @@
|
||||
import type { UpdateLogType } from "@/types/main";
|
||||
import { getUpdateLog, isElectron, openLink } from "@/utils/helper";
|
||||
import { debounce } from "lodash-es";
|
||||
import { useStatusStore } from "@/stores";
|
||||
import packageJson from "@/../package.json";
|
||||
|
||||
const statusStore = useStatusStore();
|
||||
|
||||
// 社区数据
|
||||
const communityData = [
|
||||
{
|
||||
@@ -102,18 +113,28 @@ const oldVersion = computed<UpdateLogType[]>(() => {
|
||||
});
|
||||
|
||||
// 检查更新
|
||||
const checkUpdate = debounce(() => {
|
||||
const checkUpdate = debounce(
|
||||
() => {
|
||||
if (!isElectron) {
|
||||
window.open(packageJson.github + "/releases", "_blank");
|
||||
return;
|
||||
}
|
||||
window.$notification.info({
|
||||
title: "检查更新",
|
||||
content: "正在检查更新,请稍后...",
|
||||
duration: 3000,
|
||||
});
|
||||
statusStore.updateCheck = true;
|
||||
window.electron.ipcRenderer.send("check-update", true);
|
||||
}, 300);
|
||||
},
|
||||
300,
|
||||
{ leading: true, trailing: false },
|
||||
);
|
||||
|
||||
// 链接跳转
|
||||
const jumpLink = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName !== "A") {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
openLink((target as HTMLAnchorElement).href);
|
||||
};
|
||||
|
||||
// 获取更新日志
|
||||
const getUpdateData = async () => (updateData.value = await getUpdateLog());
|
||||
|
||||
@@ -95,6 +95,13 @@
|
||||
</div>
|
||||
<n-switch class="set" v-model:value="settingStore.menuShowCover" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">开启页面缓存</n-text>
|
||||
<n-text class="tip" :depth="3">是否开启部分页面的缓存,这将会增加内存占用</n-text>
|
||||
</div>
|
||||
<n-switch class="set" v-model:value="settingStore.useKeepAlive" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">页面切换动画</n-text>
|
||||
@@ -184,6 +191,33 @@
|
||||
/>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">日语歌词字体</n-text>
|
||||
<n-text class="tip" :depth="3"> 是否在歌词为日语时单独设置字体 </n-text>
|
||||
</div>
|
||||
<n-flex>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<n-button
|
||||
v-if="settingStore.japaneseLyricFont !== 'follow'"
|
||||
type="primary"
|
||||
strong
|
||||
secondary
|
||||
@click="settingStore.japaneseLyricFont = 'follow'"
|
||||
>
|
||||
恢复默认
|
||||
</n-button>
|
||||
</Transition>
|
||||
<n-select
|
||||
v-model:value="settingStore.japaneseLyricFont"
|
||||
:options="[
|
||||
{ label: '跟随全局', value: 'follow' },
|
||||
...allFontsData.filter((v) => v.value !== 'default'),
|
||||
]"
|
||||
class="set"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">关闭软件时</n-text>
|
||||
@@ -243,12 +277,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SelectOption } from "naive-ui";
|
||||
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
import { isElectron } from "@/utils/helper";
|
||||
import { useDataStore, useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
import { isDev, isElectron } from "@/utils/helper";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import themeColor from "@/assets/data/themeColor.json";
|
||||
import player from "@/utils/player";
|
||||
|
||||
const dataStore = useDataStore();
|
||||
const musicStore = useMusicStore();
|
||||
const settingStore = useSettingStore();
|
||||
const statusStore = useStatusStore();
|
||||
@@ -306,26 +341,40 @@ const modeChange = (val: boolean) => {
|
||||
if (val) {
|
||||
window.$dialog.warning({
|
||||
title: "开启在线服务",
|
||||
content: "确定开启软件的在线服务?更改将在重启后生效!",
|
||||
content: "确定开启软件的在线服务?更改将在热重载后生效!",
|
||||
positiveText: "开启",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
useOnlineService.value = true;
|
||||
settingStore.useOnlineService = true;
|
||||
// 清理播放数据
|
||||
dataStore.$reset();
|
||||
musicStore.$reset();
|
||||
// 清空本地数据
|
||||
localStorage.removeItem("data-store");
|
||||
localStorage.removeItem("music-store");
|
||||
// 热重载
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
window.$dialog.warning({
|
||||
title: "关闭在线服务",
|
||||
content:
|
||||
"确定关闭软件的在线服务?将关闭包括搜索、登录、在线音乐播放等在内的全部在线服务,软件将会变为本地播放器!更改将在软件重启后生效!",
|
||||
"确定关闭软件的在线服务?将关闭包括搜索、登录、在线音乐播放等在内的全部在线服务,并且将会退出登录状态,软件将会变为本地播放器!更改将在重启后生效!",
|
||||
positiveText: "关闭",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
useOnlineService.value = false;
|
||||
settingStore.useOnlineService = false;
|
||||
// 清理播放数据
|
||||
dataStore.$reset();
|
||||
musicStore.$reset();
|
||||
// 清空本地数据
|
||||
localStorage.removeItem("data-store");
|
||||
localStorage.removeItem("music-store");
|
||||
// 重启
|
||||
window.electron.ipcRenderer.send("win-reload");
|
||||
if (!isDev) window.electron.ipcRenderer.send("win-reload");
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
useOnlineService.value = true;
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
</div>
|
||||
<n-switch class="set" v-model:value="settingStore.showLocalCover" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">显示本地默认歌曲目录</n-text>
|
||||
</div>
|
||||
<n-switch class="set" v-model:value="settingStore.showDefaultLocalPath" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item" id="local-list-choose" content-style="flex-direction: column">
|
||||
<n-flex justify="space-between">
|
||||
<div class="label">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">歌词字体大小</n-text>
|
||||
<n-text class="tip" :depth="3">单位 px,最小 30,最大 60</n-text>
|
||||
<n-text class="tip" :depth="3">单位 px,最小 12,最大 60</n-text>
|
||||
</div>
|
||||
<n-flex>
|
||||
<Transition name="fade" mode="out-in">
|
||||
@@ -45,7 +45,7 @@
|
||||
</Transition>
|
||||
<n-input-number
|
||||
v-model:value="settingStore.lyricFontSize"
|
||||
:min="30"
|
||||
:min="12"
|
||||
:max="60"
|
||||
class="set"
|
||||
placeholder="请输入歌词字体大小"
|
||||
@@ -58,7 +58,7 @@
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">翻译歌词大小</n-text>
|
||||
<n-text class="tip" :depth="3">单位 px,最小 12,最大 40</n-text>
|
||||
<n-text class="tip" :depth="3">单位 px,最小 5,最大 40</n-text>
|
||||
</div>
|
||||
<n-flex>
|
||||
<Transition name="fade" mode="out-in">
|
||||
@@ -74,7 +74,7 @@
|
||||
</Transition>
|
||||
<n-input-number
|
||||
v-model:value="settingStore.lyricTranFontSize"
|
||||
:min="12"
|
||||
:min="5"
|
||||
:max="40"
|
||||
:disabled="settingStore.useAMLyrics"
|
||||
class="set"
|
||||
@@ -90,7 +90,7 @@
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">音译歌词大小</n-text>
|
||||
<n-text class="tip" :depth="3">单位 px,最小 12,最大 40</n-text>
|
||||
<n-text class="tip" :depth="3">单位 px,最小 5,最大 40</n-text>
|
||||
</div>
|
||||
<n-flex>
|
||||
<Transition name="fade" mode="out-in">
|
||||
@@ -106,7 +106,7 @@
|
||||
</Transition>
|
||||
<n-input-number
|
||||
v-model:value="settingStore.lyricRomaFontSize"
|
||||
:min="12"
|
||||
:min="5"
|
||||
:max="40"
|
||||
:disabled="settingStore.useAMLyrics"
|
||||
class="set"
|
||||
@@ -232,6 +232,13 @@
|
||||
</div>
|
||||
<n-switch v-model:value="settingStore.lyricsBlur" class="set" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">歌词排除内容</n-text>
|
||||
<n-text class="tip" :depth="3"> 歌词中包含的关键词将不会显示 </n-text>
|
||||
</div>
|
||||
<n-button type="primary" strong secondary @click="openLyricExclude">配置</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
<div class="set-list">
|
||||
<n-h3 prefix="bar"> Apple Music-like Lyrics </n-h3>
|
||||
@@ -253,6 +260,15 @@
|
||||
</div>
|
||||
<n-switch v-model:value="settingStore.useAMSpring" class="set" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">启用 TTML 歌词</n-text>
|
||||
<n-text class="tip" :depth="3">
|
||||
是否启用 TTML 歌词(如有),TTML 歌词支持逐字、翻译、音译等功能, 将会在下一首歌生效
|
||||
</n-text>
|
||||
</div>
|
||||
<n-switch v-model:value="settingStore.enableTTMLLyric" class="set" :round="false" />
|
||||
</n-card>
|
||||
</div>
|
||||
<div v-if="isElectron" class="set-list">
|
||||
<n-h3 prefix="bar"> 桌面歌词 </n-h3>
|
||||
@@ -278,8 +294,8 @@
|
||||
:options="
|
||||
Array.from({ length: 41 }, (_, i) => {
|
||||
return {
|
||||
label: `${20 + i} px`,
|
||||
value: 20 + i,
|
||||
label: `${10 + i} px`,
|
||||
value: 10 + i,
|
||||
};
|
||||
})
|
||||
"
|
||||
@@ -328,6 +344,7 @@ import { useSettingStore, useStatusStore } from "@/stores";
|
||||
import { cloneDeep, isEqual } from "lodash-es";
|
||||
import { isElectron } from "@/utils/helper";
|
||||
import player from "@/utils/player";
|
||||
import { openLyricExclude } from "@/utils/modal";
|
||||
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
@@ -73,13 +73,6 @@
|
||||
</div>
|
||||
<n-switch v-model:value="settingStore.useSongUnlock" class="set" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">听歌打卡</n-text>
|
||||
<n-text class="tip" :depth="3">是否将播放歌曲同步至网易云音乐</n-text>
|
||||
</div>
|
||||
<n-switch v-model:value="settingStore.scrobbleSong" class="set" :round="false" />
|
||||
</n-card>
|
||||
<n-card v-if="isElectron" class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">音频输出设备</n-text>
|
||||
@@ -181,7 +174,7 @@
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="settingStore.dynamicCover"
|
||||
:disabled="!isLogin()"
|
||||
:disabled="isLogin() !== 1"
|
||||
:round="false"
|
||||
class="set"
|
||||
/>
|
||||
@@ -189,7 +182,13 @@
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">音乐频谱</n-text>
|
||||
<n-text class="tip" :depth="3">开启音乐频谱会极大影响性能,如遇问题请关闭</n-text>
|
||||
<n-text class="tip" :depth="3">
|
||||
{{
|
||||
isElectron
|
||||
? "开启音乐频谱会影响性能或音频输出切换等功能,如遇问题请关闭"
|
||||
: "开启可能会造成无法播放或其他问题,如遇任何问题请关闭"
|
||||
}}
|
||||
</n-text>
|
||||
</div>
|
||||
<n-switch
|
||||
class="set"
|
||||
@@ -253,30 +252,45 @@ const songLevelData = {
|
||||
value: "higher",
|
||||
},
|
||||
exhigh: {
|
||||
label: "极高 HQ",
|
||||
label: "极高 (HQ)",
|
||||
tip: "近CD品质的细节体验,最高320kbps",
|
||||
value: "exhigh",
|
||||
},
|
||||
lossless: {
|
||||
label: "无损 SQ",
|
||||
label: "无损 (SQ)",
|
||||
tip: "高保真无损音质,最高48kHz/16bit",
|
||||
value: "lossless",
|
||||
},
|
||||
hires: {
|
||||
label: "高清臻音 Spatial Audio",
|
||||
tip: "环绕声体验,声音听感增强,96kHz/24bit",
|
||||
label: "高解析度无损 (Hi-Res)",
|
||||
tip: "更饱满清晰的高解析度音质,最高192kHz/24bit",
|
||||
value: "hires",
|
||||
},
|
||||
jyeffect: {
|
||||
label: "高清臻音 (Spatial Audio)",
|
||||
tip: "声音听感增强,96kHz/24bit",
|
||||
value: "jyeffect",
|
||||
},
|
||||
jymaster: {
|
||||
label: "超清母带 Master",
|
||||
label: "超清母带 (Master)",
|
||||
tip: "还原音频细节,192kHz/24bit",
|
||||
value: "jymaster",
|
||||
},
|
||||
sky: {
|
||||
label: "沉浸环绕声 Surround Audio",
|
||||
tip: "沉浸式体验,最高 5.1 声道",
|
||||
label: "沉浸环绕声 (Surround Audio)",
|
||||
tip: "沉浸式空间环绕音感,最高5.1声道",
|
||||
value: "sky",
|
||||
},
|
||||
vivid: {
|
||||
label: "臻音全景声 (Audio Vivid)",
|
||||
tip: "极致沉浸三维空间音频,最高7.1.4声道",
|
||||
value: "vivid",
|
||||
},
|
||||
dolby: {
|
||||
label: "杜比全景声 (Dolby Atmos)",
|
||||
tip: "杜比全景声音乐,沉浸式聆听体验",
|
||||
value: "dolby",
|
||||
},
|
||||
};
|
||||
|
||||
// 获取全部输出设备
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</Transition>
|
||||
<!-- 真实图片 -->
|
||||
<img
|
||||
v-if="src"
|
||||
v-if="imgSrc"
|
||||
ref="imgRef"
|
||||
:src="imgSrc"
|
||||
:key="imgSrc"
|
||||
@@ -69,10 +69,27 @@ const imageError = (e: Event) => {
|
||||
};
|
||||
|
||||
// 可视状态变化
|
||||
watchOnce(isCanLook, (show) => {
|
||||
watch(
|
||||
isCanLook,
|
||||
(show) => {
|
||||
emit("update:show", show);
|
||||
if (show) imgSrc.value = props.src;
|
||||
});
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 监听 src 变化
|
||||
watch(
|
||||
() => props.src,
|
||||
(val) => {
|
||||
isLoaded.value = false;
|
||||
if (isCanLook.value) {
|
||||
imgSrc.value = val;
|
||||
} else {
|
||||
imgSrc.value = undefined;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -88,8 +105,8 @@ watchOnce(isCanLook, (show) => {
|
||||
}
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { formatCategoryList } from "@/utils/format";
|
||||
|
||||
interface ListState {
|
||||
playList: SongType[];
|
||||
originalPlayList: SongType[];
|
||||
historyList: SongType[];
|
||||
cloudPlayList: SongType[];
|
||||
searchHistory: string[];
|
||||
@@ -50,11 +51,12 @@ const userDB = localforage.createInstance({
|
||||
storeName: "user",
|
||||
});
|
||||
|
||||
export const useDataStore = defineStore({
|
||||
id: "data",
|
||||
export const useDataStore = defineStore("data", {
|
||||
state: (): ListState => ({
|
||||
// 播放列表
|
||||
playList: [],
|
||||
// 原始播放列表
|
||||
originalPlayList: [],
|
||||
// 播放历史
|
||||
historyList: [],
|
||||
// 搜索历史
|
||||
@@ -158,17 +160,48 @@ export const useDataStore = defineStore({
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
// 保存原始播放列表
|
||||
async setOriginalPlayList(data: SongType[]): Promise<void> {
|
||||
const snapshot = cloneDeep(data);
|
||||
this.originalPlayList = snapshot;
|
||||
await musicDB.setItem("originalPlayList", snapshot);
|
||||
},
|
||||
// 获取原始播放列表
|
||||
async getOriginalPlayList(): Promise<SongType[] | null> {
|
||||
if (Array.isArray(this.originalPlayList) && this.originalPlayList.length > 0) {
|
||||
return this.originalPlayList;
|
||||
}
|
||||
const data = (await musicDB.getItem("originalPlayList")) as SongType[] | null;
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
this.originalPlayList = data;
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// 清除原始播放列表
|
||||
async clearOriginalPlayList(): Promise<void> {
|
||||
this.originalPlayList = [];
|
||||
await musicDB.setItem("originalPlayList", []);
|
||||
},
|
||||
// 新增下一首播放歌曲
|
||||
async setNextPlaySong(song: SongType, index: number): Promise<number> {
|
||||
// 移除重复的歌曲(如果存在)
|
||||
const playList = this.playList.filter((item) => item.id !== song.id);
|
||||
// 若为空,则直接添加
|
||||
if (this.playList.length === 0) {
|
||||
this.playList = [song];
|
||||
await musicDB.setItem("playList", cloneDeep(this.playList));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 在当前播放位置之后插入歌曲
|
||||
const indexAdd = index + 1;
|
||||
playList.splice(indexAdd, 0, song);
|
||||
this.playList.splice(indexAdd, 0, song);
|
||||
// 移除重复的歌曲(如果存在)
|
||||
const playList = this.playList.filter((item, idx) => idx === indexAdd || item.id !== song.id);
|
||||
// 更新本地存储
|
||||
this.playList = playList;
|
||||
await musicDB.setItem("playList", cloneDeep(playList));
|
||||
return indexAdd;
|
||||
// 返回刚刚插入的歌曲索引
|
||||
return playList.findIndex((item) => item.id === song.id);
|
||||
},
|
||||
// 更改播放历史
|
||||
async setHistory(song: SongType) {
|
||||
@@ -296,6 +329,6 @@ export const useDataStore = defineStore({
|
||||
persist: {
|
||||
key: "data-store",
|
||||
storage: localStorage,
|
||||
paths: ["userLoginStatus", "loginType", "userData", "searchHistory", "catData"],
|
||||
pick: ["userLoginStatus", "loginType", "userData", "searchHistory", "catData"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,8 +34,7 @@ const defaultMusicData: SongType = {
|
||||
type: "song",
|
||||
};
|
||||
|
||||
export const useMusicStore = defineStore({
|
||||
id: "music",
|
||||
export const useMusicStore = defineStore("music", {
|
||||
state: (): MusicState => ({
|
||||
// 当前播放歌曲
|
||||
playSong: { ...defaultMusicData },
|
||||
@@ -86,7 +85,7 @@ export const useMusicStore = defineStore({
|
||||
actions: {
|
||||
// 恢复默认音乐数据
|
||||
resetMusicData() {
|
||||
this.playSong = defaultMusicData;
|
||||
this.playSong = { ...defaultMusicData };
|
||||
this.songLyric = {
|
||||
lrcData: [],
|
||||
yrcData: [],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { keywords } from "@/assets/data/exclude";
|
||||
|
||||
interface SettingState {
|
||||
themeMode: "light" | "dark" | "auto";
|
||||
@@ -19,6 +20,7 @@ interface SettingState {
|
||||
themeFollowCover: boolean;
|
||||
globalFont: "default" | string;
|
||||
LyricFont: "follow" | string;
|
||||
japaneseLyricFont: "follow" | string;
|
||||
showCloseAppTip: boolean;
|
||||
closeAppMethod: "exit" | "hide";
|
||||
showTaskbarProgress: boolean;
|
||||
@@ -72,6 +74,7 @@ interface SettingState {
|
||||
showSearchHistory: boolean;
|
||||
useAMLyrics: boolean;
|
||||
useAMSpring: boolean;
|
||||
enableTTMLLyric: boolean;
|
||||
menuShowCover: boolean;
|
||||
preventSleep: boolean;
|
||||
localFilesPath: string[];
|
||||
@@ -83,10 +86,12 @@ interface SettingState {
|
||||
fullPlayerCache: boolean;
|
||||
scrobbleSong: boolean;
|
||||
dynamicCover: boolean;
|
||||
useKeepAlive: boolean;
|
||||
excludeKeywords: string[];
|
||||
showDefaultLocalPath: boolean;
|
||||
}
|
||||
|
||||
export const useSettingStore = defineStore({
|
||||
id: "setting",
|
||||
export const useSettingStore = defineStore("setting", {
|
||||
state: (): SettingState => ({
|
||||
// 个性化
|
||||
themeMode: "auto", // 明暗模式
|
||||
@@ -96,6 +101,7 @@ export const useSettingStore = defineStore({
|
||||
themeGlobalColor: false, // 全局着色
|
||||
globalFont: "default", // 全局字体
|
||||
LyricFont: "follow", // 歌词区域字体
|
||||
japaneseLyricFont: "follow", // 日语歌词字体
|
||||
hideVipTag: false, // 隐藏 VIP 标签
|
||||
showSearchHistory: true, // 显示搜索历史
|
||||
menuShowCover: true, // 菜单显示封面
|
||||
@@ -108,6 +114,7 @@ export const useSettingStore = defineStore({
|
||||
checkUpdateOnStart: true, // 启动时检查更新
|
||||
preventSleep: false, // 是否禁止休眠
|
||||
fullPlayerCache: false, // 全屏播放器缓存
|
||||
useKeepAlive: true, // 使用 keep-alive
|
||||
// 播放
|
||||
songLevel: "exhigh", // 音质
|
||||
playDevice: "default", // 播放设备
|
||||
@@ -134,6 +141,7 @@ export const useSettingStore = defineStore({
|
||||
lyricFontBold: true, // 歌词字体加粗
|
||||
useAMLyrics: false, // 是否使用 AM 歌词
|
||||
useAMSpring: false, // 是否使用 AM 歌词弹簧效果
|
||||
enableTTMLLyric: true, // 启用 TTML 歌词
|
||||
showYrc: true, // 显示逐字歌词
|
||||
showYrcAnimation: true, // 显示逐字歌词动画
|
||||
showTran: true, // 显示歌词翻译
|
||||
@@ -142,8 +150,10 @@ export const useSettingStore = defineStore({
|
||||
lyricsBlur: false, // 歌词模糊
|
||||
lyricsScrollPosition: "start", // 歌词滚动位置
|
||||
lrcMousePause: false, // 鼠标悬停暂停
|
||||
excludeKeywords: keywords, // 排除歌词关键字
|
||||
// 本地
|
||||
localFilesPath: [],
|
||||
showDefaultLocalPath: true, // 显示默认本地路径
|
||||
localSeparators: ["/", "&"],
|
||||
showLocalCover: true,
|
||||
// 下载
|
||||
|
||||
@@ -21,8 +21,7 @@ interface ShortcutStore {
|
||||
};
|
||||
}
|
||||
|
||||
export const useShortcutStore = defineStore({
|
||||
id: "shortcut",
|
||||
export const useShortcutStore = defineStore("shortcut", {
|
||||
state: (): ShortcutStore => ({
|
||||
// 全局快捷键开启
|
||||
globalOpen: true,
|
||||
|
||||
@@ -29,6 +29,7 @@ interface StatusState {
|
||||
lyricIndex: number;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
chorus: number;
|
||||
progress: number;
|
||||
currentTimeOffset: number;
|
||||
playUblock: boolean;
|
||||
@@ -37,10 +38,10 @@ interface StatusState {
|
||||
showDesktopLyric: boolean;
|
||||
showPlayerComment: boolean;
|
||||
personalFmMode: boolean;
|
||||
updateCheck: boolean;
|
||||
}
|
||||
|
||||
export const useStatusStore = defineStore({
|
||||
id: "status",
|
||||
export const useStatusStore = defineStore("status", {
|
||||
state: (): StatusState => ({
|
||||
// 菜单折叠状态
|
||||
menuCollapsed: false,
|
||||
@@ -65,6 +66,8 @@ export const useStatusStore = defineStore({
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
progress: 0,
|
||||
// 副歌时间
|
||||
chorus: 0,
|
||||
// 进度偏移
|
||||
currentTimeOffset: 0,
|
||||
// 封面主题
|
||||
@@ -74,7 +77,7 @@ export const useStatusStore = defineStore({
|
||||
// 音乐频谱数据
|
||||
spectrumsData: [],
|
||||
// 当前播放索引
|
||||
playIndex: 0,
|
||||
playIndex: -1,
|
||||
// 歌词播放索引
|
||||
lyricIndex: -1,
|
||||
// 默认倍速
|
||||
@@ -97,14 +100,49 @@ export const useStatusStore = defineStore({
|
||||
showDesktopLyric: false,
|
||||
// 播放器评论
|
||||
showPlayerComment: false,
|
||||
// 更新检查
|
||||
updateCheck: false,
|
||||
}),
|
||||
getters: {},
|
||||
getters: {
|
||||
// 播放音量图标
|
||||
playVolumeIcon(state) {
|
||||
const volume = state.playVolume;
|
||||
return volume === 0
|
||||
? "VolumeOff"
|
||||
: volume < 0.4
|
||||
? "VolumeMute"
|
||||
: volume < 0.7
|
||||
? "VolumeDown"
|
||||
: "VolumeUp";
|
||||
},
|
||||
// 播放模式图标
|
||||
playModeIcon(state) {
|
||||
const mode = state.playSongMode;
|
||||
return state.playHeartbeatMode
|
||||
? "HeartBit"
|
||||
: mode === "repeat"
|
||||
? "Repeat"
|
||||
: mode === "repeat-once"
|
||||
? "RepeatSong"
|
||||
: "Shuffle";
|
||||
},
|
||||
// 音量百分比
|
||||
playVolumePercent(state) {
|
||||
return Math.round(state.playVolume * 100);
|
||||
},
|
||||
// 播放器主色
|
||||
mainColor(state) {
|
||||
const mainColor = state.songCoverTheme?.main;
|
||||
if (!mainColor) return "239, 239, 239";
|
||||
return `${mainColor.r}, ${mainColor.g}, ${mainColor.b}`;
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
// 持久化
|
||||
persist: {
|
||||
key: "status-store",
|
||||
storage: localStorage,
|
||||
paths: [
|
||||
pick: [
|
||||
"menuCollapsed",
|
||||
"currentTime",
|
||||
"duration",
|
||||
|
||||
109
src/types/amll.d.ts
vendored
Normal file
109
src/types/amll.d.ts
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* AMLL (Apple Music-like Lyrics) 相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 歌词播放器引用类型
|
||||
*/
|
||||
export interface LyricPlayerRef {
|
||||
setCurrentTime?: (time: number) => void;
|
||||
setPlaying?: (playing: boolean) => void;
|
||||
lyricPlayer?: { value?: any };
|
||||
}
|
||||
|
||||
/**
|
||||
* 歌词单词类型
|
||||
*/
|
||||
export interface LyricWord {
|
||||
word: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 歌词行类型
|
||||
*/
|
||||
export interface LyricLine {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
words: LyricWord[];
|
||||
translatedLyric?: string;
|
||||
romanLyric?: string;
|
||||
isBG?: boolean;
|
||||
isDuet?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 歌词点击事件类型
|
||||
*/
|
||||
export interface LyricClickEvent {
|
||||
line: {
|
||||
getLine: () => LyricLine;
|
||||
lyricLine?: LyricLine;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹簧参数类型
|
||||
*/
|
||||
export interface SpringParam {
|
||||
mass: number; // 质量,影响弹簧的惯性
|
||||
damping: number; // 阻尼,影响弹簧的减速速度
|
||||
stiffness: number; // 刚度,影响弹簧的弹力
|
||||
soft: boolean; // 是否使用软弹簧模式
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹簧参数集合
|
||||
*/
|
||||
export interface SpringParams {
|
||||
posX?: SpringParam;
|
||||
posY?: SpringParam;
|
||||
scale?: SpringParam;
|
||||
rotation?: SpringParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* 背景渲染器引用类型
|
||||
*/
|
||||
export interface BackgroundRenderRef {
|
||||
bgRender: any;
|
||||
wrapperEl?: HTMLDivElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 背景渲染器属性类型
|
||||
*/
|
||||
export interface BackgroundRenderProps {
|
||||
album?: string;
|
||||
albumIsVideo?: boolean;
|
||||
fps?: number;
|
||||
playing?: boolean;
|
||||
flowSpeed?: number;
|
||||
hasLyric?: boolean;
|
||||
lowFreqVolume?: number;
|
||||
renderScale?: number;
|
||||
staticMode?: boolean;
|
||||
renderer?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 歌词处理器设置类型
|
||||
*/
|
||||
export interface LyricsProcessorSettings {
|
||||
showYrc: boolean;
|
||||
showRoma: boolean;
|
||||
showTransl: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 歌曲歌词类型
|
||||
*/
|
||||
export interface SongLyric {
|
||||
lrc?: Array<{time: number, content: string, tran?: string, roma?: string}>;
|
||||
yrc?: Array<{time: number, endTime?: number, content: any[], tran?: string, roma?: string}>;
|
||||
ttml?: string;
|
||||
hasYrc?: boolean;
|
||||
lrcAMData?: LyricLine[];
|
||||
yrcAMData?: LyricLine[];
|
||||
}
|
||||
8
src/types/main.d.ts
vendored
8
src/types/main.d.ts
vendored
@@ -131,10 +131,18 @@ export type LyricContentType = {
|
||||
};
|
||||
|
||||
export type LyricType = {
|
||||
/** 歌词开始时间 */
|
||||
time: number;
|
||||
/** 歌词结束时间 */
|
||||
endTime: number;
|
||||
/** 翻译歌词 */
|
||||
tran?: string;
|
||||
/** 音译歌词 */
|
||||
roma?: string;
|
||||
/** 是否为背景歌词 */
|
||||
isBG?: boolean;
|
||||
/** 是否为对唱歌词 */
|
||||
isDuet?: boolean;
|
||||
content: string;
|
||||
contents: LyricContentType[];
|
||||
};
|
||||
|
||||
@@ -23,9 +23,13 @@ import { likePlaylist, playlistTracks } from "@/api/playlist";
|
||||
import { likeArtist } from "@/api/artist";
|
||||
import { radioSub } from "@/api/radio";
|
||||
|
||||
// 是否登录
|
||||
/**
|
||||
* 用户是否登录
|
||||
* @returns 0 - 未登录 / 1 - 正常登录 / 2 - UID 登录
|
||||
*/
|
||||
export const isLogin = (): 0 | 1 | 2 => {
|
||||
const dataStore = useDataStore();
|
||||
if (!dataStore.userLoginStatus) return 0;
|
||||
if (dataStore.loginType === "uid") return 2;
|
||||
return getCookie("MUSIC_U") ? 1 : 0;
|
||||
};
|
||||
@@ -33,7 +37,6 @@ export const isLogin = (): 0 | 1 | 2 => {
|
||||
// 退出登录
|
||||
export const toLogout = async () => {
|
||||
const dataStore = useDataStore();
|
||||
// 退出登录
|
||||
await logout();
|
||||
// 去除 cookie
|
||||
removeCookie("MUSIC_U");
|
||||
|
||||
@@ -18,7 +18,7 @@ class BlobURLManager {
|
||||
// console.log("🌱 Blob URL already exists:", key);
|
||||
return this.blobURLs.get(key)!;
|
||||
}
|
||||
const blob = new Blob([data], { type: format });
|
||||
const blob = new Blob([new Uint8Array(data)], { type: format });
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
// 存储 Blob URL
|
||||
this.blobURLs.set(key, blobURL);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SongType, CoverType, ArtistType, CommentType, MetaData, CatType } from "@/types/main";
|
||||
import { msToTime } from "./time";
|
||||
import { isArray } from "lodash-es";
|
||||
import { flatMap, isArray, uniqBy } from "lodash-es";
|
||||
|
||||
type CoverDataType = {
|
||||
cover: string;
|
||||
@@ -81,7 +81,10 @@ export const formatCoverList = (data: any[]): CoverType[] => {
|
||||
const creator = isArray(item.creator) ? item.creator[0] : item.creator;
|
||||
// 获取歌手信息
|
||||
const artists = (): string | MetaData[] => {
|
||||
const artistData = [item.artist, item.artists, item.ar].flat().filter(Boolean);
|
||||
const artistData = uniqBy(
|
||||
flatMap([item.artist, item.artists, item.ar]).filter(Boolean),
|
||||
"id",
|
||||
);
|
||||
if (artistData.length === 0) return "";
|
||||
return artistData.map((artist) => ({
|
||||
id: artist?.id,
|
||||
|
||||
@@ -273,9 +273,11 @@ export const changeLocalPath = async (delIndex?: number) => {
|
||||
// 检查是否为子文件夹
|
||||
const defaultMusicPath = await window.electron.ipcRenderer.invoke("get-default-dir", "music");
|
||||
const allPath = [defaultMusicPath, ...settingStore.localFilesPath];
|
||||
const isSubfolder = allPath.some((existingPath) => {
|
||||
return selectedDir.startsWith(existingPath);
|
||||
});
|
||||
const isSubfolder = await window.electron.ipcRenderer.invoke(
|
||||
"check-if-subfolder",
|
||||
allPath,
|
||||
selectedDir,
|
||||
);
|
||||
if (!isSubfolder) {
|
||||
settingStore.localFilesPath.push(selectedDir);
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { isElectron } from "./helper";
|
||||
import { openUpdateApp } from "./modal";
|
||||
import { useMusicStore, useDataStore } from "@/stores";
|
||||
import player from "./player";
|
||||
import { useMusicStore, useDataStore, useStatusStore } from "@/stores";
|
||||
import { toLikeSong } from "./auth";
|
||||
import player from "./player";
|
||||
|
||||
// 关闭更新状态
|
||||
const closeUpdateStatus = () => {
|
||||
const statusStore = useStatusStore();
|
||||
statusStore.updateCheck = false;
|
||||
};
|
||||
|
||||
// 全局 IPC 事件
|
||||
const initIpc = () => {
|
||||
@@ -35,13 +41,18 @@ const initIpc = () => {
|
||||
window.electron.ipcRenderer.on("closeDesktopLyric", () => player.toggleDesktopLyric());
|
||||
// 无更新
|
||||
window.electron.ipcRenderer.on("update-not-available", () => {
|
||||
closeUpdateStatus();
|
||||
window.$message.success("当前已是最新版本");
|
||||
});
|
||||
// 有更新
|
||||
window.electron.ipcRenderer.on("update-available", (_, info) => openUpdateApp(info));
|
||||
window.electron.ipcRenderer.on("update-available", (_, info) => {
|
||||
closeUpdateStatus();
|
||||
openUpdateApp(info);
|
||||
});
|
||||
// 更新错误
|
||||
window.electron.ipcRenderer.on("update-error", (_, error) => {
|
||||
console.error("Error updating:", error);
|
||||
closeUpdateStatus();
|
||||
window.$message.error("更新过程出现错误");
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { LyricLine, parseLrc, parseYrc } from "@applemusic-like-lyrics/lyric";
|
||||
import { keywords } from "@/assets/data/exclude";
|
||||
import { LyricLine, parseLrc, parseYrc, TTMLLyric } from "@applemusic-like-lyrics/lyric";
|
||||
import type { LyricType } from "@/types/main";
|
||||
import { useMusicStore } from "@/stores";
|
||||
import { useMusicStore, useSettingStore } from "@/stores";
|
||||
import { msToS } from "./time";
|
||||
|
||||
// 歌词排除内容
|
||||
const getExcludeKeywords = () => {
|
||||
const settingStore = useSettingStore();
|
||||
return settingStore.excludeKeywords;
|
||||
};
|
||||
|
||||
// 恢复默认
|
||||
export const resetSongLyric = () => {
|
||||
const musicStore = useMusicStore();
|
||||
@@ -77,7 +82,7 @@ export const parseLrcData = (lrcData: LyricLine[]): LyricType[] => {
|
||||
const time = msToS(words[0].startTime);
|
||||
const content = words[0].word.trim();
|
||||
// 排除内容
|
||||
if (!content || keywords.some((keyword) => content.includes(keyword))) {
|
||||
if (!content || getExcludeKeywords().some((keyword) => content.includes(keyword))) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
@@ -113,7 +118,7 @@ export const parseYrcData = (yrcData: LyricLine[]): LyricType[] => {
|
||||
.map((word) => word.content + (word.endsWithSpace ? " " : ""))
|
||||
.join("");
|
||||
// 排除内容
|
||||
if (!contentStr || keywords.some((keyword) => contentStr.includes(keyword))) {
|
||||
if (!contentStr || getExcludeKeywords().some((keyword) => contentStr.includes(keyword))) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
@@ -202,3 +207,108 @@ const parseAMData = (lrcData: LyricLine[], tranData?: LyricLine[], romaData?: Ly
|
||||
isDuet: line.isDuet ?? false,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 从TTML格式解析歌词并转换为AMLL格式
|
||||
* @param ttmlContent TTML格式的歌词内容
|
||||
* @returns AMLL格式的歌词行数组
|
||||
*/
|
||||
export const parseTTMLToAMLL = (ttmlContent: TTMLLyric): LyricLine[] => {
|
||||
if (!ttmlContent) return [];
|
||||
|
||||
try {
|
||||
const validLines = ttmlContent.lines
|
||||
.filter((line): line is any => line && typeof line === "object" && Array.isArray(line.words))
|
||||
.map((line) => {
|
||||
const words = line.words
|
||||
.filter((word: any) => word && typeof word === "object")
|
||||
.map((word: any) => ({
|
||||
word: String(word.word || " "),
|
||||
startTime: Number(word.startTime) || 0,
|
||||
endTime: Number(word.endTime) || 0,
|
||||
}));
|
||||
|
||||
if (!words.length) return null;
|
||||
|
||||
const startTime = words[0].startTime;
|
||||
const endTime = words[words.length - 1].endTime;
|
||||
|
||||
return {
|
||||
words,
|
||||
startTime,
|
||||
endTime,
|
||||
translatedLyric: String(line.translatedLyric || ""),
|
||||
romanLyric: String(line.romanLyric || ""),
|
||||
isBG: Boolean(line.isBG),
|
||||
isDuet: Boolean(line.isDuet),
|
||||
};
|
||||
})
|
||||
.filter((line): line is LyricLine => line !== null);
|
||||
|
||||
return validLines;
|
||||
} catch (error) {
|
||||
console.error("TTML parsing error:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从TTML格式解析歌词并转换为默认Yrc格式
|
||||
* @param ttmlContent TTML格式的歌词内容
|
||||
* @returns 默认Yrc格式的歌词行数组
|
||||
*/
|
||||
export const parseTTMLToYrc = (ttmlContent: TTMLLyric): LyricType[] => {
|
||||
if (!ttmlContent) return [];
|
||||
|
||||
try {
|
||||
// 数据处理
|
||||
const yrcList = ttmlContent.lines
|
||||
.map((line) => {
|
||||
const words = line.words;
|
||||
const time = msToS(words[0].startTime);
|
||||
const endTime = msToS(words[words.length - 1].endTime);
|
||||
const contents = words.map((word) => {
|
||||
return {
|
||||
time: msToS(word.startTime),
|
||||
endTime: msToS(word.endTime),
|
||||
duration: msToS(word.endTime - word.startTime),
|
||||
content: word.word.trim(),
|
||||
endsWithSpace: word.word.endsWith(" "),
|
||||
};
|
||||
});
|
||||
// 完整歌词
|
||||
const contentStr = contents
|
||||
.map((word) => word.content + (word.endsWithSpace ? " " : ""))
|
||||
.join("");
|
||||
// 排除内容
|
||||
if (!contentStr || getExcludeKeywords().some((keyword) => contentStr.includes(keyword))) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
time,
|
||||
endTime,
|
||||
content: contentStr,
|
||||
contents,
|
||||
tran: line.translatedLyric || "",
|
||||
roma: line.romanLyric || "",
|
||||
isBG: line.isBG,
|
||||
isDuet: line.isDuet,
|
||||
};
|
||||
})
|
||||
.filter((line) => line !== null);
|
||||
return yrcList;
|
||||
} catch (error) {
|
||||
console.error("TTML parsing to yrc error:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 检测语言
|
||||
export const getLyricLanguage = (lyric: string): string => {
|
||||
// 判断日语 根据平假名和片假名
|
||||
if (/[\u3040-\u309f\u30a0-\u30ff]/.test(lyric)) return "ja";
|
||||
// 判断简体中文 根据中日韩统一表意文字基本区
|
||||
if (/[\u4e00-\u9fa5]/.test(lyric)) return "zh-CN";
|
||||
// 默认英语
|
||||
return "en";
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import UpdatePlaylist from "@/components/Modal/UpdatePlaylist.vue";
|
||||
import DownloadSong from "@/components/Modal/DownloadSong.vue";
|
||||
import MainSetting from "@/components/Setting/MainSetting.vue";
|
||||
import UpdateApp from "@/components/Modal/UpdateApp.vue";
|
||||
import ExcludeKeywords from "@/components/Modal/ExcludeKeywords.vue";
|
||||
|
||||
// 用户协议
|
||||
export const openUserAgreement = () => {
|
||||
@@ -89,6 +90,7 @@ export const openSongInfoEditor = (song: SongType) => {
|
||||
preset: "card",
|
||||
transformOrigin: "center",
|
||||
autoFocus: false,
|
||||
trapFocus: false,
|
||||
// contentStyle: { padding: 0 },
|
||||
style: { width: "600px" },
|
||||
title: "编辑歌曲信息",
|
||||
@@ -101,7 +103,7 @@ export const openSongInfoEditor = (song: SongType) => {
|
||||
// 添加到歌单
|
||||
export const openPlaylistAdd = (data: SongType[], isLocal: boolean) => {
|
||||
if (!data.length) return window.$message.warning("请正确选择歌曲");
|
||||
if (!isLogin()) return openUserLogin();
|
||||
if (!isLogin() && !isLocal) return openUserLogin();
|
||||
const modal = window.$modal.create({
|
||||
preset: "card",
|
||||
transformOrigin: "center",
|
||||
@@ -233,3 +235,17 @@ export const openUpdateApp = (data: UpdateInfoType) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 歌词排除内容
|
||||
export const openLyricExclude = () => {
|
||||
window.$modal.create({
|
||||
preset: "card",
|
||||
transformOrigin: "center",
|
||||
autoFocus: false,
|
||||
style: { width: "600px" },
|
||||
title: "歌词排除内容",
|
||||
content: () => {
|
||||
return h(ExcludeKeywords);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import type { SongType, PlayModeType } from "@/types/main";
|
||||
import type { SongType, PlayModeType, LyricType } from "@/types/main";
|
||||
import type { MessageReactive } from "naive-ui";
|
||||
import { Howl, Howler } from "howler";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||
import { parsedLyricsData, resetSongLyric, parseLocalLyric } from "./lyric";
|
||||
import { songUrl, unlockSongUrl, songLyric } from "@/api/song";
|
||||
import {
|
||||
parsedLyricsData,
|
||||
resetSongLyric,
|
||||
parseLocalLyric,
|
||||
parseTTMLToAMLL,
|
||||
parseTTMLToYrc,
|
||||
} from "./lyric";
|
||||
import { songUrl, unlockSongUrl, songLyric, songChorus, songLyricTTML } from "@/api/song";
|
||||
import { getCoverColorData } from "@/utils/color";
|
||||
import { calculateProgress } from "./time";
|
||||
import { isElectron, isDev } from "./helper";
|
||||
@@ -12,9 +18,10 @@ import { heartRateList } from "@/api/playlist";
|
||||
import { formatSongsList } from "./format";
|
||||
import { isLogin } from "./auth";
|
||||
import { openUserLogin } from "./modal";
|
||||
import { scrobble } from "@/api/user";
|
||||
import { personalFm, personalFmToTrash } from "@/api/rec";
|
||||
import blob from "./blob";
|
||||
import { parseTTML } from "@applemusic-like-lyrics/lyric";
|
||||
import { LyricLine } from "@applemusic-like-lyrics/core";
|
||||
|
||||
// 播放器核心
|
||||
// Howler.js
|
||||
@@ -30,17 +37,30 @@ class Player {
|
||||
// 频谱数据
|
||||
private audioContext: AudioContext | null = null;
|
||||
private analyser: AnalyserNode | null = null;
|
||||
private dataArray: Uint8Array | null = null;
|
||||
private dataArray: Uint8Array<ArrayBuffer> | null = null;
|
||||
private source: MediaElementAudioSourceNode | null = null;
|
||||
// 其他数据
|
||||
private testNumber: number = 0;
|
||||
private message: MessageReactive | null = null;
|
||||
// 预载下一首歌曲播放地址缓存(仅存 URL,不创建 Howl)
|
||||
private nextPrefetch: { id: number; url: string | null; ublock: boolean } | null = null;
|
||||
constructor() {
|
||||
// 创建播放器实例
|
||||
this.player = new Howl({ src: [""], format: allowPlayFormat, autoplay: false });
|
||||
// 初始化媒体会话
|
||||
this.initMediaSession();
|
||||
}
|
||||
/**
|
||||
* 洗牌数组(Fisher-Yates)
|
||||
*/
|
||||
private shuffleArray<T>(arr: T[]): T[] {
|
||||
const copy = arr.slice();
|
||||
for (let i = copy.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[copy[i], copy[j]] = [copy[j], copy[i]];
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
@@ -52,6 +72,7 @@ class Player {
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
progress: 0,
|
||||
chorus: 0,
|
||||
currentTimeOffset: 0,
|
||||
lyricIndex: -1,
|
||||
playStatus: false,
|
||||
@@ -92,7 +113,7 @@ class Player {
|
||||
* 处理播放状态
|
||||
*/
|
||||
private handlePlayStatus() {
|
||||
const musicStore = useMusicStore();
|
||||
// const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
// 清理定时器
|
||||
@@ -104,31 +125,16 @@ class Player {
|
||||
const duration = this.player.duration();
|
||||
// 计算进度条距离
|
||||
const progress = calculateProgress(currentTime, duration);
|
||||
// 计算歌词索引
|
||||
const hasYrc = !musicStore.songLyric.yrcData.length || !settingStore.showYrc;
|
||||
const lyrics = hasYrc ? musicStore.songLyric.lrcData : musicStore.songLyric.yrcData;
|
||||
// 歌词实时偏移量
|
||||
const currentTimeOffset = statusStore.currentTimeOffset;
|
||||
const index = lyrics?.findIndex((v) => v?.time >= currentTime + currentTimeOffset);
|
||||
// 歌词跨界处理
|
||||
const lyricIndex = index === -1 ? lyrics.length - 1 : index - 1;
|
||||
// 计算歌词索引(支持 LRC 与逐字 YRC,对唱重叠处理)
|
||||
const { index: lyricIndex, lyrics } = this.calculateLyricIndex(currentTime);
|
||||
// 更新状态
|
||||
statusStore.$patch({
|
||||
currentTime,
|
||||
duration,
|
||||
progress,
|
||||
lyricIndex,
|
||||
});
|
||||
statusStore.$patch({ currentTime, duration, progress, lyricIndex });
|
||||
// 客户端事件
|
||||
if (isElectron) {
|
||||
// 歌词变化
|
||||
window.electron.ipcRenderer.send("play-lyric-change", {
|
||||
index: lyricIndex,
|
||||
lyric: cloneDeep(
|
||||
settingStore.showYrc && musicStore.songLyric.yrcData?.length
|
||||
? musicStore.songLyric.yrcData
|
||||
: musicStore.songLyric.lrcData,
|
||||
),
|
||||
lyric: cloneDeep(lyrics),
|
||||
});
|
||||
// 进度条
|
||||
if (settingStore.showTaskbarProgress) {
|
||||
@@ -137,6 +143,66 @@ class Player {
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
/**
|
||||
* 计算歌词索引
|
||||
* - 普通歌词(LRC):沿用当前按开始时间定位的算法
|
||||
* - 逐字歌词(YRC):当播放时间位于某句 [time, endTime) 区间内时,索引为该句;
|
||||
* 若下一句开始时间落在上一句区间(对唱重叠),仍保持上一句索引,直到上一句结束。
|
||||
*/
|
||||
private calculateLyricIndex(currentTime: number): { index: number; lyrics: LyricType[] } {
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
// 应用实时偏移 + 0.3s(解决对唱时歌词延迟问题)
|
||||
const playSeek = currentTime + statusStore.currentTimeOffset + 0.3;
|
||||
// 选择歌词类型
|
||||
const useYrc = !!(settingStore.showYrc && musicStore.songLyric.yrcData.length);
|
||||
const lyrics = useYrc ? musicStore.songLyric.yrcData : musicStore.songLyric.lrcData;
|
||||
// 无歌词时
|
||||
if (!lyrics || !lyrics.length) return { index: -1, lyrics: [] };
|
||||
|
||||
// 普通歌词:保持原有计算方式
|
||||
if (!useYrc) {
|
||||
const idx = lyrics.findIndex((v) => (v?.time ?? 0) >= playSeek);
|
||||
const index = idx === -1 ? lyrics.length - 1 : idx - 1;
|
||||
return { index, lyrics };
|
||||
}
|
||||
|
||||
// 逐字歌词(对唱最多两句同时存在):
|
||||
// - 计算在播放进度下处于激活区间的句子集合 activeIndices([time, endTime))
|
||||
// - 若激活数 >= 2,仅保留最后两句作为对唱对(不允许三句同时有效)
|
||||
// - 索引取该对唱对中较早的一句(保持“上一句”高亮)
|
||||
// - 若无激活句:首句之前返回 -1;否则回退到最近一句
|
||||
|
||||
const firstStart = lyrics[0]?.time ?? 0;
|
||||
if (playSeek < firstStart) {
|
||||
return { index: -1, lyrics };
|
||||
}
|
||||
|
||||
const activeIndices: number[] = [];
|
||||
for (let i = 0; i < lyrics.length; i++) {
|
||||
const start = lyrics[i]?.time ?? 0;
|
||||
const end = lyrics[i]?.endTime ?? Infinity;
|
||||
if (playSeek >= start && playSeek < end) {
|
||||
activeIndices.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (activeIndices.length === 0) {
|
||||
// 不在任何句子的区间里:退回到最近一句(按开始时间)
|
||||
const nextIdx = lyrics.findIndex((v) => (v?.time ?? 0) > playSeek);
|
||||
const index = nextIdx === -1 ? lyrics.length - 1 : nextIdx - 1;
|
||||
return { index, lyrics };
|
||||
}
|
||||
|
||||
if (activeIndices.length === 1) {
|
||||
return { index: activeIndices[0], lyrics };
|
||||
}
|
||||
|
||||
// 激活句 >= 2:限制为最后两句对唱
|
||||
const pair = activeIndices.slice(-2);
|
||||
return { index: pair[0], lyrics };
|
||||
}
|
||||
/**
|
||||
* 获取在线播放链接
|
||||
* @param id 歌曲id
|
||||
@@ -156,8 +222,14 @@ class Player {
|
||||
} else return null;
|
||||
}
|
||||
// 返回歌曲地址
|
||||
// 客户端直接返回,网页端转 https
|
||||
const url = isElectron ? songData.url : songData.url.replace(/^http:/, "https:");
|
||||
// 客户端直接返回,网页端转 https, 并转换url以便解决音乐链接cors问题
|
||||
const url = isElectron
|
||||
? songData.url
|
||||
: songData.url
|
||||
.replace(/^http:/, "https:")
|
||||
.replace(/m804\.music\.126\.net/g, "m801.music.126.net")
|
||||
.replace(/m704\.music\.126\.net/g, "m701.music.126.net");
|
||||
console.log(`🎧 ${id} music url:`, url);
|
||||
return url;
|
||||
}
|
||||
/**
|
||||
@@ -172,18 +244,87 @@ class Player {
|
||||
const keyWord = songData.name + "-" + artist;
|
||||
if (!songId || !keyWord) return null;
|
||||
// 尝试解锁
|
||||
const [neteaseUrl, kuwoUrl] = await Promise.all([
|
||||
const results = await Promise.allSettled([
|
||||
unlockSongUrl(songId, keyWord, "netease"),
|
||||
unlockSongUrl(songId, keyWord, "kuwo"),
|
||||
]);
|
||||
if (neteaseUrl.code === 200 && neteaseUrl.url !== "") return neteaseUrl.url;
|
||||
if (kuwoUrl.code === 200 && kuwoUrl.url !== "") return kuwoUrl.url;
|
||||
// 解析结果
|
||||
const [neteaseRes, kuwoRes] = results;
|
||||
if (
|
||||
neteaseRes.status === "fulfilled" &&
|
||||
neteaseRes.value.code === 200 &&
|
||||
neteaseRes.value.url
|
||||
) {
|
||||
return neteaseRes.value.url;
|
||||
}
|
||||
if (kuwoRes.status === "fulfilled" && kuwoRes.value.code === 200 && kuwoRes.value.url) {
|
||||
return kuwoRes.value.url;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error in getUnlockSongUrl", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 预载下一首歌曲的播放地址(优先官方,失败则并发尝试解灰)
|
||||
* 仅缓存 URL,不实例化播放器
|
||||
*/
|
||||
private async prefetchNextSongUrl() {
|
||||
try {
|
||||
const dataStore = useDataStore();
|
||||
const statusStore = useStatusStore();
|
||||
// const musicStore = useMusicStore();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
// 无列表或私人FM模式直接跳过
|
||||
const playList = dataStore.playList;
|
||||
if (!playList?.length || statusStore.personalFmMode) {
|
||||
this.nextPrefetch = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算下一首(循环到首)
|
||||
let nextIndex = statusStore.playIndex + 1;
|
||||
if (nextIndex >= playList.length) nextIndex = 0;
|
||||
const nextSong = playList[nextIndex];
|
||||
if (!nextSong) {
|
||||
this.nextPrefetch = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地歌曲:直接缓存 file URL
|
||||
if (nextSong.path) {
|
||||
const songId = nextSong.type === "radio" ? nextSong.dj?.id : nextSong.id;
|
||||
this.nextPrefetch = {
|
||||
id: Number(songId || nextSong.id),
|
||||
url: `file://${nextSong.path}`,
|
||||
ublock: false,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// 在线歌曲:优先官方,其次解灰
|
||||
const songId = nextSong.type === "radio" ? nextSong.dj?.id : nextSong.id;
|
||||
if (!songId) {
|
||||
this.nextPrefetch = null;
|
||||
return;
|
||||
}
|
||||
const canUnlock = isElectron && nextSong.type !== "radio" && settingStore.useSongUnlock;
|
||||
const unlockUrlPromise = canUnlock ? this.getUnlockSongUrl(nextSong) : null;
|
||||
const url = await this.getOnlineUrl(songId);
|
||||
if (url) {
|
||||
this.nextPrefetch = { id: songId, url, ublock: false };
|
||||
} else if (unlockUrlPromise) {
|
||||
const unlockUrl = await unlockUrlPromise;
|
||||
this.nextPrefetch = { id: songId, url: unlockUrl || null, ublock: !!unlockUrl };
|
||||
} else {
|
||||
this.nextPrefetch = { id: songId, url: null, ublock: false };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error prefetching next song url:", error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 创建播放器
|
||||
* @param src 播放地址
|
||||
@@ -217,9 +358,11 @@ class Player {
|
||||
if (!settingStore.showSpectrums) this.toggleOutputDevice();
|
||||
// 自动播放
|
||||
if (autoPlay) this.play();
|
||||
// 获取歌词数据 - 非电台和本地
|
||||
if (type !== "radio" && !path) this.getLyricData(id);
|
||||
else resetSongLyric();
|
||||
// 获取歌曲附加信息 - 非电台和本地
|
||||
if (type !== "radio" && !path) {
|
||||
this.getLyricData(id);
|
||||
this.getChorus(id);
|
||||
} else resetSongLyric();
|
||||
// 定时获取状态
|
||||
if (!this.playerInterval) this.handlePlayStatus();
|
||||
// 新增播放历史
|
||||
@@ -230,6 +373,8 @@ class Player {
|
||||
if (!path) this.updateMediaSession();
|
||||
// 开发模式
|
||||
if (isDev) window.player = this.player;
|
||||
// 异步预载下一首播放地址(不阻塞当前播放)
|
||||
void this.prefetchNextSongUrl();
|
||||
}
|
||||
/**
|
||||
* 播放器事件
|
||||
@@ -243,14 +388,17 @@ class Player {
|
||||
// 获取数据
|
||||
const dataStore = useDataStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
const playSongData = this.getPlaySongData();
|
||||
// 获取配置
|
||||
const { seek } = options;
|
||||
// 初次加载
|
||||
this.player.once("load", () => {
|
||||
// 允许跨域
|
||||
if (settingStore.showSpectrums) {
|
||||
const audioDom = this.getAudioDom();
|
||||
audioDom.crossOrigin = "anonymous";
|
||||
}
|
||||
// 恢复进度( 需距离本曲结束大于 2 秒 )
|
||||
if (seek && statusStore.duration - statusStore.currentTime > 2) this.setSeek(seek);
|
||||
// 更新状态
|
||||
@@ -276,7 +424,7 @@ class Player {
|
||||
});
|
||||
// 暂停
|
||||
this.player.on("pause", () => {
|
||||
window.document.title = "SPlayer";
|
||||
if (!isElectron) window.document.title = "SPlayer";
|
||||
// ipc
|
||||
if (isElectron) window.electron.ipcRenderer.send("play-status-change", false);
|
||||
console.log("⏸️ song pause:", playSongData);
|
||||
@@ -399,8 +547,58 @@ class Player {
|
||||
resetSongLyric();
|
||||
return;
|
||||
}
|
||||
const lyricRes = await songLyric(id);
|
||||
|
||||
try {
|
||||
const musicStore = useMusicStore();
|
||||
const settingStore = useSettingStore();
|
||||
const [lyricRes, ttmlContent] = await Promise.all([
|
||||
songLyric(id),
|
||||
settingStore.enableTTMLLyric && songLyricTTML(id),
|
||||
]);
|
||||
parsedLyricsData(lyricRes);
|
||||
if (ttmlContent) {
|
||||
const parsedResult = parseTTML(ttmlContent);
|
||||
if (!parsedResult?.lines?.length) return;
|
||||
const ttmlLyric = parseTTMLToAMLL(parsedResult);
|
||||
const ttmlYrcLyric = parseTTMLToYrc(parsedResult);
|
||||
console.log("TTML lyrics:", ttmlLyric, ttmlYrcLyric);
|
||||
// 合并数据
|
||||
const updates: Partial<{ yrcAMData: LyricLine[]; yrcData: LyricType[] }> = {};
|
||||
if (ttmlLyric?.length) {
|
||||
updates.yrcAMData = ttmlLyric;
|
||||
console.log("✅ TTML AMLL lyrics success");
|
||||
}
|
||||
if (ttmlYrcLyric?.length) {
|
||||
updates.yrcData = ttmlYrcLyric;
|
||||
console.log("✅ TTML Yrc lyrics success");
|
||||
}
|
||||
if (Object.keys(updates).length) {
|
||||
musicStore.songLyric = {
|
||||
...musicStore.songLyric,
|
||||
...updates,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Error loading lyrics:", error);
|
||||
resetSongLyric();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取副歌时间
|
||||
* @param id 歌曲id
|
||||
*/
|
||||
private async getChorus(id: number) {
|
||||
const statusStore = useStatusStore();
|
||||
const result = await songChorus(id);
|
||||
if (result?.code !== 200 || result?.chorus?.length === 0) {
|
||||
statusStore.chorus = 0;
|
||||
return;
|
||||
}
|
||||
// 计算并保存
|
||||
const chorus = result?.chorus?.[0]?.startTime;
|
||||
const time = ((chorus / 1000 / statusStore.duration) * 100).toFixed(2);
|
||||
statusStore.chorus = Number(time);
|
||||
}
|
||||
/**
|
||||
* 播放错误
|
||||
@@ -512,7 +710,7 @@ class Player {
|
||||
statusStore.playLoading = true;
|
||||
// 本地歌曲
|
||||
if (path) {
|
||||
await this.createPlayer(path, autoPlay, seek);
|
||||
await this.createPlayer(`file://${path}`, autoPlay, seek);
|
||||
// 获取歌曲元信息
|
||||
await this.parseLocalMusicInfo(path);
|
||||
}
|
||||
@@ -520,6 +718,16 @@ class Player {
|
||||
else if (id && dataStore.playList.length) {
|
||||
const songId = type === "radio" ? dj?.id : id;
|
||||
if (!songId) throw new Error("Get song id error");
|
||||
// 优先使用预载的下一首 URL(若命中缓存)
|
||||
const cached = this.nextPrefetch;
|
||||
if (cached && cached.id === songId && cached.url) {
|
||||
statusStore.playUblock = cached.ublock;
|
||||
await this.createPlayer(cached.url, autoPlay, seek);
|
||||
} else {
|
||||
// 并发启动解灰请求(仅在 Electron 且非电台且开启解灰时)
|
||||
const canUnlock = isElectron && type !== "radio" && settingStore.useSongUnlock;
|
||||
const unlockUrlPromise = canUnlock ? this.getUnlockSongUrl(playSongData) : null;
|
||||
// 先请求正常播放地址
|
||||
const url = await this.getOnlineUrl(songId);
|
||||
// 正常播放地址
|
||||
if (url) {
|
||||
@@ -527,8 +735,9 @@ class Player {
|
||||
await this.createPlayer(url, autoPlay, seek);
|
||||
}
|
||||
// 尝试解灰
|
||||
else if (isElectron && type !== "radio" && settingStore.useSongUnlock) {
|
||||
const unlockUrl = await this.getUnlockSongUrl(playSongData);
|
||||
else if (unlockUrlPromise) {
|
||||
// 若正常地址不可用,则等待并使用并发中的解灰结果
|
||||
const unlockUrl = await unlockUrlPromise;
|
||||
if (unlockUrl) {
|
||||
statusStore.playUblock = true;
|
||||
console.log("🎼 Song unlock successfully:", unlockUrl);
|
||||
@@ -556,6 +765,7 @@ class Player {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 初始化音乐播放器出错:", error);
|
||||
window.$message.error("播放器遇到错误,请尝试软件热重载");
|
||||
@@ -573,10 +783,11 @@ class Player {
|
||||
return;
|
||||
}
|
||||
this.player.play();
|
||||
statusStore.playStatus = true;
|
||||
// 淡入
|
||||
await new Promise<void>((resolve) => {
|
||||
this.player.once("play", () => {
|
||||
// 在淡入开始时立即设置播放状态
|
||||
statusStore.playStatus = true;
|
||||
this.player.fade(0, statusStore.playVolume, this.getFadeTime());
|
||||
resolve();
|
||||
});
|
||||
@@ -588,12 +799,20 @@ class Player {
|
||||
*/
|
||||
async pause(changeStatus: boolean = true) {
|
||||
const statusStore = useStatusStore();
|
||||
|
||||
// 播放器未加载完成
|
||||
if (this.player.state() !== "loaded") {
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即设置播放状态
|
||||
if (changeStatus) statusStore.playStatus = false;
|
||||
|
||||
// 淡出
|
||||
await new Promise<void>((resolve) => {
|
||||
this.player.fade(statusStore.playVolume, 0, this.getFadeTime());
|
||||
this.player.once("fade", () => {
|
||||
this.player.pause();
|
||||
if (changeStatus) statusStore.playStatus = false;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
@@ -624,8 +843,6 @@ class Player {
|
||||
const playListLength = playList.length;
|
||||
// 播放列表是否为空
|
||||
if (playListLength === 0) throw new Error("Play list is empty");
|
||||
// 打卡
|
||||
this.scrobbleSong();
|
||||
// 若为私人FM
|
||||
if (statusStore.personalFmMode) {
|
||||
await this.initPersonalFM(true);
|
||||
@@ -637,19 +854,15 @@ class Player {
|
||||
this.setSeek(0);
|
||||
await this.play();
|
||||
}
|
||||
// 列表循环或处于心动模式
|
||||
if (playSongMode === "repeat" || playHeartbeatMode || playSong.type === "radio") {
|
||||
// 列表循环或处于心动模式或随机模式
|
||||
if (
|
||||
playSongMode === "repeat" ||
|
||||
playSongMode === "shuffle" ||
|
||||
playHeartbeatMode ||
|
||||
playSong.type === "radio"
|
||||
) {
|
||||
statusStore.playIndex += type === "next" ? 1 : -1;
|
||||
}
|
||||
// 随机播放
|
||||
else if (playSongMode === "shuffle") {
|
||||
let newIndex: number;
|
||||
// 确保不会随机到同一首
|
||||
do {
|
||||
newIndex = Math.floor(Math.random() * playListLength);
|
||||
} while (newIndex === statusStore.playIndex);
|
||||
statusStore.playIndex = newIndex;
|
||||
}
|
||||
// 单曲循环
|
||||
else if (playSongMode === "repeat-once") {
|
||||
statusStore.lyricIndex = -1;
|
||||
@@ -678,28 +891,65 @@ class Player {
|
||||
* 切换播放模式
|
||||
* @param mode 播放模式 repeat / repeat-once / shuffle
|
||||
*/
|
||||
togglePlayMode(mode: PlayModeType | false) {
|
||||
async togglePlayMode(mode: PlayModeType | false) {
|
||||
const statusStore = useStatusStore();
|
||||
const dataStore = useDataStore();
|
||||
const musicStore = useMusicStore();
|
||||
// 退出心动模式
|
||||
if (statusStore.playHeartbeatMode) this.toggleHeartMode(false);
|
||||
// 若传入了指定模式
|
||||
// 计算目标模式
|
||||
let targetMode: PlayModeType;
|
||||
if (mode) {
|
||||
statusStore.playSongMode = mode;
|
||||
targetMode = mode;
|
||||
} else {
|
||||
switch (statusStore.playSongMode) {
|
||||
case "repeat":
|
||||
statusStore.playSongMode = "repeat-once";
|
||||
targetMode = "repeat-once";
|
||||
break;
|
||||
case "shuffle":
|
||||
statusStore.playSongMode = "repeat";
|
||||
targetMode = "repeat";
|
||||
break;
|
||||
case "repeat-once":
|
||||
statusStore.playSongMode = "shuffle";
|
||||
targetMode = "shuffle";
|
||||
break;
|
||||
default:
|
||||
statusStore.playSongMode = "repeat";
|
||||
targetMode = "repeat";
|
||||
}
|
||||
}
|
||||
// 进入随机模式:保存原顺序并打乱当前歌单
|
||||
if (targetMode === "shuffle" && statusStore.playSongMode !== "shuffle") {
|
||||
const currentList = dataStore.playList;
|
||||
if (currentList && currentList.length > 1) {
|
||||
const currentSongId = musicStore.playSong?.id;
|
||||
await dataStore.setOriginalPlayList(currentList);
|
||||
const shuffled = this.shuffleArray(currentList);
|
||||
await dataStore.setPlayList(shuffled);
|
||||
if (currentSongId) {
|
||||
const newIndex = shuffled.findIndex((s: any) => s?.id === currentSongId);
|
||||
if (newIndex !== -1) useStatusStore().playIndex = newIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 离开随机模式:恢复到原顺序
|
||||
if (
|
||||
statusStore.playSongMode === "shuffle" &&
|
||||
(targetMode === "repeat" || targetMode === "repeat-once")
|
||||
) {
|
||||
const original = await dataStore.getOriginalPlayList();
|
||||
if (original && original.length) {
|
||||
const currentSongId = musicStore.playSong?.id;
|
||||
await dataStore.setPlayList(original);
|
||||
if (currentSongId) {
|
||||
const origIndex = original.findIndex((s: any) => s?.id === currentSongId);
|
||||
useStatusStore().playIndex = origIndex !== -1 ? origIndex : 0;
|
||||
} else {
|
||||
useStatusStore().playIndex = 0;
|
||||
}
|
||||
await dataStore.clearOriginalPlayList();
|
||||
}
|
||||
}
|
||||
// 应用模式
|
||||
statusStore.playSongMode = targetMode;
|
||||
this.playModeSyncIpc();
|
||||
}
|
||||
/**
|
||||
@@ -738,20 +988,28 @@ class Player {
|
||||
}
|
||||
/**
|
||||
* 设置播放音量
|
||||
* @param volume 音量
|
||||
* @param actions 音量
|
||||
*/
|
||||
setVolume(volume: number | "up" | "down") {
|
||||
setVolume(actions: number | "up" | "down" | WheelEvent) {
|
||||
const statusStore = useStatusStore();
|
||||
// 直接设置
|
||||
if (typeof volume === "number") {
|
||||
volume = Math.max(0, Math.min(volume, 1));
|
||||
} else {
|
||||
const increment = 0.05;
|
||||
// 直接设置
|
||||
if (typeof actions === "number") {
|
||||
actions = Math.max(0, Math.min(actions, 1));
|
||||
}
|
||||
// 分类调节
|
||||
else if (actions === "up" || actions === "down") {
|
||||
statusStore.playVolume = Math.max(
|
||||
0,
|
||||
Math.min(statusStore.playVolume + (volume === "up" ? increment : -increment), 1),
|
||||
Math.min(statusStore.playVolume + (actions === "up" ? increment : -increment), 1),
|
||||
);
|
||||
}
|
||||
// 鼠标滚轮
|
||||
else {
|
||||
const deltaY = actions.deltaY;
|
||||
const volumeChange = deltaY > 0 ? -increment : increment;
|
||||
statusStore.playVolume = Math.max(0, Math.min(statusStore.playVolume + volumeChange, 1));
|
||||
}
|
||||
// 调整音量
|
||||
this.player.volume(statusStore.playVolume);
|
||||
}
|
||||
@@ -822,11 +1080,19 @@ class Player {
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
// 获取配置
|
||||
const { showTip, scrobble, play } = options;
|
||||
// 打卡
|
||||
if (scrobble) this.scrobbleSong();
|
||||
const { showTip, play } = options;
|
||||
|
||||
// 处理随机播放模式
|
||||
let processedData = cloneDeep(data);
|
||||
if (statusStore.playSongMode === "shuffle") {
|
||||
// 保存原始播放列表
|
||||
await dataStore.setOriginalPlayList(cloneDeep(data));
|
||||
// 随机排序
|
||||
processedData = this.shuffleArray(processedData);
|
||||
}
|
||||
|
||||
// 更新列表
|
||||
await dataStore.setPlayList(cloneDeep(data));
|
||||
await dataStore.setPlayList(processedData);
|
||||
// 关闭特殊模式
|
||||
if (statusStore.playHeartbeatMode) this.toggleHeartMode(false);
|
||||
if (statusStore.personalFmMode) statusStore.personalFmMode = false;
|
||||
@@ -836,15 +1102,17 @@ class Player {
|
||||
if (musicStore.playSong.id === song.id) {
|
||||
if (play) await this.play();
|
||||
} else {
|
||||
// 查找索引
|
||||
statusStore.playIndex = data.findIndex((item) => item.id === song.id);
|
||||
// 查找索引(在处理后的列表中查找)
|
||||
statusStore.playIndex = processedData.findIndex((item) => item.id === song.id);
|
||||
// 播放
|
||||
await this.pause(false);
|
||||
await this.initPlayer();
|
||||
}
|
||||
} else {
|
||||
statusStore.playIndex =
|
||||
statusStore.playSongMode === "shuffle" ? Math.floor(Math.random() * data.length) : 0;
|
||||
statusStore.playSongMode === "shuffle"
|
||||
? Math.floor(Math.random() * processedData.length)
|
||||
: 0;
|
||||
// 播放
|
||||
await this.pause(false);
|
||||
await this.initPlayer();
|
||||
@@ -873,15 +1141,16 @@ class Player {
|
||||
// 尝试添加
|
||||
const songIndex = await dataStore.setNextPlaySong(song, statusStore.playIndex);
|
||||
// 播放歌曲
|
||||
if (!songIndex) return;
|
||||
if (play) this.togglePlayIndex(songIndex);
|
||||
if (songIndex < 0) return;
|
||||
if (play) this.togglePlayIndex(songIndex, true);
|
||||
else window.$message.success("已添加至下一首播放");
|
||||
}
|
||||
/**
|
||||
* 切换播放索引
|
||||
* @param index 播放索引
|
||||
* @param play 是否立即播放
|
||||
*/
|
||||
async togglePlayIndex(index: number) {
|
||||
async togglePlayIndex(index: number, play: boolean = false) {
|
||||
const dataStore = useDataStore();
|
||||
const statusStore = useStatusStore();
|
||||
// 获取数据
|
||||
@@ -889,7 +1158,7 @@ class Player {
|
||||
// 若超出播放列表
|
||||
if (index >= playList.length) return;
|
||||
// 相同
|
||||
if (statusStore.playIndex === index) {
|
||||
if (!play && statusStore.playIndex === index) {
|
||||
this.play();
|
||||
return;
|
||||
}
|
||||
@@ -915,6 +1184,8 @@ class Player {
|
||||
this.cleanPlayList();
|
||||
return;
|
||||
}
|
||||
// 是否为当前播放歌曲
|
||||
const isCurrentPlay = statusStore.playIndex === index;
|
||||
// 深拷贝,防止影响原数据
|
||||
const newPlaylist = cloneDeep(playList);
|
||||
// 若将移除最后一首
|
||||
@@ -929,7 +1200,7 @@ class Player {
|
||||
newPlaylist.splice(index, 1);
|
||||
dataStore.setPlayList(newPlaylist);
|
||||
// 若为当前播放
|
||||
if (statusStore.playIndex === index) {
|
||||
if (isCurrentPlay) {
|
||||
this.initPlayer(statusStore.playStatus);
|
||||
}
|
||||
}
|
||||
@@ -949,6 +1220,7 @@ class Player {
|
||||
showFullPlayer: false,
|
||||
playHeartbeatMode: false,
|
||||
personalFmMode: false,
|
||||
playIndex: -1,
|
||||
});
|
||||
musicStore.resetMusicData();
|
||||
dataStore.setPlayList([]);
|
||||
@@ -1028,8 +1300,12 @@ class Player {
|
||||
window.$message.success("已退出心动模式");
|
||||
return;
|
||||
}
|
||||
if (!isLogin()) {
|
||||
if (isLogin() !== 1) {
|
||||
if (isLogin() === 0) {
|
||||
openUserLogin(true);
|
||||
} else {
|
||||
window.$message.warning("该登录模式暂不支持该操作");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (statusStore.playHeartbeatMode) {
|
||||
@@ -1070,29 +1346,6 @@ class Player {
|
||||
this.message?.destroy();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 听歌打卡
|
||||
*/
|
||||
async scrobbleSong() {
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
try {
|
||||
if (!isLogin()) return;
|
||||
if (!settingStore.scrobbleSong) return;
|
||||
// 获取所需数据
|
||||
const playSongData = this.getPlaySongData();
|
||||
if (!playSongData) return;
|
||||
const { id, name } = playSongData;
|
||||
const sourceid = musicStore.playPlaylistId;
|
||||
const time = statusStore.duration;
|
||||
// 网易云打卡
|
||||
console.log("打卡:", id, name, sourceid, time);
|
||||
await scrobble(id, sourceid, time);
|
||||
} catch (error) {
|
||||
console.error("Failed to scrobble song:", error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 初始化私人FM
|
||||
* @param playNext 是否播放下一首
|
||||
|
||||
@@ -83,13 +83,15 @@ server.interceptors.response.use(
|
||||
// 处理其他状态码或错误条件
|
||||
console.error("未处理的错误:", error.message);
|
||||
}
|
||||
window.$notification.error({
|
||||
title: "请求错误",
|
||||
description: `状态码: ${response?.status || ""}`,
|
||||
content: (response && (response.data as { message?: string }).message) || error.message,
|
||||
meta: "若持续发生,可尝试软件热重载",
|
||||
duration: 5000,
|
||||
});
|
||||
// window.$notification.error({
|
||||
// title: "请求错误",
|
||||
// description: `状态码: ${response?.status || ""}`,
|
||||
// content: (response && (response.data as { message?: string }).message) || error.message,
|
||||
// meta: "若持续发生,可尝试软件热重载",
|
||||
// duration: 5000,
|
||||
// });
|
||||
// 控制台输出
|
||||
window.$message.warning("请求出错,若持续发生,可尝试软件热重载");
|
||||
// 返回错误
|
||||
return Promise.reject(error);
|
||||
},
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
<!-- 路由 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive>
|
||||
<KeepAlive v-if="settingStore.useKeepAlive">
|
||||
<component
|
||||
ref="componentRef"
|
||||
:is="Component"
|
||||
@@ -138,6 +138,14 @@
|
||||
@scroll="listScroll"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
v-else
|
||||
ref="componentRef"
|
||||
:is="Component"
|
||||
:id="artistId"
|
||||
class="router-view"
|
||||
@scroll="listScroll"
|
||||
/>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
<!-- 路由 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive>
|
||||
<KeepAlive v-if="settingStore.useKeepAlive">
|
||||
<component :is="Component" class="router-view" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" class="router-view" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
</n-grid>
|
||||
<!-- 公共推荐 -->
|
||||
<div v-for="(item, index) in recData" :key="index" class="rec-public">
|
||||
<n-flex class="title" align="center" justify="space-between">
|
||||
<n-flex
|
||||
class="title"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
@click="router.push({ path: item.path ?? undefined })"
|
||||
>
|
||||
<n-h3 prefix="bar">
|
||||
<n-text>{{ item.name }}</n-text>
|
||||
<SvgIcon v-if="item.path" :size="26" name="Right" />
|
||||
@@ -194,7 +199,10 @@ const getAllRecData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getAllRecData();
|
||||
onActivated(getAllRecData);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -31,9 +31,10 @@
|
||||
<!-- 路由 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive>
|
||||
<KeepAlive v-if="settingStore.useKeepAlive">
|
||||
<component :is="Component" class="router-view" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" class="router-view" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,11 @@
|
||||
<n-flex class="meta">
|
||||
<div class="item">
|
||||
<SvgIcon name="Person" :depth="3" />
|
||||
<div v-if="Array.isArray(albumDetailData.artists)" class="artists text-hidden">
|
||||
<div
|
||||
v-if="Array.isArray(albumDetailData.artists)"
|
||||
class="artists text-hidden"
|
||||
@click="openJumpArtist(albumDetailData.artists)"
|
||||
>
|
||||
<n-text
|
||||
v-for="(ar, arIndex) in albumDetailData.artists"
|
||||
:key="arIndex"
|
||||
@@ -58,7 +62,11 @@
|
||||
{{ ar.name || "未知艺术家" }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div v-else class="artists text-hidden">
|
||||
<div
|
||||
v-else
|
||||
class="artists text-hidden"
|
||||
@click="openJumpArtist(albumDetailData.artists || '')"
|
||||
>
|
||||
<n-text class="ar"> {{ albumDetailData.artists || "未知艺术家" }} </n-text>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,6 +177,7 @@ import { renderToolbar } from "@/utils/meta";
|
||||
import { useDataStore, useStatusStore } from "@/stores";
|
||||
import { debounce } from "lodash-es";
|
||||
import { formatTimestamp } from "@/utils/time";
|
||||
import { openJumpArtist } from "@/utils/modal";
|
||||
import player from "@/utils/player";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -178,7 +178,7 @@ import { debounce, isObject } from "lodash-es";
|
||||
import { useDataStore, useStatusStore } from "@/stores";
|
||||
import { openBatchList, openUpdatePlaylist } from "@/utils/modal";
|
||||
import { formatTimestamp } from "@/utils/time";
|
||||
import { isLogin } from "@/utils/auth";
|
||||
import { isLogin, updateUserLikePlaylist } from "@/utils/auth";
|
||||
import player from "@/utils/player";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -408,9 +408,32 @@ onDeactivated(() => loadingMsgShow(false));
|
||||
onUnmounted(() => loadingMsgShow(false));
|
||||
|
||||
onMounted(async () => {
|
||||
// 首先确保用户歌单数据已加载
|
||||
if (!dataStore.userLikeData.playlists?.length) {
|
||||
try {
|
||||
await updateUserLikePlaylist();
|
||||
} catch (error) {
|
||||
console.error("Failed to update user playlist data:", error);
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取我喜欢的音乐歌单ID
|
||||
const likedPlaylistId = dataStore.userLikeData.playlists?.[0]?.id;
|
||||
if (likedPlaylistId) {
|
||||
getPlaylistDetail(likedPlaylistId);
|
||||
} else {
|
||||
// 如果没有找到我喜欢的音乐歌单,尝试从缓存获取
|
||||
const data: any = await dataStore.getUserLikePlaylist();
|
||||
const id = data?.detail?.id;
|
||||
if (id) getPlaylistDetail(id);
|
||||
if (id) {
|
||||
getPlaylistDetail(id);
|
||||
} else {
|
||||
loading.value = false;
|
||||
window.$message.error("无法获取我喜欢的音乐歌单");
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -215,7 +215,12 @@
|
||||
import type { CoverType, SongType } from "@/types/main";
|
||||
import type { DropdownOption, MessageReactive } from "naive-ui";
|
||||
import { songDetail } from "@/api/song";
|
||||
import { playlistDetail, playlistAllSongs, deletePlaylist } from "@/api/playlist";
|
||||
import {
|
||||
playlistDetail,
|
||||
playlistAllSongs,
|
||||
deletePlaylist,
|
||||
updatePlaylistPrivacy,
|
||||
} from "@/api/playlist";
|
||||
import { formatCoverList, formatSongsList } from "@/utils/format";
|
||||
import { coverLoaded, formatNumber, fuzzySearch, renderIcon } from "@/utils/helper";
|
||||
import { renderToolbar } from "@/utils/meta";
|
||||
@@ -283,6 +288,7 @@ const moreOptions = computed<DropdownOption[]>(() => [
|
||||
label: "公开隐私歌单",
|
||||
key: "privacy",
|
||||
show: playlistDetailData.value?.privacy === 10,
|
||||
props: { onClick: openPrivacy },
|
||||
icon: renderIcon("ListLockOpen"),
|
||||
},
|
||||
{
|
||||
@@ -355,21 +361,24 @@ const handleLocalPlaylist = (id: number) => {
|
||||
|
||||
// 获取在线歌单
|
||||
const handleOnlinePlaylist = async (id: number, getList: boolean, refresh: boolean) => {
|
||||
console.log(id, getList, refresh);
|
||||
|
||||
// 获取歌单详情
|
||||
const detail = await playlistDetail(id);
|
||||
playlistDetailData.value = formatCoverList(detail.playlist)[0];
|
||||
const count = playlistDetailData.value?.count || 0;
|
||||
// 不需要获取列表或无歌曲
|
||||
if (!getList || playlistDetailData.value.count === 0) {
|
||||
if (!getList || count === 0) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
// 如果已登录且歌曲数量少于 800,直接加载所有歌曲
|
||||
if (isLogin() === 1 && (playlistDetailData.value?.count as number) < 800) {
|
||||
const ids: number[] = detail.privileges.map((song: any) => song.id as number);
|
||||
if (isLogin() === 1 && count === detail.privileges?.length && count < 800) {
|
||||
const ids = detail.privileges.map((song: any) => song.id as number);
|
||||
const result = await songDetail(ids);
|
||||
playlistData.value = formatSongsList(result.songs);
|
||||
} else {
|
||||
await getPlaylistAllSongs(id, playlistDetailData.value.count || 0, refresh);
|
||||
await getPlaylistAllSongs(id, count, refresh);
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
@@ -424,7 +433,6 @@ const loadingMsgShow = (show: boolean = true, count?: number) => {
|
||||
closable: true,
|
||||
});
|
||||
} else {
|
||||
loading.value = false;
|
||||
loadingMsg.value?.destroy();
|
||||
loadingMsg.value = null;
|
||||
}
|
||||
@@ -481,6 +489,23 @@ const updatePlaylist = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// 公开隐私歌单
|
||||
const openPrivacy = async () => {
|
||||
if (playlistDetailData.value?.privacy !== 10) return;
|
||||
window.$dialog.warning({
|
||||
title: "公开隐私歌单",
|
||||
content: "确认公开这个歌单?该操作无法撤销!",
|
||||
positiveText: "公开",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
const result = await updatePlaylistPrivacy(playlistId.value);
|
||||
if (result.code !== 200) return;
|
||||
if (playlistDetailData.value) playlistDetailData.value.privacy = 0;
|
||||
window.$message.success("歌单公开成功");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onBeforeRouteUpdate((to) => {
|
||||
const id = Number(to.query.id as string);
|
||||
if (id) {
|
||||
|
||||
@@ -82,9 +82,10 @@
|
||||
<!-- 路由 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive>
|
||||
<KeepAlive v-if="settingStore.useKeepAlive">
|
||||
<component :is="Component" :data="listData" :loading="loading" class="router-view" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" :data="listData" :loading="loading" class="router-view" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<!-- 目录管理 -->
|
||||
@@ -105,7 +106,14 @@
|
||||
<template #prefix>
|
||||
<SvgIcon :size="20" name="FolderMusic" />
|
||||
</template>
|
||||
<n-thing :title="defaultMusicPath" description="系统默认音乐文件夹,无法更改" />
|
||||
<template #suffix>
|
||||
<n-switch
|
||||
v-model:value="settingStore.showDefaultLocalPath"
|
||||
:round="false"
|
||||
class="set"
|
||||
/>
|
||||
</template>
|
||||
<n-thing :title="defaultMusicPath" description="系统默认音乐文件夹" />
|
||||
</n-list-item>
|
||||
<n-list-item v-for="(item, index) in settingStore.localFilesPath" :key="index">
|
||||
<template #prefix>
|
||||
@@ -175,7 +183,10 @@ const listData = computed<SongType[]>(() => {
|
||||
// 获取音乐文件夹
|
||||
const getMusicFolder = async (): Promise<string[]> => {
|
||||
defaultMusicPath.value = await window.electron.ipcRenderer.invoke("get-default-dir", "music");
|
||||
return [defaultMusicPath.value, ...settingStore.localFilesPath];
|
||||
return [
|
||||
settingStore.showDefaultLocalPath ? defaultMusicPath.value : "",
|
||||
...settingStore.localFilesPath,
|
||||
];
|
||||
};
|
||||
|
||||
// 全部音乐大小
|
||||
@@ -261,7 +272,7 @@ localEventBus.on(() => getAllLocalMusic());
|
||||
|
||||
// 本地目录变化
|
||||
watch(
|
||||
() => settingStore.localFilesPath,
|
||||
() => [settingStore.localFilesPath, settingStore.showDefaultLocalPath],
|
||||
async () => await getAllLocalMusic(),
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
@@ -16,16 +16,17 @@
|
||||
<!-- 路由 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive>
|
||||
<KeepAlive v-if="settingStore.useKeepAlive">
|
||||
<component :is="Component" :keyword="searchKeyword" class="router-view" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" :keyword="searchKeyword" class="router-view" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/stores';
|
||||
import { useSettingStore } from "@/stores";
|
||||
|
||||
const router = useRouter();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"electron/main/utils.ts",
|
||||
"electron/main/index.d.ts",
|
||||
"electron/preload/index.d.ts",
|
||||
"electron/preload/index.ts"
|
||||
"electron/preload/index.ts",
|
||||
"dist/lastfm.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
@@ -5,15 +5,20 @@
|
||||
"src/**/*",
|
||||
"src/**/*.vue",
|
||||
"electron/main/index.d.ts",
|
||||
"electron/preload/index.d.ts"
|
||||
"electron/preload/index.d.ts",
|
||||
"dist/lastfm.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "bundler",
|
||||
"maxNodeModuleJsDepth": 2,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"types": ["node", "electron", "electron-vite/node", "./auto-imports.d.ts", "./components.d.ts"]
|
||||
"types": ["node", "electron", "electron-vite/node", "./auto-imports.d.ts", "./components.d.ts"],
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"target": "es2022"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +365,7 @@
|
||||
}
|
||||
case "font-size-reduce": {
|
||||
let fontSize = options.fontSize;
|
||||
if (fontSize > 20) {
|
||||
if (fontSize > 10) {
|
||||
fontSize--;
|
||||
this.changeOptions({ ...options, fontSize });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user