Compare commits
209 Commits
v3.0.0-alp
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b3f321a3d | ||
|
|
58e3c6e21c | ||
|
|
68756f2502 | ||
|
|
cea9f7b025 | ||
|
|
6b653bc5e8 | ||
|
|
1b2985892b | ||
|
|
eb0094c189 | ||
|
|
5edbd66398 | ||
|
|
4e731e976c | ||
|
|
bd6e23435e | ||
|
|
8ec91e1392 | ||
|
|
8f69b56378 | ||
|
|
ccd0c6bdeb | ||
|
|
1c9109af73 | ||
|
|
a556a2e102 | ||
|
|
9fcd5b0e98 | ||
|
|
a1be1e16b2 | ||
|
|
fc7fc08a6e | ||
|
|
d74515142d | ||
|
|
0aae10e8a0 | ||
|
|
242c6f2ca7 | ||
|
|
413b74bf9a | ||
|
|
aad5c9461f | ||
|
|
23cccbb660 | ||
|
|
ba7666526c | ||
|
|
82ea11b6fa | ||
|
|
9771cea25f | ||
|
|
6f4afd85d7 | ||
|
|
fe82fa623b | ||
|
|
28b521a192 | ||
|
|
bdfbb4d2b1 | ||
|
|
6937a93d17 | ||
|
|
2bc4fca4dd | ||
|
|
92428593e2 | ||
|
|
95e3f74301 | ||
|
|
9145eb034b | ||
|
|
829dc3b591 | ||
|
|
c4c6178b9e | ||
|
|
9967bcb102 | ||
|
|
feb186db65 | ||
|
|
84999338e6 | ||
|
|
7943b56dbb | ||
|
|
6a1657cf20 | ||
|
|
0eaf32b8a7 | ||
|
|
424c1e76f0 | ||
|
|
e854dbb816 | ||
|
|
5e37678348 | ||
|
|
9fcb5ca633 | ||
|
|
55aedc33c3 | ||
|
|
1d4aa43185 | ||
|
|
8014eedd72 | ||
|
|
4f018775ba | ||
|
|
4e7bd95366 | ||
|
|
3035056ded | ||
|
|
df42292f0a | ||
|
|
ca2ff2a890 | ||
|
|
bd06c3f8f4 | ||
|
|
a44cd904dc | ||
|
|
09838e632d | ||
|
|
7f77ccb18a | ||
|
|
f3f05aa72b | ||
|
|
6beb9c78e1 | ||
|
|
2efc0a5228 | ||
|
|
bb0cae2be1 | ||
|
|
22a6fdd0c4 | ||
|
|
d257a441c2 | ||
|
|
49c81b6afd | ||
|
|
045497a7c8 | ||
|
|
521970b8a1 | ||
|
|
a1e9dd7746 | ||
|
|
61d2467916 | ||
|
|
f3adf773a7 | ||
|
|
ad807cf478 | ||
|
|
a1194232af | ||
|
|
9fc1746495 | ||
|
|
103bf7948d | ||
|
|
12c2dea226 | ||
|
|
7fbe8ac6a5 | ||
|
|
ef6868fd40 | ||
|
|
f238845f9b | ||
|
|
d4d16b71ae | ||
|
|
9baf571478 | ||
|
|
6370ac77e6 | ||
|
|
2bf3f2a5c6 | ||
|
|
e8bf42891e | ||
|
|
c21f970b86 | ||
|
|
56045cd338 | ||
|
|
dbf6121098 | ||
|
|
103b2fe923 | ||
|
|
c70a266b22 | ||
|
|
a7b34ca4b0 | ||
|
|
caa6fa838f | ||
|
|
e98140d0ac | ||
|
|
ac47b38773 | ||
|
|
fef6e4f6fa | ||
|
|
0215e5944d | ||
|
|
1b6ad163e3 | ||
|
|
080de35545 | ||
|
|
163423222d | ||
|
|
fc49b7ad00 | ||
|
|
b83cd99d8c | ||
|
|
74a5de96c0 | ||
|
|
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 | ||
|
|
a8f01d5728 | ||
|
|
3a6f8e7462 | ||
|
|
57325bfc46 | ||
|
|
a3e8931460 | ||
|
|
2e0f448f66 | ||
|
|
44cfbbf4e0 | ||
|
|
963343c39e | ||
|
|
a697799f48 | ||
|
|
7c59cc2ccb | ||
|
|
11acbcf7eb | ||
|
|
c35ede2158 | ||
|
|
dc480459eb | ||
|
|
c100bfed8d | ||
|
|
2b48713565 | ||
|
|
528f9b0aa6 | ||
|
|
0e0bde89bb | ||
|
|
acf60b8b75 | ||
|
|
3b5338d582 | ||
|
|
0c82d6a096 | ||
|
|
50374e173e |
@@ -1,6 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
out
|
|
||||||
.gitignore
|
|
||||||
auto-imports.d.ts
|
|
||||||
components.d.ts
|
|
||||||
@@ -3,11 +3,14 @@
|
|||||||
"Component": true,
|
"Component": true,
|
||||||
"ComponentPublicInstance": true,
|
"ComponentPublicInstance": true,
|
||||||
"ComputedRef": true,
|
"ComputedRef": true,
|
||||||
|
"DirectiveBinding": true,
|
||||||
"EffectScope": true,
|
"EffectScope": true,
|
||||||
"ExtractDefaultPropTypes": true,
|
"ExtractDefaultPropTypes": true,
|
||||||
"ExtractPropTypes": true,
|
"ExtractPropTypes": true,
|
||||||
"ExtractPublicPropTypes": true,
|
"ExtractPublicPropTypes": true,
|
||||||
"InjectionKey": true,
|
"InjectionKey": true,
|
||||||
|
"MaybeRef": true,
|
||||||
|
"MaybeRefOrGetter": true,
|
||||||
"PropType": true,
|
"PropType": true,
|
||||||
"Ref": true,
|
"Ref": true,
|
||||||
"VNode": true,
|
"VNode": true,
|
||||||
@@ -71,6 +74,7 @@
|
|||||||
"onStartTyping": true,
|
"onStartTyping": true,
|
||||||
"onUnmounted": true,
|
"onUnmounted": true,
|
||||||
"onUpdated": true,
|
"onUpdated": true,
|
||||||
|
"onWatcherCleanup": true,
|
||||||
"pausableWatch": true,
|
"pausableWatch": true,
|
||||||
"provide": true,
|
"provide": true,
|
||||||
"provideLocal": true,
|
"provideLocal": true,
|
||||||
@@ -180,6 +184,7 @@
|
|||||||
"useFullscreen": true,
|
"useFullscreen": true,
|
||||||
"useGamepad": true,
|
"useGamepad": true,
|
||||||
"useGeolocation": true,
|
"useGeolocation": true,
|
||||||
|
"useId": true,
|
||||||
"useIdle": true,
|
"useIdle": true,
|
||||||
"useImage": true,
|
"useImage": true,
|
||||||
"useInfiniteScroll": true,
|
"useInfiniteScroll": true,
|
||||||
@@ -198,6 +203,7 @@
|
|||||||
"useMemoize": true,
|
"useMemoize": true,
|
||||||
"useMemory": true,
|
"useMemory": true,
|
||||||
"useMessage": true,
|
"useMessage": true,
|
||||||
|
"useModel": true,
|
||||||
"useMounted": true,
|
"useMounted": true,
|
||||||
"useMouse": true,
|
"useMouse": true,
|
||||||
"useMouseInElement": true,
|
"useMouseInElement": true,
|
||||||
@@ -246,6 +252,7 @@
|
|||||||
"useStyleTag": true,
|
"useStyleTag": true,
|
||||||
"useSupported": true,
|
"useSupported": true,
|
||||||
"useSwipe": true,
|
"useSwipe": true,
|
||||||
|
"useTemplateRef": true,
|
||||||
"useTemplateRefsList": true,
|
"useTemplateRefsList": true,
|
||||||
"useTextDirection": true,
|
"useTextDirection": true,
|
||||||
"useTextSelection": true,
|
"useTextSelection": true,
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2021: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:vue/vue3-essential",
|
|
||||||
],
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
files: [".eslintrc.{js,cjs}"],
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: "script",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
plugins: ["@typescript-eslint", "vue"],
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"vue/multi-word-component-names": "off",
|
|
||||||
},
|
|
||||||
global: {
|
|
||||||
h: "readonly",
|
|
||||||
ref: "readonly",
|
|
||||||
computed: "readonly",
|
|
||||||
watch: "readonly",
|
|
||||||
watchEffect: "readonly",
|
|
||||||
onBeforeMount: "readonly",
|
|
||||||
onBeforeUnmount: "readonly",
|
|
||||||
onBeforeUpdate: "readonly",
|
|
||||||
reactive: "readonly",
|
|
||||||
onMounted: "readonly",
|
|
||||||
onUnmounted: "readonly",
|
|
||||||
onActivated: "readonly",
|
|
||||||
onDeactivated: "readonly",
|
|
||||||
onRenderTracked: "readonly",
|
|
||||||
onRenderTriggered: "readonly",
|
|
||||||
onServerPrefetch: "readonly",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
62
.github/workflows/build.yml
vendored
@@ -1,62 +0,0 @@
|
|||||||
# Dev 分支推送部署预览
|
|
||||||
## 仅部署 Win 端
|
|
||||||
name: Build Dev
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Build and electron app
|
|
||||||
runs-on: windows-latest
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
run: npm run build:win
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
|
||||||
# 清理不必要的构建产物
|
|
||||||
- name: Cleanup Artifacts
|
|
||||||
run: |
|
|
||||||
npx rimraf "dist/!(*.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')
|
|
||||||
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
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
|
||||||
67
.github/workflows/dev.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Dev 分支推送部署预览
|
||||||
|
## 部署 Windows x64 和 ARM64 版本
|
||||||
|
name: Build Dev
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 22.x
|
||||||
|
PNPM_VERSION: 8
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Windows x64 架构
|
||||||
|
build-win:
|
||||||
|
name: Build Electron App for Windows
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# 检出 Git 仓库
|
||||||
|
- name: Check out git repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
# 设置 pnpm 版本
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: ${{ env.PNPM_VERSION }}
|
||||||
|
# 安装 Node.js
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: "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 (x64)
|
||||||
|
- name: Build Electron App for Windows x64
|
||||||
|
# 仅 x64
|
||||||
|
run: pnpm run build:win -- --arch=x64
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# 清理不必要的构建产物(保留 .exe 和 .blockmap 文件)
|
||||||
|
- name: Cleanup Artifacts
|
||||||
|
run: npx del-cli "dist/**/*.yaml" "dist/**/*.yml"
|
||||||
|
# 上传构建产物(仅上传 x64 架构的 .exe 文件)
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SPlayer-dev
|
||||||
|
path: |
|
||||||
|
dist/**.exe
|
||||||
10
.github/workflows/docker.yml
vendored
@@ -3,6 +3,7 @@ name: Publish Docker image
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push_to_registry:
|
push_to_registry:
|
||||||
@@ -15,6 +16,12 @@ jobs:
|
|||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -41,6 +48,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
253
.github/workflows/release.yml
vendored
@@ -1,154 +1,169 @@
|
|||||||
# Release 发行版本部署
|
name: Build & Release
|
||||||
## 多端部署
|
|
||||||
name: Build Release
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v* # 只在 tag v* 时触发
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 22.x
|
||||||
|
PNPM_VERSION: 8
|
||||||
|
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Windows
|
# ===================================================================
|
||||||
build-windows:
|
# 并行构建所有平台和架构
|
||||||
name: Build for Windows
|
# ===================================================================
|
||||||
runs-on: windows-latest
|
build:
|
||||||
timeout-minutes: 30
|
name: Build on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
# 矩阵策略
|
||||||
|
# 即使一个矩阵任务失败,其他任务也会继续运行
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
# 开始步骤
|
||||||
steps:
|
steps:
|
||||||
# 检出 Git 仓库
|
# 检出 Git 仓库
|
||||||
- name: Check out git repository
|
- name: Check out git repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
# 安装 Node.js
|
# 设置 pnpm 版本
|
||||||
- name: Install Node.js
|
- name: Setup pnpm
|
||||||
uses: actions/setup-node@v4
|
uses: pnpm/action-setup@v3
|
||||||
with:
|
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: |
|
run: |
|
||||||
if (-not (Test-Path .env)) {
|
if (-not (Test-Path .env)) {
|
||||||
Copy-Item .env.example .env
|
Copy-Item .env.example .env
|
||||||
} else {
|
} else {
|
||||||
Write-Host ".env file already exists. Skipping the copy step."
|
Write-Host ".env file already exists. Skipping the copy step."
|
||||||
}
|
}
|
||||||
# 安装项目依赖
|
- name: Copy .env file on macOS & Linux
|
||||||
- name: Install Dependencies
|
if: runner.os == 'macOS' || runner.os == 'Linux'
|
||||||
run: npm install
|
|
||||||
# 构建 Electron App
|
|
||||||
- name: Build Electron App for Windows
|
|
||||||
run: npm run build:win || true
|
|
||||||
shell: bash
|
|
||||||
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: true
|
|
||||||
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
|
|
||||||
run: |
|
run: |
|
||||||
if [ ! -f .env ]; then
|
if [ ! -f .env ]; then
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
else
|
else
|
||||||
echo ".env file already exists. Skipping the copy step."
|
echo ".env file already exists. Skipping the copy step."
|
||||||
fi
|
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: true
|
|
||||||
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 软件源
|
# 更新 Ubuntu 软件源
|
||||||
- name: Ubuntu Update with sudo
|
- name: Ubuntu Update with sudo
|
||||||
|
if: runner.os == 'Linux'
|
||||||
run: sudo apt-get update
|
run: sudo apt-get update
|
||||||
# 复制环境变量文件
|
# 安装依赖
|
||||||
- name: Copy .env.example
|
- name: Install RPM & Pacman
|
||||||
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
if [ ! -f .env ]; then
|
sudo apt-get install --no-install-recommends -y rpm &&
|
||||||
cp .env.example .env
|
sudo apt-get install --no-install-recommends -y libarchive-tools &&
|
||||||
else
|
sudo apt-get install --no-install-recommends -y libopenjp2-tools
|
||||||
echo ".env file already exists. Skipping the copy step."
|
# 安裝 Snapcraft
|
||||||
fi
|
- name: Install Snapcraft
|
||||||
# 安装项目依赖
|
if: runner.os == 'Linux'
|
||||||
- name: Install Dependencies
|
uses: samuelmeuli/action-snapcraft@v2
|
||||||
run: npm install
|
|
||||||
# 构建 Electron App
|
# 构建 Electron App
|
||||||
- name: Build Electron App for Linux
|
- name: Build Windows x64 & ARM64 App
|
||||||
run: npm run build:linux || true
|
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
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# 上传构建产物
|
- name: Build Linux x64 & ARM64 App
|
||||||
- name: Upload Linux artifact
|
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
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: SPlarer-Linux
|
name: SPlayer-${{ runner.os }}
|
||||||
if-no-files-found: ignore
|
|
||||||
path: dist/*.*
|
path: dist/*.*
|
||||||
# 创建 GitHub Release
|
|
||||||
- name: Release
|
# ===================================================================
|
||||||
uses: softprops/action-gh-release@v2
|
# 收集并发布 Release
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
# ===================================================================
|
||||||
|
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:
|
with:
|
||||||
draft: true
|
path: artifacts
|
||||||
files: dist/*.*
|
# 创建 GitHub Release 并上传所有产物
|
||||||
env:
|
- name: Create GitHub Release and Upload Assets
|
||||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
uses: softprops/action-gh-release@v2
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# 自动生成 Release 说明
|
||||||
|
generate_release_notes: true
|
||||||
|
# 发布为草稿
|
||||||
|
draft: false
|
||||||
|
# 发布为预发布
|
||||||
|
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
@@ -28,3 +28,5 @@ components.d.ts
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
.env.development
|
||||||
|
.env.production
|
||||||
|
|||||||
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
|
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
@@ -1,6 +1,12 @@
|
|||||||
# SPlayer
|
# 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)
|
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
|
||||||
|
|
||||||
|
## Snap Store
|
||||||
|
|
||||||
|
[](https://snapcraft.io/splayer)
|
||||||
|
|
||||||
## ⚙️ Docker 部署
|
## ⚙️ Docker 部署
|
||||||
|
|
||||||
> 安装及配置 `Docker` 将不在此处说明,请自行解决
|
> 安装及配置 `Docker` 将不在此处说明,请自行解决
|
||||||
|
|||||||
315
auto-eslint.mjs
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
export default {
|
||||||
|
"globals": {
|
||||||
|
"Component": true,
|
||||||
|
"ComponentPublicInstance": true,
|
||||||
|
"ComputedRef": true,
|
||||||
|
"DirectiveBinding": true,
|
||||||
|
"EffectScope": true,
|
||||||
|
"ExtractDefaultPropTypes": true,
|
||||||
|
"ExtractPropTypes": true,
|
||||||
|
"ExtractPublicPropTypes": true,
|
||||||
|
"InjectionKey": true,
|
||||||
|
"MaybeRef": true,
|
||||||
|
"MaybeRefOrGetter": true,
|
||||||
|
"PropType": true,
|
||||||
|
"Ref": true,
|
||||||
|
"ShallowRef": true,
|
||||||
|
"Slot": true,
|
||||||
|
"Slots": true,
|
||||||
|
"VNode": true,
|
||||||
|
"WritableComputedRef": true,
|
||||||
|
"asyncComputed": true,
|
||||||
|
"autoResetRef": true,
|
||||||
|
"computed": true,
|
||||||
|
"computedAsync": true,
|
||||||
|
"computedEager": true,
|
||||||
|
"computedInject": true,
|
||||||
|
"computedWithControl": true,
|
||||||
|
"controlledComputed": true,
|
||||||
|
"controlledRef": true,
|
||||||
|
"createApp": true,
|
||||||
|
"createEventHook": true,
|
||||||
|
"createGlobalState": true,
|
||||||
|
"createInjectionState": true,
|
||||||
|
"createReactiveFn": true,
|
||||||
|
"createRef": true,
|
||||||
|
"createReusableTemplate": true,
|
||||||
|
"createSharedComposable": true,
|
||||||
|
"createTemplatePromise": true,
|
||||||
|
"createUnrefFn": true,
|
||||||
|
"customRef": true,
|
||||||
|
"debouncedRef": true,
|
||||||
|
"debouncedWatch": true,
|
||||||
|
"defineAsyncComponent": true,
|
||||||
|
"defineComponent": true,
|
||||||
|
"eagerComputed": true,
|
||||||
|
"effectScope": true,
|
||||||
|
"extendRef": true,
|
||||||
|
"getCurrentInstance": true,
|
||||||
|
"getCurrentScope": true,
|
||||||
|
"getCurrentWatcher": true,
|
||||||
|
"h": true,
|
||||||
|
"ignorableWatch": true,
|
||||||
|
"inject": true,
|
||||||
|
"injectLocal": true,
|
||||||
|
"isDefined": true,
|
||||||
|
"isProxy": true,
|
||||||
|
"isReactive": true,
|
||||||
|
"isReadonly": true,
|
||||||
|
"isRef": true,
|
||||||
|
"isShallow": true,
|
||||||
|
"makeDestructurable": true,
|
||||||
|
"markRaw": true,
|
||||||
|
"nextTick": true,
|
||||||
|
"onActivated": true,
|
||||||
|
"onBeforeMount": true,
|
||||||
|
"onBeforeRouteLeave": true,
|
||||||
|
"onBeforeRouteUpdate": true,
|
||||||
|
"onBeforeUnmount": true,
|
||||||
|
"onBeforeUpdate": true,
|
||||||
|
"onClickOutside": true,
|
||||||
|
"onDeactivated": true,
|
||||||
|
"onElementRemoval": true,
|
||||||
|
"onErrorCaptured": true,
|
||||||
|
"onKeyStroke": true,
|
||||||
|
"onLongPress": true,
|
||||||
|
"onMounted": true,
|
||||||
|
"onRenderTracked": true,
|
||||||
|
"onRenderTriggered": true,
|
||||||
|
"onScopeDispose": true,
|
||||||
|
"onServerPrefetch": true,
|
||||||
|
"onStartTyping": true,
|
||||||
|
"onUnmounted": true,
|
||||||
|
"onUpdated": true,
|
||||||
|
"onWatcherCleanup": true,
|
||||||
|
"pausableWatch": true,
|
||||||
|
"provide": true,
|
||||||
|
"provideLocal": true,
|
||||||
|
"reactify": true,
|
||||||
|
"reactifyObject": true,
|
||||||
|
"reactive": true,
|
||||||
|
"reactiveComputed": true,
|
||||||
|
"reactiveOmit": true,
|
||||||
|
"reactivePick": true,
|
||||||
|
"readonly": true,
|
||||||
|
"ref": true,
|
||||||
|
"refAutoReset": true,
|
||||||
|
"refDebounced": true,
|
||||||
|
"refDefault": true,
|
||||||
|
"refThrottled": true,
|
||||||
|
"refWithControl": true,
|
||||||
|
"resolveComponent": true,
|
||||||
|
"resolveRef": true,
|
||||||
|
"resolveUnref": true,
|
||||||
|
"shallowReactive": true,
|
||||||
|
"shallowReadonly": true,
|
||||||
|
"shallowRef": true,
|
||||||
|
"syncRef": true,
|
||||||
|
"syncRefs": true,
|
||||||
|
"templateRef": true,
|
||||||
|
"throttledRef": true,
|
||||||
|
"throttledWatch": true,
|
||||||
|
"toRaw": true,
|
||||||
|
"toReactive": true,
|
||||||
|
"toRef": true,
|
||||||
|
"toRefs": true,
|
||||||
|
"toValue": true,
|
||||||
|
"triggerRef": true,
|
||||||
|
"tryOnBeforeMount": true,
|
||||||
|
"tryOnBeforeUnmount": true,
|
||||||
|
"tryOnMounted": true,
|
||||||
|
"tryOnScopeDispose": true,
|
||||||
|
"tryOnUnmounted": true,
|
||||||
|
"unref": true,
|
||||||
|
"unrefElement": true,
|
||||||
|
"until": true,
|
||||||
|
"useActiveElement": true,
|
||||||
|
"useAnimate": true,
|
||||||
|
"useArrayDifference": true,
|
||||||
|
"useArrayEvery": true,
|
||||||
|
"useArrayFilter": true,
|
||||||
|
"useArrayFind": true,
|
||||||
|
"useArrayFindIndex": true,
|
||||||
|
"useArrayFindLast": true,
|
||||||
|
"useArrayIncludes": true,
|
||||||
|
"useArrayJoin": true,
|
||||||
|
"useArrayMap": true,
|
||||||
|
"useArrayReduce": true,
|
||||||
|
"useArraySome": true,
|
||||||
|
"useArrayUnique": true,
|
||||||
|
"useAsyncQueue": true,
|
||||||
|
"useAsyncState": true,
|
||||||
|
"useAttrs": true,
|
||||||
|
"useBase64": true,
|
||||||
|
"useBattery": true,
|
||||||
|
"useBluetooth": true,
|
||||||
|
"useBreakpoints": true,
|
||||||
|
"useBroadcastChannel": true,
|
||||||
|
"useBrowserLocation": true,
|
||||||
|
"useCached": true,
|
||||||
|
"useClipboard": true,
|
||||||
|
"useClipboardItems": true,
|
||||||
|
"useCloned": true,
|
||||||
|
"useColorMode": true,
|
||||||
|
"useConfirmDialog": true,
|
||||||
|
"useCountdown": true,
|
||||||
|
"useCounter": true,
|
||||||
|
"useCssModule": true,
|
||||||
|
"useCssVar": true,
|
||||||
|
"useCssVars": true,
|
||||||
|
"useCurrentElement": true,
|
||||||
|
"useCycleList": true,
|
||||||
|
"useDark": true,
|
||||||
|
"useDateFormat": true,
|
||||||
|
"useDebounce": true,
|
||||||
|
"useDebounceFn": true,
|
||||||
|
"useDebouncedRefHistory": true,
|
||||||
|
"useDeviceMotion": true,
|
||||||
|
"useDeviceOrientation": true,
|
||||||
|
"useDevicePixelRatio": true,
|
||||||
|
"useDevicesList": true,
|
||||||
|
"useDialog": true,
|
||||||
|
"useDisplayMedia": true,
|
||||||
|
"useDocumentVisibility": true,
|
||||||
|
"useDraggable": true,
|
||||||
|
"useDropZone": true,
|
||||||
|
"useElementBounding": true,
|
||||||
|
"useElementByPoint": true,
|
||||||
|
"useElementHover": true,
|
||||||
|
"useElementSize": true,
|
||||||
|
"useElementVisibility": true,
|
||||||
|
"useEventBus": true,
|
||||||
|
"useEventListener": true,
|
||||||
|
"useEventSource": true,
|
||||||
|
"useEyeDropper": true,
|
||||||
|
"useFavicon": true,
|
||||||
|
"useFetch": true,
|
||||||
|
"useFileDialog": true,
|
||||||
|
"useFileSystemAccess": true,
|
||||||
|
"useFocus": true,
|
||||||
|
"useFocusWithin": true,
|
||||||
|
"useFps": true,
|
||||||
|
"useFullscreen": true,
|
||||||
|
"useGamepad": true,
|
||||||
|
"useGeolocation": true,
|
||||||
|
"useId": true,
|
||||||
|
"useIdle": true,
|
||||||
|
"useImage": true,
|
||||||
|
"useInfiniteScroll": true,
|
||||||
|
"useIntersectionObserver": true,
|
||||||
|
"useInterval": true,
|
||||||
|
"useIntervalFn": true,
|
||||||
|
"useKeyModifier": true,
|
||||||
|
"useLastChanged": true,
|
||||||
|
"useLink": true,
|
||||||
|
"useLoadingBar": true,
|
||||||
|
"useLocalStorage": true,
|
||||||
|
"useMagicKeys": true,
|
||||||
|
"useManualRefHistory": true,
|
||||||
|
"useMediaControls": true,
|
||||||
|
"useMediaQuery": true,
|
||||||
|
"useMemoize": true,
|
||||||
|
"useMemory": true,
|
||||||
|
"useMessage": true,
|
||||||
|
"useModel": true,
|
||||||
|
"useMounted": true,
|
||||||
|
"useMouse": true,
|
||||||
|
"useMouseInElement": true,
|
||||||
|
"useMousePressed": true,
|
||||||
|
"useMutationObserver": true,
|
||||||
|
"useNavigatorLanguage": true,
|
||||||
|
"useNetwork": true,
|
||||||
|
"useNotification": true,
|
||||||
|
"useNow": true,
|
||||||
|
"useObjectUrl": true,
|
||||||
|
"useOffsetPagination": true,
|
||||||
|
"useOnline": true,
|
||||||
|
"usePageLeave": true,
|
||||||
|
"useParallax": true,
|
||||||
|
"useParentElement": true,
|
||||||
|
"usePerformanceObserver": true,
|
||||||
|
"usePermission": true,
|
||||||
|
"usePointer": true,
|
||||||
|
"usePointerLock": true,
|
||||||
|
"usePointerSwipe": true,
|
||||||
|
"usePreferredColorScheme": true,
|
||||||
|
"usePreferredContrast": true,
|
||||||
|
"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,
|
||||||
|
"useScroll": true,
|
||||||
|
"useScrollLock": true,
|
||||||
|
"useSessionStorage": true,
|
||||||
|
"useShare": true,
|
||||||
|
"useSlots": true,
|
||||||
|
"useSorted": true,
|
||||||
|
"useSpeechRecognition": true,
|
||||||
|
"useSpeechSynthesis": true,
|
||||||
|
"useStepper": true,
|
||||||
|
"useStorage": true,
|
||||||
|
"useStorageAsync": true,
|
||||||
|
"useStyleTag": true,
|
||||||
|
"useSupported": true,
|
||||||
|
"useSwipe": true,
|
||||||
|
"useTemplateRef": true,
|
||||||
|
"useTemplateRefsList": true,
|
||||||
|
"useTextDirection": true,
|
||||||
|
"useTextSelection": true,
|
||||||
|
"useTextareaAutosize": true,
|
||||||
|
"useThrottle": true,
|
||||||
|
"useThrottleFn": true,
|
||||||
|
"useThrottledRefHistory": true,
|
||||||
|
"useTimeAgo": true,
|
||||||
|
"useTimeAgoIntl": true,
|
||||||
|
"useTimeout": true,
|
||||||
|
"useTimeoutFn": true,
|
||||||
|
"useTimeoutPoll": true,
|
||||||
|
"useTimestamp": true,
|
||||||
|
"useTitle": true,
|
||||||
|
"useToNumber": true,
|
||||||
|
"useToString": true,
|
||||||
|
"useToggle": true,
|
||||||
|
"useTransition": true,
|
||||||
|
"useUrlSearchParams": true,
|
||||||
|
"useUserMedia": true,
|
||||||
|
"useVModel": true,
|
||||||
|
"useVModels": true,
|
||||||
|
"useVibrate": true,
|
||||||
|
"useVirtualList": true,
|
||||||
|
"useWakeLock": true,
|
||||||
|
"useWebNotification": true,
|
||||||
|
"useWebSocket": true,
|
||||||
|
"useWebWorker": true,
|
||||||
|
"useWebWorkerFn": true,
|
||||||
|
"useWindowFocus": true,
|
||||||
|
"useWindowScroll": true,
|
||||||
|
"useWindowSize": true,
|
||||||
|
"watch": true,
|
||||||
|
"watchArray": true,
|
||||||
|
"watchAtMost": true,
|
||||||
|
"watchDebounced": true,
|
||||||
|
"watchDeep": true,
|
||||||
|
"watchEffect": true,
|
||||||
|
"watchIgnorable": true,
|
||||||
|
"watchImmediate": true,
|
||||||
|
"watchOnce": true,
|
||||||
|
"watchPausable": true,
|
||||||
|
"watchPostEffect": true,
|
||||||
|
"watchSyncEffect": true,
|
||||||
|
"watchThrottled": true,
|
||||||
|
"watchTriggerable": true,
|
||||||
|
"watchWithFilter": true,
|
||||||
|
"whenever": true
|
||||||
|
}
|
||||||
|
}
|
||||||
15
auto-imports.d.ts
vendored
@@ -3,6 +3,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
// Generated by unplugin-auto-import
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
@@ -20,6 +21,7 @@ declare global {
|
|||||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||||
|
const createRef: typeof import('@vueuse/core')['createRef']
|
||||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||||
@@ -34,6 +36,7 @@ declare global {
|
|||||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
||||||
const h: typeof import('vue')['h']
|
const h: typeof import('vue')['h']
|
||||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||||
const inject: typeof import('vue')['inject']
|
const inject: typeof import('vue')['inject']
|
||||||
@@ -43,6 +46,7 @@ declare global {
|
|||||||
const isReactive: typeof import('vue')['isReactive']
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
const isReadonly: typeof import('vue')['isReadonly']
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
const isRef: typeof import('vue')['isRef']
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const isShallow: typeof import('vue')['isShallow']
|
||||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
@@ -54,6 +58,7 @@ declare global {
|
|||||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
|
||||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||||
@@ -65,6 +70,7 @@ declare global {
|
|||||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
const onUpdated: typeof import('vue')['onUpdated']
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||||
const provide: typeof import('vue')['provide']
|
const provide: typeof import('vue')['provide']
|
||||||
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
||||||
@@ -135,6 +141,7 @@ declare global {
|
|||||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||||
|
const useCountdown: typeof import('@vueuse/core')['useCountdown']
|
||||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||||
const useCssModule: typeof import('vue')['useCssModule']
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||||
@@ -174,6 +181,7 @@ declare global {
|
|||||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||||
const useImage: typeof import('@vueuse/core')['useImage']
|
const useImage: typeof import('@vueuse/core')['useImage']
|
||||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||||
@@ -192,6 +200,7 @@ declare global {
|
|||||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||||
const useMessage: typeof import('naive-ui')['useMessage']
|
const useMessage: typeof import('naive-ui')['useMessage']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||||
@@ -217,12 +226,14 @@ declare global {
|
|||||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||||
|
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
|
||||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||||
const useRoute: typeof import('vue-router')['useRoute']
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
const useRouter: typeof import('vue-router')['useRouter']
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
|
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
|
||||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||||
@@ -240,6 +251,7 @@ declare global {
|
|||||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||||
const useSupported: typeof import('@vueuse/core')['useSupported']
|
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||||
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||||
@@ -248,6 +260,7 @@ declare global {
|
|||||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||||
|
const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl']
|
||||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||||
@@ -291,6 +304,6 @@ declare global {
|
|||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, 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')
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|||||||
25
components.d.ts
vendored
@@ -2,6 +2,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// Generated by unplugin-vue-components
|
// Generated by unplugin-vue-components
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
@@ -9,7 +10,9 @@ declare module 'vue' {
|
|||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AboutSetting: typeof import('./src/components/Setting/AboutSetting.vue')['default']
|
AboutSetting: typeof import('./src/components/Setting/AboutSetting.vue')['default']
|
||||||
ArtistList: typeof import('./src/components/List/ArtistList.vue')['default']
|
ArtistList: typeof import('./src/components/List/ArtistList.vue')['default']
|
||||||
BatchList: typeof import('./src/components/Modal/batchList.vue')['default']
|
AutoClose: typeof import('./src/components/Modal/AutoClose.vue')['default']
|
||||||
|
BatchList: typeof import('./src/components/Modal/BatchList.vue')['default']
|
||||||
|
ChangeRate: typeof import('./src/components/Modal/ChangeRate.vue')['default']
|
||||||
CloudMatch: typeof import('./src/components/Modal/CloudMatch.vue')['default']
|
CloudMatch: typeof import('./src/components/Modal/CloudMatch.vue')['default']
|
||||||
CommentList: typeof import('./src/components/List/CommentList.vue')['default']
|
CommentList: typeof import('./src/components/List/CommentList.vue')['default']
|
||||||
CountDown: typeof import('./src/components/Player/CountDown.vue')['default']
|
CountDown: typeof import('./src/components/Player/CountDown.vue')['default']
|
||||||
@@ -17,14 +20,19 @@ declare module 'vue' {
|
|||||||
CoverMenu: typeof import('./src/components/Menu/CoverMenu.vue')['default']
|
CoverMenu: typeof import('./src/components/Menu/CoverMenu.vue')['default']
|
||||||
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
|
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
|
||||||
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
|
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
|
||||||
|
Equalizer: typeof import('./src/components/Modal/Equalizer.vue')['default']
|
||||||
|
ExcludeLyrics: typeof import('./src/components/Modal/ExcludeLyrics.vue')['default']
|
||||||
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
|
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
|
||||||
GeneralSetting: typeof import('./src/components/Setting/GeneralSetting.vue')['default']
|
GeneralSetting: typeof import('./src/components/Setting/GeneralSetting.vue')['default']
|
||||||
JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default']
|
JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default']
|
||||||
KeyboardSetting: typeof import('./src/components/Setting/KeyboardSetting.vue')['default']
|
KeyboardSetting: typeof import('./src/components/Setting/KeyboardSetting.vue')['default']
|
||||||
LocalSetting: typeof import('./src/components/Setting/LocalSetting.vue')['default']
|
LocalSetting: typeof import('./src/components/Setting/LocalSetting.vue')['default']
|
||||||
Login: typeof import('./src/components/Modal/Login.vue')['default']
|
Login: typeof import('./src/components/Modal/Login/Login.vue')['default']
|
||||||
LoginPhone: typeof import('./src/components/Modal/loginPhone.vue')['default']
|
LoginCookie: typeof import('./src/components/Modal/Login/LoginCookie.vue')['default']
|
||||||
LoginQRCode: typeof import('./src/components/Modal/loginQRCode.vue')['default']
|
LoginPhone: typeof import('./src/components/Modal/Login/LoginPhone.vue')['default']
|
||||||
|
LoginQRCode: typeof import('./src/components/Modal/Login/LoginQRCode.vue')['default']
|
||||||
|
LoginUID: typeof import('./src/components/Modal/Login/LoginUID.vue')['default']
|
||||||
|
LyricMenu: typeof import('./src/components/Player/LyricMenu.vue')['default']
|
||||||
LyricsSetting: typeof import('./src/components/Setting/LyricsSetting.vue')['default']
|
LyricsSetting: typeof import('./src/components/Setting/LyricsSetting.vue')['default']
|
||||||
MainAMLyric: typeof import('./src/components/Player/MainAMLyric.vue')['default']
|
MainAMLyric: typeof import('./src/components/Player/MainAMLyric.vue')['default']
|
||||||
MainLyric: typeof import('./src/components/Player/MainLyric.vue')['default']
|
MainLyric: typeof import('./src/components/Player/MainLyric.vue')['default']
|
||||||
@@ -52,6 +60,7 @@ declare module 'vue' {
|
|||||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
|
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
||||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NFlex: typeof import('naive-ui')['NFlex']
|
NFlex: typeof import('naive-ui')['NFlex']
|
||||||
@@ -86,19 +95,17 @@ declare module 'vue' {
|
|||||||
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
|
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
|
||||||
NOl: typeof import('naive-ui')['NOl']
|
NOl: typeof import('naive-ui')['NOl']
|
||||||
NP: typeof import('naive-ui')['NP']
|
NP: typeof import('naive-ui')['NP']
|
||||||
|
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||||
NPopover: typeof import('naive-ui')['NPopover']
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
NProgress: typeof import('naive-ui')['NProgress']
|
|
||||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||||
NRadio: typeof import('naive-ui')['NRadio']
|
NRadio: typeof import('naive-ui')['NRadio']
|
||||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||||
NResult: typeof import('naive-ui')['NResult']
|
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSelect: typeof import('naive-ui')['NSelect']
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||||
NSlider: typeof import('naive-ui')['NSlider']
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
NSpin: typeof import('naive-ui')['NSpin']
|
NSpin: typeof import('naive-ui')['NSpin']
|
||||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
NTab: typeof import('naive-ui')['NTab']
|
|
||||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||||
NTabs: typeof import('naive-ui')['NTabs']
|
NTabs: typeof import('naive-ui')['NTabs']
|
||||||
NTag: typeof import('naive-ui')['NTag']
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
@@ -113,6 +120,8 @@ declare module 'vue' {
|
|||||||
PlayerCover: typeof import('./src/components/Player/PlayerCover.vue')['default']
|
PlayerCover: typeof import('./src/components/Player/PlayerCover.vue')['default']
|
||||||
PlayerData: typeof import('./src/components/Player/PlayerData.vue')['default']
|
PlayerData: typeof import('./src/components/Player/PlayerData.vue')['default']
|
||||||
PlayerMenu: typeof import('./src/components/Player/PlayerMenu.vue')['default']
|
PlayerMenu: typeof import('./src/components/Player/PlayerMenu.vue')['default']
|
||||||
|
PlayerRightMenu: typeof import('./src/components/Player/PlayerRightMenu.vue')['default']
|
||||||
|
PlayerSlider: typeof import('./src/components/Player/PlayerSlider.vue')['default']
|
||||||
PlayerSpectrum: typeof import('./src/components/Player/PlayerSpectrum.vue')['default']
|
PlayerSpectrum: typeof import('./src/components/Player/PlayerSpectrum.vue')['default']
|
||||||
PlaylistAdd: typeof import('./src/components/Modal/PlaylistAdd.vue')['default']
|
PlaylistAdd: typeof import('./src/components/Modal/PlaylistAdd.vue')['default']
|
||||||
PlaySetting: typeof import('./src/components/Setting/PlaySetting.vue')['default']
|
PlaySetting: typeof import('./src/components/Setting/PlaySetting.vue')['default']
|
||||||
@@ -124,6 +133,8 @@ declare module 'vue' {
|
|||||||
SearchInpMenu: typeof import('./src/components/Menu/SearchInpMenu.vue')['default']
|
SearchInpMenu: typeof import('./src/components/Menu/SearchInpMenu.vue')['default']
|
||||||
SearchSuggest: typeof import('./src/components/Search/SearchSuggest.vue')['default']
|
SearchSuggest: typeof import('./src/components/Search/SearchSuggest.vue')['default']
|
||||||
Sider: typeof import('./src/components/Layout/Sider.vue')['default']
|
Sider: typeof import('./src/components/Layout/Sider.vue')['default']
|
||||||
|
SImage: typeof import('./src/components/UI/s-image.vue')['default']
|
||||||
|
SongCard: typeof import('./src/components/Card/SongCard.vue')['default']
|
||||||
SongDataCard: typeof import('./src/components/Card/SongDataCard.vue')['default']
|
SongDataCard: typeof import('./src/components/Card/SongDataCard.vue')['default']
|
||||||
SongInfoEditor: typeof import('./src/components/Modal/SongInfoEditor.vue')['default']
|
SongInfoEditor: typeof import('./src/components/Modal/SongInfoEditor.vue')['default']
|
||||||
SongList: typeof import('./src/components/List/SongList.vue')['default']
|
SongList: typeof import('./src/components/List/SongList.vue')['default']
|
||||||
|
|||||||
@@ -10,3 +10,21 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 25884:25884
|
- 25884:25884
|
||||||
restart: always
|
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
@@ -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 bodian pyncmd} 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 "$@"
|
||||||
173
electron-builder.config.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
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/",
|
||||||
|
},
|
||||||
|
// 发布配置
|
||||||
|
// 先留空,不自动上传
|
||||||
|
publish: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
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"
|
|
||||||
@@ -6,6 +6,7 @@ import vue from "@vitejs/plugin-vue";
|
|||||||
import AutoImport from "unplugin-auto-import/vite";
|
import AutoImport from "unplugin-auto-import/vite";
|
||||||
import Components from "unplugin-vue-components/vite";
|
import Components from "unplugin-vue-components/vite";
|
||||||
import viteCompression from "vite-plugin-compression";
|
import viteCompression from "vite-plugin-compression";
|
||||||
|
// import VueDevTools from "vite-plugin-vue-devtools";
|
||||||
import wasm from "vite-plugin-wasm";
|
import wasm from "vite-plugin-wasm";
|
||||||
|
|
||||||
export default defineConfig(({ command, mode }) => {
|
export default defineConfig(({ command, mode }) => {
|
||||||
@@ -28,7 +29,6 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
input: {
|
input: {
|
||||||
index: resolve(__dirname, "electron/main/index.ts"),
|
index: resolve(__dirname, "electron/main/index.ts"),
|
||||||
lyric: resolve(__dirname, "web/lyric.html"),
|
lyric: resolve(__dirname, "web/lyric.html"),
|
||||||
loading: resolve(__dirname, "web/loading.html"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -49,6 +49,7 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
root: ".",
|
root: ".",
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
|
// mode === "development" && VueDevTools(),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
imports: [
|
imports: [
|
||||||
"vue",
|
"vue",
|
||||||
@@ -60,7 +61,7 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
],
|
],
|
||||||
eslintrc: {
|
eslintrc: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
filepath: "./.eslintrc-auto-import.json",
|
filepath: "./auto-eslint.mjs",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
Components({
|
Components({
|
||||||
@@ -74,6 +75,13 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
"@": resolve(__dirname, "src/"),
|
"@": resolve(__dirname, "src/"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
silenceDeprecations: ["legacy-js-api"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: webPort,
|
port: webPort,
|
||||||
// 代理
|
// 代理
|
||||||
@@ -81,7 +89,7 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
"/api": {
|
"/api": {
|
||||||
target: `http://127.0.0.1:${servePort}`,
|
target: `http://127.0.0.1:${servePort}`,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, "/api/"),
|
rewrite: (path) => path.replace(/^\/api/, "/api"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -94,6 +102,7 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
index: resolve(__dirname, "index.html"),
|
index: resolve(__dirname, "index.html"),
|
||||||
|
loading: resolve(__dirname, "web/loading/index.html"),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
|
|||||||
@@ -1,201 +1,60 @@
|
|||||||
import { app, shell, BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
import { app, BrowserWindow } 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 { release } from "os";
|
import { isMac } from "./utils/config";
|
||||||
import { isDev, isMac, appName } from "./utils";
|
import { unregisterShortcuts } from "./shortcut";
|
||||||
import { registerAllShortcuts, unregisterShortcuts } from "./shortcut";
|
|
||||||
import { initTray, MainTray } from "./tray";
|
import { initTray, MainTray } from "./tray";
|
||||||
import { initThumbar, Thumbar } from "./thumbar";
|
import { processLog } from "./logger";
|
||||||
import initAppServer from "../server";
|
import initAppServer from "../server";
|
||||||
import initIpcMain from "./ipcMain";
|
import { initSingleLock } from "./utils/single-lock";
|
||||||
import log from "./logger";
|
import loadWindow from "./windows/load-window";
|
||||||
import store from "./store";
|
import mainWindow from "./windows/main-window";
|
||||||
// icon
|
import initIpc from "./ipc";
|
||||||
import icon from "../../public/icons/favicon.png?asset";
|
|
||||||
|
|
||||||
// 屏蔽报错
|
// 屏蔽报错
|
||||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||||
|
|
||||||
// 模拟打包
|
|
||||||
Object.defineProperty(app, "isPackaged", {
|
|
||||||
get() {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 主进程
|
// 主进程
|
||||||
class MainProcess {
|
class MainProcess {
|
||||||
// 窗口
|
// 窗口
|
||||||
mainWindow: BrowserWindow | null = null;
|
mainWindow: BrowserWindow | null = null;
|
||||||
lyricWindow: BrowserWindow | null = null;
|
loadWindow: BrowserWindow | null = null;
|
||||||
loadingWindow: BrowserWindow | null = null;
|
|
||||||
// 托盘
|
// 托盘
|
||||||
mainTray: MainTray | null = null;
|
mainTray: MainTray | null = null;
|
||||||
// 工具栏
|
|
||||||
thumbar: Thumbar | null = null;
|
|
||||||
// 是否退出
|
// 是否退出
|
||||||
isQuit: boolean = false;
|
isQuit: boolean = false;
|
||||||
constructor() {
|
constructor() {
|
||||||
log.info("🚀 Main process startup");
|
processLog.info("🚀 Main process startup");
|
||||||
|
// 程序单例锁
|
||||||
|
initSingleLock();
|
||||||
// 禁用 Windows 7 的 GPU 加速功能
|
// 禁用 Windows 7 的 GPU 加速功能
|
||||||
if (release().startsWith("6.1")) app.disableHardwareAcceleration();
|
if (release().startsWith("6.1") && type() == "Windows_NT") app.disableHardwareAcceleration();
|
||||||
// 单例锁
|
// 监听应用事件
|
||||||
if (!app.requestSingleInstanceLock()) {
|
this.handleAppEvents();
|
||||||
log.error("❌ There is already a program running and this process is terminated");
|
// Electron 初始化完成后
|
||||||
app.quit();
|
// 某些API只有在此事件发生后才能使用
|
||||||
process.exit(0);
|
|
||||||
} else this.showWindow();
|
|
||||||
// 准备就绪
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
log.info("🚀 Application Process Startup");
|
processLog.info("🚀 Application Process Startup");
|
||||||
// 设置应用程序名称
|
// 设置应用程序名称
|
||||||
electronApp.setAppUserModelId(app.getName());
|
electronApp.setAppUserModelId("com.imsyy.splayer");
|
||||||
// 启动主服务进程
|
// 启动主服务进程
|
||||||
await initAppServer();
|
await initAppServer();
|
||||||
// 启动进程
|
// 启动窗口
|
||||||
this.createLoadingWindow();
|
this.loadWindow = loadWindow.create();
|
||||||
this.createMainWindow();
|
this.mainWindow = mainWindow.create();
|
||||||
this.createLyricsWindow();
|
|
||||||
this.handleAppEvents();
|
|
||||||
this.handleWindowEvents();
|
|
||||||
// 注册其他服务
|
// 注册其他服务
|
||||||
this.mainTray = initTray(this.mainWindow!, this.lyricWindow!);
|
this.mainTray = initTray(this.mainWindow!);
|
||||||
this.thumbar = initThumbar(this.mainWindow!);
|
// 注册 IPC 通信
|
||||||
// 注册主进程事件
|
initIpc();
|
||||||
initIpcMain(
|
|
||||||
this.mainWindow,
|
|
||||||
this.lyricWindow,
|
|
||||||
this.loadingWindow,
|
|
||||||
this.mainTray,
|
|
||||||
this.thumbar,
|
|
||||||
store,
|
|
||||||
);
|
|
||||||
// 注册快捷键
|
|
||||||
registerAllShortcuts(this.mainWindow!);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 创建窗口
|
|
||||||
createWindow(options: BrowserWindowConstructorOptions = {}): BrowserWindow {
|
|
||||||
const defaultOptions: BrowserWindowConstructorOptions = {
|
|
||||||
title: appName,
|
|
||||||
width: 1280,
|
|
||||||
height: 720,
|
|
||||||
frame: false,
|
|
||||||
center: true,
|
|
||||||
// 图标
|
|
||||||
icon,
|
|
||||||
webPreferences: {
|
|
||||||
preload: join(__dirname, "../preload/index.mjs"),
|
|
||||||
// 禁用渲染器沙盒
|
|
||||||
sandbox: false,
|
|
||||||
// 禁用同源策略
|
|
||||||
webSecurity: false,
|
|
||||||
// 允许 HTTP
|
|
||||||
allowRunningInsecureContent: true,
|
|
||||||
// 禁用拼写检查
|
|
||||||
spellcheck: false,
|
|
||||||
// 启用 Node.js
|
|
||||||
nodeIntegration: true,
|
|
||||||
nodeIntegrationInWorker: true,
|
|
||||||
// 启用上下文隔离
|
|
||||||
contextIsolation: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// 合并参数
|
|
||||||
options = Object.assign(defaultOptions, options);
|
|
||||||
// 创建窗口
|
|
||||||
const win = new BrowserWindow(options);
|
|
||||||
return win;
|
|
||||||
}
|
|
||||||
// 创建主窗口
|
|
||||||
createMainWindow() {
|
|
||||||
// 窗口配置项
|
|
||||||
const options: BrowserWindowConstructorOptions = {
|
|
||||||
width: store.get("window").width,
|
|
||||||
height: store.get("window").height,
|
|
||||||
minHeight: 800,
|
|
||||||
minWidth: 1280,
|
|
||||||
// 菜单栏
|
|
||||||
titleBarStyle: "customButtonsOnHover",
|
|
||||||
// 立即显示窗口
|
|
||||||
show: false,
|
|
||||||
};
|
|
||||||
// 初始化窗口
|
|
||||||
this.mainWindow = this.createWindow(options);
|
|
||||||
|
|
||||||
// 渲染路径
|
|
||||||
if (isDev && process.env["ELECTRON_RENDERER_URL"]) {
|
|
||||||
this.mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
|
||||||
} else {
|
|
||||||
const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884);
|
|
||||||
this.mainWindow.loadURL(`http://127.0.0.1:${port}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置网络代理
|
|
||||||
if (store.get("proxy")) {
|
|
||||||
this.mainWindow.webContents.session.setProxy({ proxyRules: store.get("proxy") });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 窗口打开处理程序
|
|
||||||
this.mainWindow.webContents.setWindowOpenHandler((details) => {
|
|
||||||
const { url } = details;
|
|
||||||
if (url.startsWith("https://") || url.startsWith("http://")) {
|
|
||||||
shell.openExternal(url);
|
|
||||||
}
|
|
||||||
return { action: "deny" };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 创建加载窗口
|
|
||||||
createLoadingWindow() {
|
|
||||||
// 初始化窗口
|
|
||||||
this.loadingWindow = this.createWindow({
|
|
||||||
width: 800,
|
|
||||||
height: 560,
|
|
||||||
maxWidth: 800,
|
|
||||||
maxHeight: 560,
|
|
||||||
resizable: false,
|
|
||||||
});
|
|
||||||
// 渲染路径
|
|
||||||
this.loadingWindow.loadFile(join(__dirname, "../main/web/loading.html"));
|
|
||||||
}
|
|
||||||
// 创建桌面歌词窗口
|
|
||||||
createLyricsWindow() {
|
|
||||||
// 初始化窗口
|
|
||||||
this.lyricWindow = this.createWindow({
|
|
||||||
width: store.get("lyric").width || 800,
|
|
||||||
height: store.get("lyric").height || 180,
|
|
||||||
minWidth: 440,
|
|
||||||
minHeight: 120,
|
|
||||||
maxWidth: 1600,
|
|
||||||
maxHeight: 300,
|
|
||||||
// 窗口位置
|
|
||||||
x: store.get("lyric").x,
|
|
||||||
y: store.get("lyric").y,
|
|
||||||
transparent: true,
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
|
||||||
alwaysOnTop: true,
|
|
||||||
resizable: true,
|
|
||||||
movable: true,
|
|
||||||
// 不在任务栏显示
|
|
||||||
skipTaskbar: true,
|
|
||||||
// 窗口不能最小化
|
|
||||||
minimizable: false,
|
|
||||||
// 窗口不能最大化
|
|
||||||
maximizable: false,
|
|
||||||
// 窗口不能进入全屏状态
|
|
||||||
fullscreenable: false,
|
|
||||||
show: false,
|
|
||||||
});
|
|
||||||
// 渲染路径
|
|
||||||
this.lyricWindow.loadFile(join(__dirname, "../main/web/lyric.html"));
|
|
||||||
}
|
|
||||||
// 应用程序事件
|
// 应用程序事件
|
||||||
handleAppEvents() {
|
handleAppEvents() {
|
||||||
// 窗口被关闭时
|
// 窗口被关闭时
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
if (!isMac) app.quit();
|
if (!isMac) app.quit();
|
||||||
this.mainWindow = null;
|
this.mainWindow = null;
|
||||||
this.loadingWindow = null;
|
this.loadWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 应用被激活
|
// 应用被激活
|
||||||
@@ -203,24 +62,12 @@ class MainProcess {
|
|||||||
const allWindows = BrowserWindow.getAllWindows();
|
const allWindows = BrowserWindow.getAllWindows();
|
||||||
if (allWindows.length) {
|
if (allWindows.length) {
|
||||||
allWindows[0].focus();
|
allWindows[0].focus();
|
||||||
} else {
|
|
||||||
this.createMainWindow();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 新增 session
|
|
||||||
app.on("second-instance", () => {
|
|
||||||
this.showWindow();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 开发环境控制台
|
|
||||||
app.on("browser-window-created", (_, window) => {
|
|
||||||
optimizer.watchWindowShortcuts(window);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 自定义协议
|
// 自定义协议
|
||||||
app.on("open-url", (_, url) => {
|
app.on("open-url", (_, url) => {
|
||||||
console.log("Received custom protocol URL:", url);
|
processLog.log("Received custom protocol URL:", url);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 将要退出
|
// 将要退出
|
||||||
@@ -234,57 +81,6 @@ class MainProcess {
|
|||||||
this.isQuit = true;
|
this.isQuit = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 窗口事件
|
|
||||||
handleWindowEvents() {
|
|
||||||
this.mainWindow?.on("show", () => {
|
|
||||||
// this.mainWindow?.webContents.send("lyricsScroll");
|
|
||||||
});
|
|
||||||
this.mainWindow?.on("focus", () => {
|
|
||||||
this.saveBounds();
|
|
||||||
});
|
|
||||||
// 移动或缩放
|
|
||||||
this.mainWindow?.on("resized", () => {
|
|
||||||
// 若处于全屏则不保存
|
|
||||||
if (this.mainWindow?.isFullScreen()) return;
|
|
||||||
this.saveBounds();
|
|
||||||
});
|
|
||||||
this.mainWindow?.on("moved", () => {
|
|
||||||
this.saveBounds();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 歌词窗口缩放
|
|
||||||
this.lyricWindow?.on("resized", () => {
|
|
||||||
const bounds = this.lyricWindow?.getBounds();
|
|
||||||
if (bounds) {
|
|
||||||
const { width, height } = bounds;
|
|
||||||
store.set("lyric", { ...store.get("lyric"), width, height });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 窗口关闭
|
|
||||||
this.mainWindow?.on("close", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (this.isQuit) {
|
|
||||||
app.exit();
|
|
||||||
} else {
|
|
||||||
this.mainWindow?.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 更新窗口大小
|
|
||||||
saveBounds() {
|
|
||||||
if (this.mainWindow?.isFullScreen()) return;
|
|
||||||
const bounds = this.mainWindow?.getBounds();
|
|
||||||
if (bounds) store.set("window", bounds);
|
|
||||||
}
|
|
||||||
// 显示窗口
|
|
||||||
showWindow() {
|
|
||||||
if (this.mainWindow) {
|
|
||||||
this.mainWindow.show();
|
|
||||||
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
|
|
||||||
this.mainWindow.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new MainProcess();
|
export default new MainProcess();
|
||||||
|
|||||||
27
electron/main/ipc/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import initFileIpc from "./ipc-file";
|
||||||
|
import initLyricIpc from "./ipc-lyric";
|
||||||
|
import initShortcutIpc from "./ipc-shortcut";
|
||||||
|
import initStoreIpc from "./ipc-store";
|
||||||
|
import initSystemIpc from "./ipc-system";
|
||||||
|
import initThumbarIpc from "./ipc-thumbar";
|
||||||
|
import initTrayIpc from "./ipc-tray";
|
||||||
|
import initUpdateIpc from "./ipc-update";
|
||||||
|
import initWindowsIpc from "./ipc-window";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化全部 IPC 通信
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const initIpc = (): void => {
|
||||||
|
initSystemIpc();
|
||||||
|
initWindowsIpc();
|
||||||
|
initUpdateIpc();
|
||||||
|
initFileIpc();
|
||||||
|
initTrayIpc();
|
||||||
|
initLyricIpc();
|
||||||
|
initStoreIpc();
|
||||||
|
initThumbarIpc();
|
||||||
|
initShortcutIpc();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initIpc;
|
||||||
386
electron/main/ipc/ipc-file.ts
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
|
||||||
|
import { basename, isAbsolute, join, relative, resolve } from "path";
|
||||||
|
import { access, readFile, stat, unlink, writeFile } from "fs/promises";
|
||||||
|
import { parseFile } from "music-metadata";
|
||||||
|
import { getFileID, getFileMD5, metaDataLyricsArrayToLrc } from "../utils/helper";
|
||||||
|
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
|
||||||
|
import { ipcLog } from "../logger";
|
||||||
|
import FastGlob from "fast-glob";
|
||||||
|
import { download } from "electron-dl";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件相关 IPC
|
||||||
|
*/
|
||||||
|
const initFileIpc = (): void => {
|
||||||
|
// 默认文件夹
|
||||||
|
ipcMain.handle(
|
||||||
|
"get-default-dir",
|
||||||
|
(_event, type: "documents" | "downloads" | "pictures" | "music" | "videos"): string => {
|
||||||
|
return app.getPath(type);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 遍历音乐文件
|
||||||
|
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 FastGlob("**/*.{mp3,wav,flac,aac,webm}", { cwd: filePath });
|
||||||
|
// 解析元信息
|
||||||
|
const metadataPromises = musicFiles.map(async (file) => {
|
||||||
|
const filePath = join(dirPath, file);
|
||||||
|
// 处理元信息
|
||||||
|
const { common, format } = await parseFile(filePath);
|
||||||
|
// 获取文件大小
|
||||||
|
const { size } = await stat(filePath);
|
||||||
|
// 判断音质等级
|
||||||
|
let quality: string;
|
||||||
|
if ((format.sampleRate || 0) >= 96000 || (format.bitsPerSample || 0) > 16) {
|
||||||
|
quality = "Hi-Res";
|
||||||
|
} else if ((format.sampleRate || 0) >= 44100) {
|
||||||
|
quality = "HQ";
|
||||||
|
} else {
|
||||||
|
quality = "SQ";
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: getFileID(filePath),
|
||||||
|
name: common.title || basename(filePath),
|
||||||
|
artists: common.artists?.[0] || common.artist,
|
||||||
|
album: common.album || "",
|
||||||
|
alia: common.comment?.[0]?.text || "",
|
||||||
|
duration: (format?.duration ?? 0) * 1000,
|
||||||
|
size: (size / (1024 * 1024)).toFixed(2),
|
||||||
|
path: filePath,
|
||||||
|
quality,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const metadataArray = await Promise.all(metadataPromises);
|
||||||
|
return metadataArray;
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error("❌ Error fetching music metadata:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取音乐元信息
|
||||||
|
ipcMain.handle("get-music-metadata", async (_, path: string) => {
|
||||||
|
try {
|
||||||
|
const filePath = resolve(path).replace(/\\/g, "/");
|
||||||
|
const { common, format } = await parseFile(filePath);
|
||||||
|
return {
|
||||||
|
// 文件名称
|
||||||
|
fileName: basename(filePath),
|
||||||
|
// 文件大小
|
||||||
|
fileSize: (await stat(filePath)).size / (1024 * 1024),
|
||||||
|
// 元信息
|
||||||
|
common,
|
||||||
|
// 歌词
|
||||||
|
lyric:
|
||||||
|
metaDataLyricsArrayToLrc(common?.lyrics?.[0]?.syncText || []) ||
|
||||||
|
common?.lyrics?.[0]?.text ||
|
||||||
|
"",
|
||||||
|
// 音质信息
|
||||||
|
format,
|
||||||
|
// md5
|
||||||
|
md5: await getFileMD5(filePath),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error("❌ Error fetching music metadata:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修改音乐元信息
|
||||||
|
ipcMain.handle("set-music-metadata", async (_, path: string, metadata: any) => {
|
||||||
|
try {
|
||||||
|
const { name, artist, album, alia, lyric, cover } = metadata;
|
||||||
|
// 规范化路径
|
||||||
|
const songPath = resolve(path);
|
||||||
|
const coverPath = cover ? resolve(cover) : null;
|
||||||
|
// 读取歌曲文件
|
||||||
|
const songFile = File.createFromPath(songPath);
|
||||||
|
// 读取封面文件
|
||||||
|
const songCover = coverPath ? Picture.fromPath(coverPath) : null;
|
||||||
|
// 保存元数据
|
||||||
|
Id3v2Settings.forceDefaultVersion = true;
|
||||||
|
Id3v2Settings.defaultVersion = 3;
|
||||||
|
songFile.tag.title = name || "未知曲目";
|
||||||
|
songFile.tag.performers = [artist || "未知艺术家"];
|
||||||
|
songFile.tag.album = album || "未知专辑";
|
||||||
|
songFile.tag.albumArtists = [artist || "未知艺术家"];
|
||||||
|
songFile.tag.lyrics = lyric || "";
|
||||||
|
songFile.tag.description = alia || "";
|
||||||
|
songFile.tag.comment = alia || "";
|
||||||
|
if (songCover) songFile.tag.pictures = [songCover];
|
||||||
|
// 保存元信息
|
||||||
|
songFile.save();
|
||||||
|
songFile.dispose();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error("❌ Error setting music metadata:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取音乐歌词
|
||||||
|
ipcMain.handle(
|
||||||
|
"get-music-lyric",
|
||||||
|
async (
|
||||||
|
_,
|
||||||
|
path: string,
|
||||||
|
): Promise<{
|
||||||
|
lyric: string;
|
||||||
|
format: "lrc" | "ttml";
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
const filePath = resolve(path).replace(/\\/g, "/");
|
||||||
|
const { common } = await parseFile(filePath);
|
||||||
|
|
||||||
|
// 尝试获取同名的歌词文件
|
||||||
|
const filePathWithoutExt = filePath.replace(/\.[^.]+$/, "");
|
||||||
|
for (const ext of ["ttml", "lrc"] as const) {
|
||||||
|
const lyricPath = `${filePathWithoutExt}.${ext}`;
|
||||||
|
ipcLog.info("lyricPath", lyricPath);
|
||||||
|
try {
|
||||||
|
await access(lyricPath);
|
||||||
|
const lyric = await readFile(lyricPath, "utf-8");
|
||||||
|
if (lyric && lyric != "") return { lyric, format: ext };
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取元数据
|
||||||
|
const lyric = common?.lyrics?.[0]?.syncText;
|
||||||
|
if (lyric && lyric.length > 0) {
|
||||||
|
return { lyric: metaDataLyricsArrayToLrc(lyric), format: "lrc" };
|
||||||
|
} else if (common?.lyrics?.[0]?.text) {
|
||||||
|
return { lyric: common?.lyrics?.[0]?.text, format: "lrc" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有歌词
|
||||||
|
return { lyric: "", format: "lrc" };
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error("❌ Error fetching music lyric:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取音乐封面
|
||||||
|
ipcMain.handle(
|
||||||
|
"get-music-cover",
|
||||||
|
async (_, path: string): Promise<{ data: Buffer; format: string } | null> => {
|
||||||
|
try {
|
||||||
|
const { common } = await parseFile(path);
|
||||||
|
// 获取封面数据
|
||||||
|
const picture = common.picture?.[0];
|
||||||
|
if (picture) {
|
||||||
|
return { data: Buffer.from(picture.data), format: picture.format };
|
||||||
|
} else {
|
||||||
|
const coverFilePath = path.replace(/\.[^.]+$/, ".jpg");
|
||||||
|
try {
|
||||||
|
await access(coverFilePath);
|
||||||
|
const coverData = await readFile(coverFilePath);
|
||||||
|
return { data: coverData, format: "image/jpeg" };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error fetching music cover:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 读取本地歌词
|
||||||
|
ipcMain.handle(
|
||||||
|
"read-local-lyric",
|
||||||
|
async (_, lyricDir: string, id: number, ext: string): Promise<string> => {
|
||||||
|
const lyricPath = join(lyricDir, `${id}.${ext}`);
|
||||||
|
try {
|
||||||
|
await access(lyricPath);
|
||||||
|
const lyric = await readFile(lyricPath, "utf-8");
|
||||||
|
if (lyric) return lyric;
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
ipcMain.handle("delete-file", async (_, path: string) => {
|
||||||
|
try {
|
||||||
|
// 规范化路径
|
||||||
|
const resolvedPath = resolve(path);
|
||||||
|
// 检查文件是否存在
|
||||||
|
try {
|
||||||
|
await access(resolvedPath);
|
||||||
|
} catch {
|
||||||
|
throw new Error("❌ File not found");
|
||||||
|
}
|
||||||
|
// 删除文件
|
||||||
|
await unlink(resolvedPath);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error("❌ File delete error", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 打开文件夹
|
||||||
|
ipcMain.on("open-folder", async (_, path: string) => {
|
||||||
|
try {
|
||||||
|
// 规范化路径
|
||||||
|
const resolvedPath = resolve(path);
|
||||||
|
// 检查文件夹是否存在
|
||||||
|
try {
|
||||||
|
await access(resolvedPath);
|
||||||
|
} catch {
|
||||||
|
throw new Error("❌ Folder not found");
|
||||||
|
}
|
||||||
|
// 打开文件夹
|
||||||
|
shell.showItemInFolder(resolvedPath);
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error("❌ Folder open error", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 图片选择窗口
|
||||||
|
ipcMain.handle("choose-image", async () => {
|
||||||
|
try {
|
||||||
|
const { filePaths } = await dialog.showOpenDialog({
|
||||||
|
properties: ["openFile"],
|
||||||
|
filters: [{ name: "Images", extensions: ["jpg", "jpeg", "png"] }],
|
||||||
|
});
|
||||||
|
if (!filePaths || filePaths.length === 0) return null;
|
||||||
|
return filePaths[0];
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error("❌ Image choose error", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 路径选择窗口
|
||||||
|
ipcMain.handle("choose-path", async () => {
|
||||||
|
try {
|
||||||
|
const { filePaths } = await dialog.showOpenDialog({
|
||||||
|
title: "选择文件夹",
|
||||||
|
defaultPath: app.getPath("downloads"),
|
||||||
|
properties: ["openDirectory", "createDirectory"],
|
||||||
|
buttonLabel: "选择文件夹",
|
||||||
|
});
|
||||||
|
if (!filePaths || filePaths.length === 0) return null;
|
||||||
|
return filePaths[0];
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error("❌ Path choose error", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
ipcMain.handle(
|
||||||
|
"download-file",
|
||||||
|
async (
|
||||||
|
event,
|
||||||
|
url: string,
|
||||||
|
options: {
|
||||||
|
fileName: string;
|
||||||
|
fileType: string;
|
||||||
|
path: string;
|
||||||
|
downloadMeta?: boolean;
|
||||||
|
downloadCover?: boolean;
|
||||||
|
downloadLyric?: boolean;
|
||||||
|
saveMetaFile?: boolean;
|
||||||
|
lyric?: string;
|
||||||
|
songData?: any;
|
||||||
|
} = {
|
||||||
|
fileName: "未知文件名",
|
||||||
|
fileType: "mp3",
|
||||||
|
path: app.getPath("downloads"),
|
||||||
|
},
|
||||||
|
): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
// 获取窗口
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (!win) return false;
|
||||||
|
// 获取配置
|
||||||
|
const {
|
||||||
|
fileName,
|
||||||
|
fileType,
|
||||||
|
path,
|
||||||
|
lyric,
|
||||||
|
downloadMeta,
|
||||||
|
downloadCover,
|
||||||
|
downloadLyric,
|
||||||
|
saveMetaFile,
|
||||||
|
songData,
|
||||||
|
} = options;
|
||||||
|
// 规范化路径
|
||||||
|
const downloadPath = resolve(path);
|
||||||
|
// 检查文件夹是否存在
|
||||||
|
try {
|
||||||
|
await access(downloadPath);
|
||||||
|
} catch {
|
||||||
|
throw new Error("❌ Folder not found");
|
||||||
|
}
|
||||||
|
// 下载文件
|
||||||
|
const songDownload = await download(win, url, {
|
||||||
|
directory: downloadPath,
|
||||||
|
filename: `${fileName}.${fileType}`,
|
||||||
|
});
|
||||||
|
if (!downloadMeta || !songData?.cover) return true;
|
||||||
|
// 下载封面
|
||||||
|
const coverUrl = songData?.coverSize?.l || songData.cover;
|
||||||
|
const coverDownload = await download(win, coverUrl, {
|
||||||
|
directory: downloadPath,
|
||||||
|
filename: `${fileName}.jpg`,
|
||||||
|
});
|
||||||
|
// 读取歌曲文件
|
||||||
|
const songFile = File.createFromPath(songDownload.getSavePath());
|
||||||
|
// 生成图片信息
|
||||||
|
const songCover = Picture.fromPath(coverDownload.getSavePath());
|
||||||
|
// 保存修改后的元数据
|
||||||
|
Id3v2Settings.forceDefaultVersion = true;
|
||||||
|
Id3v2Settings.defaultVersion = 3;
|
||||||
|
songFile.tag.title = songData?.name || "未知曲目";
|
||||||
|
songFile.tag.album = songData?.album?.name || "未知专辑";
|
||||||
|
songFile.tag.performers = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
|
||||||
|
songFile.tag.albumArtists = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
|
||||||
|
if (lyric && downloadLyric) songFile.tag.lyrics = lyric;
|
||||||
|
if (songCover && downloadCover) songFile.tag.pictures = [songCover];
|
||||||
|
// 保存元信息
|
||||||
|
songFile.save();
|
||||||
|
songFile.dispose();
|
||||||
|
// 创建同名歌词文件
|
||||||
|
if (lyric && saveMetaFile && downloadLyric) {
|
||||||
|
const lrcPath = join(downloadPath, `${fileName}.lrc`);
|
||||||
|
await writeFile(lrcPath, lyric, "utf-8");
|
||||||
|
}
|
||||||
|
// 是否删除封面
|
||||||
|
if (!saveMetaFile || !downloadCover) await unlink(coverDownload.getSavePath());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error("❌ Error downloading file:", error);
|
||||||
|
return 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initFileIpc;
|
||||||
201
electron/main/ipc/ipc-lyric.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import { BrowserWindow, ipcMain, screen } from "electron";
|
||||||
|
import { useStore } from "../store";
|
||||||
|
import lyricWindow from "../windows/lyric-window";
|
||||||
|
import mainWindow from "../windows/main-window";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌词相关 IPC
|
||||||
|
*/
|
||||||
|
const initLyricIpc = (): void => {
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
// 歌词窗口
|
||||||
|
let lyricWin: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 窗口是否存活
|
||||||
|
* @param win 窗口实例
|
||||||
|
* @returns 是否存活
|
||||||
|
*/
|
||||||
|
const isWinAlive = (win: BrowserWindow | null): win is BrowserWindow =>
|
||||||
|
!!win && !win.isDestroyed();
|
||||||
|
|
||||||
|
// 切换桌面歌词
|
||||||
|
ipcMain.on("toggle-desktop-lyric", (_event, val: boolean) => {
|
||||||
|
if (val) {
|
||||||
|
if (!isWinAlive(lyricWin)) {
|
||||||
|
lyricWin = lyricWindow.create();
|
||||||
|
// 监听关闭,置空引用,防止后续调用报错
|
||||||
|
lyricWin?.on("closed", () => {
|
||||||
|
lyricWin = null;
|
||||||
|
});
|
||||||
|
// 设置位置
|
||||||
|
const { x, y } = store.get("lyric");
|
||||||
|
const xPos = Number(x);
|
||||||
|
const yPos = Number(y);
|
||||||
|
if (Number.isFinite(xPos) && Number.isFinite(yPos)) {
|
||||||
|
lyricWin?.setPosition(Math.round(xPos), Math.round(yPos));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lyricWin.show();
|
||||||
|
}
|
||||||
|
if (isWinAlive(lyricWin)) {
|
||||||
|
lyricWin.setAlwaysOnTop(true, "screen-saver");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 关闭:不销毁窗口,直接隐藏,保留位置与状态
|
||||||
|
if (!isWinAlive(lyricWin)) return;
|
||||||
|
lyricWin.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新歌词窗口数据
|
||||||
|
ipcMain.on("update-desktop-lyric-data", (_, lyricData) => {
|
||||||
|
if (!lyricData || !isWinAlive(lyricWin)) return;
|
||||||
|
lyricWin.webContents.send("update-desktop-lyric-data", lyricData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新歌词窗口配置
|
||||||
|
ipcMain.on("update-desktop-lyric-option", (_, option, callback: boolean = false) => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!option || !isWinAlive(lyricWin)) return;
|
||||||
|
// 增量更新
|
||||||
|
const prevOption = store.get("lyric.config");
|
||||||
|
if (prevOption) {
|
||||||
|
option = { ...prevOption, ...option };
|
||||||
|
}
|
||||||
|
store.set("lyric.config", option);
|
||||||
|
// 触发窗口更新
|
||||||
|
if (callback && isWinAlive(lyricWin)) {
|
||||||
|
lyricWin.webContents.send("update-desktop-lyric-option", option);
|
||||||
|
}
|
||||||
|
if (isWinAlive(mainWin)) {
|
||||||
|
mainWin?.webContents.send("update-desktop-lyric-option", option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 播放状态更改
|
||||||
|
ipcMain.on("play-status-change", (_, status) => {
|
||||||
|
if (!isWinAlive(lyricWin)) return;
|
||||||
|
lyricWin.webContents.send("update-desktop-lyric-data", { playStatus: status });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 音乐名称更改
|
||||||
|
ipcMain.on("play-song-change", (_, title) => {
|
||||||
|
if (!title || !isWinAlive(lyricWin)) return;
|
||||||
|
lyricWin.webContents.send("update-desktop-lyric-data", { playName: title });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 音乐歌词更改
|
||||||
|
ipcMain.on("play-lyric-change", (_, lyricData) => {
|
||||||
|
if (!lyricData || !isWinAlive(lyricWin)) return;
|
||||||
|
lyricWin.webContents.send("update-desktop-lyric-data", lyricData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取窗口位置
|
||||||
|
ipcMain.handle("get-window-bounds", () => {
|
||||||
|
if (!isWinAlive(lyricWin)) return {};
|
||||||
|
return lyricWin.getBounds();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取屏幕尺寸
|
||||||
|
ipcMain.handle("get-screen-size", () => {
|
||||||
|
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
||||||
|
return { width, height };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取多屏虚拟边界(支持负坐标)
|
||||||
|
ipcMain.handle("get-virtual-screen-bounds", () => {
|
||||||
|
const displays = screen.getAllDisplays();
|
||||||
|
const bounds = displays.map((d) => d.workArea);
|
||||||
|
const minX = Math.min(...bounds.map((b) => b.x));
|
||||||
|
const minY = Math.min(...bounds.map((b) => b.y));
|
||||||
|
const maxX = Math.max(...bounds.map((b) => b.x + b.width));
|
||||||
|
const maxY = Math.max(...bounds.map((b) => b.y + b.height));
|
||||||
|
return { minX, minY, maxX, maxY };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移动窗口
|
||||||
|
ipcMain.on("move-window", (_, x, y, width, height) => {
|
||||||
|
if (!isWinAlive(lyricWin)) return;
|
||||||
|
lyricWin.setBounds({ x, y, width, height });
|
||||||
|
// 保存配置
|
||||||
|
store.set("lyric", { ...store.get("lyric"), x, y, width, height });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新歌词窗口宽高
|
||||||
|
ipcMain.on("update-lyric-size", (_, width, height) => {
|
||||||
|
if (!isWinAlive(lyricWin)) return;
|
||||||
|
// 更新窗口宽度
|
||||||
|
lyricWin.setBounds({ width, height });
|
||||||
|
store.set("lyric", { ...store.get("lyric"), width, height });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新高度
|
||||||
|
ipcMain.on("update-window-height", (_, height) => {
|
||||||
|
if (!isWinAlive(lyricWin)) return;
|
||||||
|
const store = useStore();
|
||||||
|
const { width } = lyricWin.getBounds();
|
||||||
|
// 更新窗口高度
|
||||||
|
lyricWin.setBounds({ width, height });
|
||||||
|
store.set("lyric", { ...store.get("lyric"), height });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否固定当前最大宽高
|
||||||
|
ipcMain.on(
|
||||||
|
"toggle-fixed-max-size",
|
||||||
|
(_, options: { width: number; height: number; fixed: boolean }) => {
|
||||||
|
if (!isWinAlive(lyricWin)) return;
|
||||||
|
const { width, height, fixed } = options;
|
||||||
|
if (fixed) {
|
||||||
|
lyricWin.setMaximumSize(width, height);
|
||||||
|
} else {
|
||||||
|
lyricWin.setMaximumSize(1400, 360);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 请求歌词数据
|
||||||
|
ipcMain.on("request-desktop-lyric-data", () => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
|
||||||
|
// 触发窗口更新
|
||||||
|
mainWin?.webContents.send("request-desktop-lyric-data");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 请求歌词配置
|
||||||
|
ipcMain.handle("request-desktop-lyric-option", () => {
|
||||||
|
const config = store.get("lyric.config");
|
||||||
|
if (isWinAlive(lyricWin)) {
|
||||||
|
lyricWin.webContents.send("update-desktop-lyric-option", config);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭桌面歌词
|
||||||
|
ipcMain.on("closeDesktopLyric", () => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
|
||||||
|
lyricWin.hide();
|
||||||
|
mainWin?.webContents.send("closeDesktopLyric");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 锁定/解锁桌面歌词
|
||||||
|
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean, isTemp: boolean = false) => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
|
||||||
|
// 是否穿透
|
||||||
|
if (isLock) {
|
||||||
|
lyricWin.setIgnoreMouseEvents(true, { forward: true });
|
||||||
|
} else {
|
||||||
|
lyricWin.setIgnoreMouseEvents(false);
|
||||||
|
}
|
||||||
|
if (isTemp) return;
|
||||||
|
store.set("lyric.config", { ...store.get("lyric.config"), isLock });
|
||||||
|
// 触发窗口更新
|
||||||
|
const config = store.get("lyric.config");
|
||||||
|
mainWin?.webContents.send("update-desktop-lyric-option", config);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initLyricIpc;
|
||||||
36
electron/main/ipc/ipc-shortcut.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { ipcMain } from "electron";
|
||||||
|
import { isShortcutRegistered, registerShortcut, unregisterShortcuts } from "../shortcut";
|
||||||
|
import mainWindow from "../windows/main-window";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化快捷键 IPC 主进程
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const initShortcutIpc = (): void => {
|
||||||
|
// 快捷键是否被注册
|
||||||
|
ipcMain.handle("is-shortcut-registered", (_, shortcut: string) => isShortcutRegistered(shortcut));
|
||||||
|
|
||||||
|
// 注册快捷键
|
||||||
|
ipcMain.handle("register-all-shortcut", (_, allShortcuts: any): string[] | false => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin || !allShortcuts) return false;
|
||||||
|
// 卸载所有快捷键
|
||||||
|
unregisterShortcuts();
|
||||||
|
// 注册快捷键
|
||||||
|
const failedShortcuts: string[] = [];
|
||||||
|
for (const key in allShortcuts) {
|
||||||
|
const shortcut = allShortcuts[key].globalShortcut;
|
||||||
|
if (!shortcut) continue;
|
||||||
|
// 快捷键回调
|
||||||
|
const callback = () => mainWin.webContents.send(key);
|
||||||
|
const isSuccess = registerShortcut(shortcut, callback);
|
||||||
|
if (!isSuccess) failedShortcuts.push(shortcut);
|
||||||
|
}
|
||||||
|
return failedShortcuts;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 卸载所有快捷键
|
||||||
|
ipcMain.on("unregister-all-shortcut", () => unregisterShortcuts());
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initShortcutIpc;
|
||||||
45
electron/main/ipc/ipc-store.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ipcMain } from "electron";
|
||||||
|
import { useStore } from "../store";
|
||||||
|
import type { StoreType } from "../store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 store IPC 主进程
|
||||||
|
*/
|
||||||
|
const initStoreIpc = (): void => {
|
||||||
|
const store = useStore();
|
||||||
|
if (!store) return;
|
||||||
|
|
||||||
|
// 获取配置项
|
||||||
|
ipcMain.handle("store-get", (_event, key: keyof StoreType) => {
|
||||||
|
return store.get(key as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置配置项
|
||||||
|
ipcMain.handle("store-set", (_event, key: keyof StoreType, value: unknown) => {
|
||||||
|
store.set(key as any, value as any);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 判断配置项是否存在
|
||||||
|
ipcMain.handle("store-has", (_event, key: keyof StoreType) => {
|
||||||
|
return store.has(key as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除配置项
|
||||||
|
ipcMain.handle("store-delete", (_event, key: keyof StoreType) => {
|
||||||
|
store.delete(key as any);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置配置(支持指定 keys 或全部重置)
|
||||||
|
ipcMain.handle("store-reset", (_event, keys?: (keyof StoreType)[]) => {
|
||||||
|
if (keys && keys.length > 0) {
|
||||||
|
store.reset(...(keys as any));
|
||||||
|
} else {
|
||||||
|
store.reset();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initStoreIpc;
|
||||||
104
electron/main/ipc/ipc-system.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { app, ipcMain, net, powerSaveBlocker, session } from "electron";
|
||||||
|
import { ipcLog } from "../logger";
|
||||||
|
import { getFonts } from "font-list";
|
||||||
|
import { useStore } from "../store";
|
||||||
|
import mainWindow from "../windows/main-window";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化系统 IPC 通信
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const initSystemIpc = (): void => {
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
/** 阻止系统息屏 ID */
|
||||||
|
let preventId: number | null = null;
|
||||||
|
|
||||||
|
// 是否阻止系统息屏
|
||||||
|
ipcMain.on("prevent-sleep", (_event, val: boolean) => {
|
||||||
|
if (val) {
|
||||||
|
preventId = powerSaveBlocker.start("prevent-display-sleep");
|
||||||
|
ipcLog.info("⏾ System sleep prevention started");
|
||||||
|
} else {
|
||||||
|
if (preventId !== null) {
|
||||||
|
powerSaveBlocker.stop(preventId);
|
||||||
|
ipcLog.info("✅ System sleep prevention stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 退出应用
|
||||||
|
ipcMain.on("quit-app", () => {
|
||||||
|
app.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取系统全部字体
|
||||||
|
ipcMain.handle("get-all-fonts", async () => {
|
||||||
|
try {
|
||||||
|
const fonts = await getFonts();
|
||||||
|
return fonts;
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error(`❌ Failed to get all system fonts: ${error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 取消代理
|
||||||
|
ipcMain.on("remove-proxy", () => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
store.set("proxy", "");
|
||||||
|
if (mainWin) {
|
||||||
|
mainWin?.webContents.session.setProxy({ proxyRules: "" });
|
||||||
|
}
|
||||||
|
ipcLog.info("✅ Remove proxy successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 配置网络代理
|
||||||
|
ipcMain.on("set-proxy", (_, config) => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
||||||
|
store.set("proxy", proxyRules);
|
||||||
|
mainWin?.webContents.session.setProxy({ proxyRules });
|
||||||
|
ipcLog.info("✅ Set proxy successfully:", proxyRules);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 代理测试
|
||||||
|
ipcMain.handle("test-proxy", async (_, config) => {
|
||||||
|
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
||||||
|
try {
|
||||||
|
// 设置代理
|
||||||
|
const ses = session.defaultSession;
|
||||||
|
await ses.setProxy({ proxyRules });
|
||||||
|
// 测试请求
|
||||||
|
const request = net.request({ url: "https://www.baidu.com" });
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
request.on("response", (response) => {
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
ipcLog.info("✅ Proxy test successful");
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
ipcLog.error(`❌ Proxy test failed with status code: ${response.statusCode}`);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
request.on("error", (error) => {
|
||||||
|
ipcLog.error("❌ Error testing proxy:", error);
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
ipcLog.error("❌ Error testing proxy:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置全部设置
|
||||||
|
ipcMain.on("reset-setting", () => {
|
||||||
|
store.reset();
|
||||||
|
ipcLog.info("✅ Reset setting successfully");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initSystemIpc;
|
||||||
15
electron/main/ipc/ipc-thumbar.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ipcMain } from "electron";
|
||||||
|
import { getThumbar } from "../thumbar";
|
||||||
|
|
||||||
|
const initThumbarIpc = (): void => {
|
||||||
|
// 更新工具栏
|
||||||
|
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
|
||||||
|
const thumbar = getThumbar();
|
||||||
|
if (!thumbar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thumbar.updateThumbar(playStatus);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initThumbarIpc;
|
||||||
48
electron/main/ipc/ipc-tray.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { ipcMain } from "electron";
|
||||||
|
import { getMainTray } from "../tray";
|
||||||
|
import lyricWindow from "../windows/lyric-window";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 托盘 IPC
|
||||||
|
*/
|
||||||
|
const initTrayIpc = (): void => {
|
||||||
|
const tray = getMainTray();
|
||||||
|
|
||||||
|
// 音乐播放状态更改
|
||||||
|
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
|
||||||
|
const lyricWin = lyricWindow.getWin();
|
||||||
|
tray?.setPlayState(playStatus ? "play" : "pause");
|
||||||
|
if (!lyricWin) return;
|
||||||
|
lyricWin.webContents.send("play-status-change", playStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 音乐名称更改
|
||||||
|
ipcMain.on("play-song-change", (_, title) => {
|
||||||
|
if (!title) return;
|
||||||
|
// 更改标题
|
||||||
|
tray?.setTitle(title);
|
||||||
|
tray?.setPlayName(title);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 播放模式切换
|
||||||
|
ipcMain.on("play-mode-change", (_, mode) => {
|
||||||
|
tray?.setPlayMode(mode);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 喜欢状态切换
|
||||||
|
ipcMain.on("like-status-change", (_, likeStatus: boolean) => {
|
||||||
|
tray?.setLikeState(likeStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 桌面歌词开关
|
||||||
|
ipcMain.on("toggle-desktop-lyric", (_, val: boolean) => {
|
||||||
|
tray?.setDesktopLyricShow(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 锁定/解锁桌面歌词
|
||||||
|
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => {
|
||||||
|
tray?.setDesktopLyricLock(isLock);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initTrayIpc;
|
||||||
17
electron/main/ipc/ipc-update.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { ipcMain } from "electron";
|
||||||
|
import { checkUpdate, startDownloadUpdate } from "../update";
|
||||||
|
import mainWindow from "../windows/main-window";
|
||||||
|
|
||||||
|
const initUpdateIpc = () => {
|
||||||
|
// 检查更新
|
||||||
|
ipcMain.on("check-update", (_event, showTip) => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
checkUpdate(mainWin, showTip);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开始下载更新
|
||||||
|
ipcMain.on("start-download-update", () => startDownloadUpdate());
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initUpdateIpc;
|
||||||
152
electron/main/ipc/ipc-window.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { app, ipcMain } from "electron";
|
||||||
|
import { useStore } from "../store";
|
||||||
|
import { isDev } from "../utils/config";
|
||||||
|
import { initThumbar } from "../thumbar";
|
||||||
|
import mainWindow from "../windows/main-window";
|
||||||
|
import loadWindow from "../windows/load-window";
|
||||||
|
import loginWindow from "../windows/login-window";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 窗口 IPC 通信
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const initWindowsIpc = (): void => {
|
||||||
|
// store
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
// 当前窗口状态
|
||||||
|
ipcMain.on("win-state", (event) => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
event.returnValue = mainWin?.isMaximized();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载完成
|
||||||
|
ipcMain.on("win-loaded", () => {
|
||||||
|
const loadWin = loadWindow.getWin();
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (loadWin && !loadWin.isDestroyed()) loadWin.destroy();
|
||||||
|
const isMaximized = store.get("window")?.maximized;
|
||||||
|
if (isMaximized) mainWin?.maximize();
|
||||||
|
if (!mainWin) return;
|
||||||
|
mainWin?.show();
|
||||||
|
mainWin?.focus();
|
||||||
|
// 解决窗口不立即显示
|
||||||
|
mainWin?.setAlwaysOnTop(true);
|
||||||
|
// 100ms 后取消置顶
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (mainWin && !mainWin.isDestroyed()) {
|
||||||
|
mainWin.setAlwaysOnTop(false);
|
||||||
|
mainWin.focus();
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
// 初始化缩略图工具栏
|
||||||
|
if (mainWin) initThumbar(mainWin);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 最小化
|
||||||
|
ipcMain.on("win-min", (event) => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
event.preventDefault();
|
||||||
|
mainWin?.minimize();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 最大化
|
||||||
|
ipcMain.on("win-max", () => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
mainWin?.maximize();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 还原
|
||||||
|
ipcMain.on("win-restore", () => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
mainWin?.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 隐藏
|
||||||
|
ipcMain.on("win-hide", () => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
mainWin?.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示
|
||||||
|
ipcMain.on("win-show", () => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
mainWin?.show();
|
||||||
|
mainWin?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重启
|
||||||
|
ipcMain.on("win-reload", () => {
|
||||||
|
app.quit();
|
||||||
|
app.relaunch();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 向主窗口发送事件
|
||||||
|
ipcMain.on("send-to-mainWin", (_, eventName, ...args) => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin || mainWin.isDestroyed() || mainWin.webContents.isDestroyed()) return;
|
||||||
|
mainWin.webContents.send(eventName, ...args);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示进度
|
||||||
|
ipcMain.on("set-bar", (_event, val: number | "none" | "indeterminate" | "error" | "paused") => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
switch (val) {
|
||||||
|
case "none":
|
||||||
|
mainWin?.setProgressBar(-1);
|
||||||
|
break;
|
||||||
|
case "indeterminate":
|
||||||
|
mainWin?.setProgressBar(2, { mode: "indeterminate" });
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
mainWin?.setProgressBar(1, { mode: "error" });
|
||||||
|
break;
|
||||||
|
case "paused":
|
||||||
|
mainWin?.setProgressBar(1, { mode: "paused" });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (typeof val === "number") {
|
||||||
|
mainWin?.setProgressBar(val / 100);
|
||||||
|
} else {
|
||||||
|
mainWin?.setProgressBar(-1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开启控制台
|
||||||
|
ipcMain.on("open-dev-tools", () => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
mainWin?.webContents.openDevTools({
|
||||||
|
title: "SPlayer DevTools",
|
||||||
|
mode: isDev ? "right" : "detach",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开启登录窗口
|
||||||
|
ipcMain.on("open-login-web", () => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
loginWindow.create(mainWin);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开启设置
|
||||||
|
ipcMain.on("open-setting", (_, type) => {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) return;
|
||||||
|
mainWin?.show();
|
||||||
|
mainWin?.focus();
|
||||||
|
mainWin?.webContents.send("openSetting", type);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initWindowsIpc;
|
||||||
@@ -1,701 +0,0 @@
|
|||||||
import {
|
|
||||||
app,
|
|
||||||
ipcMain,
|
|
||||||
BrowserWindow,
|
|
||||||
powerSaveBlocker,
|
|
||||||
screen,
|
|
||||||
shell,
|
|
||||||
dialog,
|
|
||||||
net,
|
|
||||||
session,
|
|
||||||
} from "electron";
|
|
||||||
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
|
|
||||||
import { parseFile } from "music-metadata";
|
|
||||||
import { getFonts } from "font-list";
|
|
||||||
import { MainTray } from "./tray";
|
|
||||||
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 { 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";
|
|
||||||
|
|
||||||
// 注册 ipcMain
|
|
||||||
const initIpcMain = (
|
|
||||||
win: BrowserWindow | null,
|
|
||||||
lyricWin: BrowserWindow | null,
|
|
||||||
loadingWin: BrowserWindow | null,
|
|
||||||
tray: MainTray | null,
|
|
||||||
thumbar: Thumbar | null,
|
|
||||||
store: Store<StoreType>,
|
|
||||||
) => {
|
|
||||||
initWinIpcMain(win, loadingWin, lyricWin, store);
|
|
||||||
initLyricIpcMain(lyricWin, win, store);
|
|
||||||
initTrayIpcMain(tray, win, lyricWin);
|
|
||||||
initThumbarIpcMain(thumbar);
|
|
||||||
initStoreIpcMain(store);
|
|
||||||
initOtherIpcMain(win);
|
|
||||||
};
|
|
||||||
|
|
||||||
// win
|
|
||||||
const initWinIpcMain = (
|
|
||||||
win: BrowserWindow | null,
|
|
||||||
loadingWin: BrowserWindow | null,
|
|
||||||
lyricWin: BrowserWindow | null,
|
|
||||||
store: Store<StoreType>,
|
|
||||||
) => {
|
|
||||||
let preventId: number | null = null;
|
|
||||||
|
|
||||||
// 当前窗口状态
|
|
||||||
ipcMain.on("win-state", (ev) => {
|
|
||||||
ev.returnValue = win?.isMaximized();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 加载完成
|
|
||||||
ipcMain.on("win-loaded", () => {
|
|
||||||
if (loadingWin && !loadingWin.isDestroyed()) loadingWin.close();
|
|
||||||
win?.show();
|
|
||||||
win?.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 最小化
|
|
||||||
ipcMain.on("win-min", (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
win?.minimize();
|
|
||||||
});
|
|
||||||
// 最大化
|
|
||||||
ipcMain.on("win-max", () => {
|
|
||||||
win?.maximize();
|
|
||||||
});
|
|
||||||
// 还原
|
|
||||||
ipcMain.on("win-restore", () => {
|
|
||||||
win?.restore();
|
|
||||||
});
|
|
||||||
// 关闭
|
|
||||||
ipcMain.on("win-close", (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
win?.close();
|
|
||||||
app.quit();
|
|
||||||
});
|
|
||||||
// 隐藏
|
|
||||||
ipcMain.on("win-hide", () => {
|
|
||||||
win?.hide();
|
|
||||||
});
|
|
||||||
// 显示
|
|
||||||
ipcMain.on("win-show", () => {
|
|
||||||
win?.show();
|
|
||||||
});
|
|
||||||
// 重启
|
|
||||||
ipcMain.on("win-reload", () => {
|
|
||||||
app.quit();
|
|
||||||
app.relaunch();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 显示进度
|
|
||||||
ipcMain.on("set-bar", (_, val: number | "none" | "indeterminate" | "error" | "paused") => {
|
|
||||||
switch (val) {
|
|
||||||
case "none":
|
|
||||||
win?.setProgressBar(-1);
|
|
||||||
break;
|
|
||||||
case "indeterminate":
|
|
||||||
win?.setProgressBar(2, { mode: "indeterminate" });
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
win?.setProgressBar(1, { mode: "error" });
|
|
||||||
break;
|
|
||||||
case "paused":
|
|
||||||
win?.setProgressBar(1, { mode: "paused" });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (typeof val === "number") {
|
|
||||||
win?.setProgressBar(val / 100);
|
|
||||||
} else {
|
|
||||||
win?.setProgressBar(-1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 开启控制台
|
|
||||||
ipcMain.on("open-dev-tools", () => {
|
|
||||||
win?.webContents.openDevTools({
|
|
||||||
title: "SPlayer DevTools",
|
|
||||||
mode: isDev ? "right" : "detach",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取系统全部字体
|
|
||||||
ipcMain.handle("get-all-fonts", async () => {
|
|
||||||
try {
|
|
||||||
const fonts = await getFonts();
|
|
||||||
return fonts;
|
|
||||||
} catch (error) {
|
|
||||||
log.error(`❌ Failed to get all system fonts: ${error}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 切换桌面歌词
|
|
||||||
ipcMain.on("change-desktop-lyric", (_, val: boolean) => {
|
|
||||||
val ? lyricWin?.show() : lyricWin?.hide();
|
|
||||||
if (val) lyricWin?.setAlwaysOnTop(true, "screen-saver");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 是否阻止系统息屏
|
|
||||||
ipcMain.on("prevent-sleep", (_, val: boolean) => {
|
|
||||||
if (val) {
|
|
||||||
preventId = powerSaveBlocker.start("prevent-display-sleep");
|
|
||||||
log.info("⏾ System sleep prevention started");
|
|
||||||
} else {
|
|
||||||
if (preventId !== null) {
|
|
||||||
powerSaveBlocker.stop(preventId);
|
|
||||||
log.info("✅ System sleep prevention stopped");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 默认文件夹
|
|
||||||
ipcMain.handle(
|
|
||||||
"get-default-dir",
|
|
||||||
(_, type: "documents" | "downloads" | "pictures" | "music" | "videos"): string => {
|
|
||||||
return app.getPath(type);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 遍历音乐文件
|
|
||||||
ipcMain.handle("get-music-files", async (_, dirPath: string) => {
|
|
||||||
try {
|
|
||||||
// 查找指定目录下的所有音乐文件
|
|
||||||
const musicFiles = await fg("**/*.{mp3,wav,flac}", { cwd: dirPath });
|
|
||||||
// 解析元信息
|
|
||||||
const metadataPromises = musicFiles.map(async (file) => {
|
|
||||||
const filePath = join(dirPath, file);
|
|
||||||
// 处理元信息
|
|
||||||
const { common, format } = await parseFile(filePath);
|
|
||||||
// 获取文件大小
|
|
||||||
const { size } = await fs.stat(filePath);
|
|
||||||
// 判断音质等级
|
|
||||||
let quality: string;
|
|
||||||
if ((format.sampleRate || 0) >= 96000 || (format.bitsPerSample || 0) > 16) {
|
|
||||||
quality = "Hi-Res";
|
|
||||||
} else if ((format.sampleRate || 0) >= 44100) {
|
|
||||||
quality = "HQ";
|
|
||||||
} else {
|
|
||||||
quality = "SQ";
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: getFileID(filePath),
|
|
||||||
name: common.title || basename(filePath),
|
|
||||||
artists: common.artists?.[0] || common.artist,
|
|
||||||
album: common.album || "",
|
|
||||||
alia: common.comment?.[0],
|
|
||||||
duration: (format?.duration ?? 0) * 1000,
|
|
||||||
size: (size / (1024 * 1024)).toFixed(2),
|
|
||||||
path: filePath,
|
|
||||||
quality,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const metadataArray = await Promise.all(metadataPromises);
|
|
||||||
return metadataArray;
|
|
||||||
} catch (error) {
|
|
||||||
log.error("❌ Error fetching music metadata:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取音乐元信息
|
|
||||||
ipcMain.handle("get-music-metadata", async (_, path: string) => {
|
|
||||||
try {
|
|
||||||
const { common, format } = await parseFile(path);
|
|
||||||
return {
|
|
||||||
// 文件名称
|
|
||||||
fileName: basename(path),
|
|
||||||
// 文件大小
|
|
||||||
fileSize: (await fs.stat(path)).size / (1024 * 1024),
|
|
||||||
// 元信息
|
|
||||||
common,
|
|
||||||
// 音质信息
|
|
||||||
format,
|
|
||||||
// md5
|
|
||||||
md5: await getFileMD5(path),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
log.error("❌ Error fetching music metadata:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取音乐歌词
|
|
||||||
ipcMain.handle("get-music-lyric", async (_, path: string): Promise<string> => {
|
|
||||||
try {
|
|
||||||
const { common, native } = await parseFile(path);
|
|
||||||
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");
|
|
||||||
try {
|
|
||||||
await fs.access(lrcFilePath);
|
|
||||||
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
|
|
||||||
return lrcData || "";
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error("❌ Error fetching music lyric:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取音乐封面
|
|
||||||
ipcMain.handle(
|
|
||||||
"get-music-cover",
|
|
||||||
async (_, path: string): Promise<{ data: Buffer; format: string } | null> => {
|
|
||||||
try {
|
|
||||||
const { common } = await parseFile(path);
|
|
||||||
// 获取封面数据
|
|
||||||
const picture = common.picture?.[0];
|
|
||||||
if (picture) {
|
|
||||||
return { data: Buffer.from(picture.data), format: picture.format };
|
|
||||||
} else {
|
|
||||||
const coverFilePath = path.replace(/\.[^.]+$/, ".jpg");
|
|
||||||
try {
|
|
||||||
await fs.access(coverFilePath);
|
|
||||||
const coverData = await fs.readFile(coverFilePath);
|
|
||||||
return { data: coverData, format: "image/jpeg" };
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Error fetching music cover:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 删除文件
|
|
||||||
ipcMain.handle("delete-file", async (_, path: string) => {
|
|
||||||
try {
|
|
||||||
// 规范化路径
|
|
||||||
const resolvedPath = resolve(path);
|
|
||||||
// 检查文件是否存在
|
|
||||||
try {
|
|
||||||
await fs.access(resolvedPath);
|
|
||||||
} catch {
|
|
||||||
throw new Error("❌ File not found");
|
|
||||||
}
|
|
||||||
// 删除文件
|
|
||||||
await fs.unlink(resolvedPath);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
log.error("❌ File delete error", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 打开文件夹
|
|
||||||
ipcMain.on("open-folder", async (_, path: string) => {
|
|
||||||
try {
|
|
||||||
// 规范化路径
|
|
||||||
const resolvedPath = resolve(path);
|
|
||||||
// 检查文件夹是否存在
|
|
||||||
try {
|
|
||||||
await fs.access(resolvedPath);
|
|
||||||
} catch {
|
|
||||||
throw new Error("❌ Folder not found");
|
|
||||||
}
|
|
||||||
// 打开文件夹
|
|
||||||
shell.showItemInFolder(resolvedPath);
|
|
||||||
} catch (error) {
|
|
||||||
log.error("❌ Folder open error", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 图片选择窗口
|
|
||||||
ipcMain.handle("choose-image", async () => {
|
|
||||||
try {
|
|
||||||
const { filePaths } = await dialog.showOpenDialog({
|
|
||||||
properties: ["openFile"],
|
|
||||||
filters: [{ name: "Images", extensions: ["jpg", "jpeg", "png"] }],
|
|
||||||
});
|
|
||||||
if (!filePaths || filePaths.length === 0) return null;
|
|
||||||
return filePaths[0];
|
|
||||||
} catch (error) {
|
|
||||||
log.error("❌ Image choose error", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 路径选择窗口
|
|
||||||
ipcMain.handle("choose-path", async () => {
|
|
||||||
try {
|
|
||||||
const { filePaths } = await dialog.showOpenDialog({
|
|
||||||
title: "选择文件夹",
|
|
||||||
defaultPath: app.getPath("downloads"),
|
|
||||||
properties: ["openDirectory", "createDirectory"],
|
|
||||||
buttonLabel: "选择文件夹",
|
|
||||||
});
|
|
||||||
if (!filePaths || filePaths.length === 0) return null;
|
|
||||||
return filePaths[0];
|
|
||||||
} catch (error) {
|
|
||||||
log.error("❌ Path choose error", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 修改音乐元信息
|
|
||||||
ipcMain.handle("set-music-metadata", async (_, path: string, metadata: any) => {
|
|
||||||
try {
|
|
||||||
const { name, artist, album, alia, lyric, cover } = metadata;
|
|
||||||
// 规范化路径
|
|
||||||
const songPath = resolve(path);
|
|
||||||
const coverPath = cover ? resolve(cover) : null;
|
|
||||||
// 读取歌曲文件
|
|
||||||
const songFile = File.createFromPath(songPath);
|
|
||||||
// 读取封面文件
|
|
||||||
const songCover = coverPath ? Picture.fromPath(coverPath) : null;
|
|
||||||
// 保存元数据
|
|
||||||
Id3v2Settings.forceDefaultVersion = true;
|
|
||||||
Id3v2Settings.defaultVersion = 3;
|
|
||||||
songFile.tag.title = name || "未知曲目";
|
|
||||||
songFile.tag.performers = [artist || "未知艺术家"];
|
|
||||||
songFile.tag.album = album || "未知专辑";
|
|
||||||
songFile.tag.albumArtists = [artist || "未知艺术家"];
|
|
||||||
songFile.tag.lyrics = lyric || "";
|
|
||||||
songFile.tag.description = alia || "";
|
|
||||||
songFile.tag.comment = alia || "";
|
|
||||||
if (songCover) songFile.tag.pictures = [songCover];
|
|
||||||
// 保存元信息
|
|
||||||
songFile.save();
|
|
||||||
songFile.dispose();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
log.error("❌ Error setting music metadata:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 下载文件
|
|
||||||
ipcMain.handle(
|
|
||||||
"download-file",
|
|
||||||
async (
|
|
||||||
_,
|
|
||||||
url: string,
|
|
||||||
options: {
|
|
||||||
fileName: string;
|
|
||||||
fileType: string;
|
|
||||||
path: string;
|
|
||||||
downloadMeta?: boolean;
|
|
||||||
downloadCover?: boolean;
|
|
||||||
downloadLyric?: boolean;
|
|
||||||
saveMetaFile?: boolean;
|
|
||||||
lyric?: string;
|
|
||||||
songData?: any;
|
|
||||||
} = {
|
|
||||||
fileName: "未知文件名",
|
|
||||||
fileType: "mp3",
|
|
||||||
path: app.getPath("downloads"),
|
|
||||||
},
|
|
||||||
): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
if (!win) return false;
|
|
||||||
// 获取配置
|
|
||||||
const {
|
|
||||||
fileName,
|
|
||||||
fileType,
|
|
||||||
path,
|
|
||||||
lyric,
|
|
||||||
downloadMeta,
|
|
||||||
downloadCover,
|
|
||||||
downloadLyric,
|
|
||||||
saveMetaFile,
|
|
||||||
songData,
|
|
||||||
} = options;
|
|
||||||
// 规范化路径
|
|
||||||
const downloadPath = resolve(path);
|
|
||||||
// 检查文件夹是否存在
|
|
||||||
try {
|
|
||||||
await fs.access(downloadPath);
|
|
||||||
} catch {
|
|
||||||
throw new Error("❌ Folder not found");
|
|
||||||
}
|
|
||||||
// 下载文件
|
|
||||||
const songDownload = await download(win, url, {
|
|
||||||
directory: downloadPath,
|
|
||||||
filename: `${fileName}.${fileType}`,
|
|
||||||
});
|
|
||||||
if (!downloadMeta || !songData?.cover) return true;
|
|
||||||
// 下载封面
|
|
||||||
const coverUrl = songData?.coverSize?.l || songData.cover;
|
|
||||||
const coverDownload = await download(win, coverUrl, {
|
|
||||||
directory: downloadPath,
|
|
||||||
filename: `${fileName}.jpg`,
|
|
||||||
});
|
|
||||||
// 读取歌曲文件
|
|
||||||
const songFile = File.createFromPath(songDownload.getSavePath());
|
|
||||||
// 生成图片信息
|
|
||||||
const songCover = Picture.fromPath(coverDownload.getSavePath());
|
|
||||||
// 保存修改后的元数据
|
|
||||||
Id3v2Settings.forceDefaultVersion = true;
|
|
||||||
Id3v2Settings.defaultVersion = 3;
|
|
||||||
songFile.tag.title = songData?.name || "未知曲目";
|
|
||||||
songFile.tag.album = songData?.album?.name || "未知专辑";
|
|
||||||
songFile.tag.performers = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
|
|
||||||
songFile.tag.albumArtists = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
|
|
||||||
if (lyric && downloadLyric) songFile.tag.lyrics = lyric;
|
|
||||||
if (songCover && downloadCover) songFile.tag.pictures = [songCover];
|
|
||||||
// 保存元信息
|
|
||||||
songFile.save();
|
|
||||||
songFile.dispose();
|
|
||||||
// 创建同名歌词文件
|
|
||||||
if (lyric && saveMetaFile && downloadLyric) {
|
|
||||||
const lrcPath = join(downloadPath, `${fileName}.lrc`);
|
|
||||||
await fs.writeFile(lrcPath, lyric, "utf-8");
|
|
||||||
}
|
|
||||||
// 是否删除封面
|
|
||||||
if (!saveMetaFile || !downloadCover) await fs.unlink(coverDownload.getSavePath());
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
log.error("❌ Error downloading file:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 取消代理
|
|
||||||
ipcMain.on("remove-proxy", () => {
|
|
||||||
store.set("proxy", "");
|
|
||||||
win?.webContents.session.setProxy({ proxyRules: "" });
|
|
||||||
log.info("✅ Remove proxy successfully");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 配置网络代理
|
|
||||||
ipcMain.on("set-proxy", (_, config) => {
|
|
||||||
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
|
||||||
store.set("proxy", proxyRules);
|
|
||||||
win?.webContents.session.setProxy({ proxyRules });
|
|
||||||
log.info("✅ Set proxy successfully:", proxyRules);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 代理测试
|
|
||||||
ipcMain.handle("test-proxy", async (_, config) => {
|
|
||||||
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
|
||||||
try {
|
|
||||||
// 设置代理
|
|
||||||
const ses = session.defaultSession;
|
|
||||||
await ses.setProxy({ proxyRules });
|
|
||||||
// 测试请求
|
|
||||||
const request = net.request({ url: "https://www.baidu.com" });
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
request.on("response", (response) => {
|
|
||||||
if (response.statusCode === 200) {
|
|
||||||
log.info("✅ Proxy test successful");
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
log.error(`❌ Proxy test failed with status code: ${response.statusCode}`);
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
request.on("error", (error) => {
|
|
||||||
log.error("❌ Error testing proxy:", error);
|
|
||||||
resolve(false);
|
|
||||||
});
|
|
||||||
request.end();
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
log.error("❌ Error testing proxy:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 重置全部设置
|
|
||||||
ipcMain.on("reset-setting", () => {
|
|
||||||
store.reset();
|
|
||||||
log.info("✅ Reset setting successfully");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查更新
|
|
||||||
ipcMain.on("check-update", (_, showTip) => checkUpdate(win!, showTip));
|
|
||||||
|
|
||||||
// 开始下载更新
|
|
||||||
ipcMain.on("start-download-update", () => startDownloadUpdate());
|
|
||||||
};
|
|
||||||
|
|
||||||
// lyric
|
|
||||||
const initLyricIpcMain = (
|
|
||||||
lyricWin: BrowserWindow | null,
|
|
||||||
mainWin: BrowserWindow | null,
|
|
||||||
store: Store<StoreType>,
|
|
||||||
): void => {
|
|
||||||
// 音乐名称更改
|
|
||||||
ipcMain.on("play-song-change", (_, title) => {
|
|
||||||
if (!title) return;
|
|
||||||
lyricWin?.webContents.send("play-song-change", title);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 音乐歌词更改
|
|
||||||
ipcMain.on("play-lyric-change", (_, lyricData) => {
|
|
||||||
if (!lyricData) return;
|
|
||||||
lyricWin?.webContents.send("play-lyric-change", lyricData);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取窗口位置
|
|
||||||
ipcMain.handle("get-window-bounds", () => {
|
|
||||||
return lyricWin?.getBounds();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取屏幕尺寸
|
|
||||||
ipcMain.handle("get-screen-size", () => {
|
|
||||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
|
||||||
return { width, height };
|
|
||||||
});
|
|
||||||
|
|
||||||
// 移动窗口
|
|
||||||
ipcMain.on("move-window", (_, x, y, width, height) => {
|
|
||||||
lyricWin?.setBounds({ x, y, width, height });
|
|
||||||
// 保存配置
|
|
||||||
store.set("lyric", { ...store.get("lyric"), x, y, width, height });
|
|
||||||
// 保持置顶
|
|
||||||
lyricWin?.setAlwaysOnTop(true, "screen-saver");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新高度
|
|
||||||
ipcMain.on("update-window-height", (_, height) => {
|
|
||||||
if (!lyricWin) return;
|
|
||||||
const { width } = lyricWin.getBounds();
|
|
||||||
// 更新窗口高度
|
|
||||||
lyricWin.setBounds({ width, height });
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取配置
|
|
||||||
ipcMain.handle("get-desktop-lyric-option", () => {
|
|
||||||
return store.get("lyric");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保存配置
|
|
||||||
ipcMain.on("set-desktop-lyric-option", (_, option, callback: boolean = false) => {
|
|
||||||
store.set("lyric", option);
|
|
||||||
// 触发窗口更新
|
|
||||||
if (callback && lyricWin) {
|
|
||||||
lyricWin.webContents.send("desktop-lyric-option-change", option);
|
|
||||||
}
|
|
||||||
mainWin?.webContents.send("desktop-lyric-option-change", option);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 发送主程序事件
|
|
||||||
ipcMain.on("send-main-event", (_, name, val) => {
|
|
||||||
mainWin?.webContents.send(name, val);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 关闭桌面歌词
|
|
||||||
ipcMain.on("closeDesktopLyric", () => {
|
|
||||||
lyricWin?.hide();
|
|
||||||
mainWin?.webContents.send("closeDesktopLyric");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 锁定/解锁桌面歌词
|
|
||||||
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => {
|
|
||||||
if (!lyricWin) return;
|
|
||||||
// 是否穿透
|
|
||||||
if (isLock) {
|
|
||||||
lyricWin.setIgnoreMouseEvents(true, { forward: true });
|
|
||||||
} else {
|
|
||||||
lyricWin.setIgnoreMouseEvents(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// tray
|
|
||||||
const initTrayIpcMain = (
|
|
||||||
tray: MainTray | null,
|
|
||||||
win: BrowserWindow | null,
|
|
||||||
lyricWin: BrowserWindow | null,
|
|
||||||
): void => {
|
|
||||||
// 音乐播放状态更改
|
|
||||||
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
|
|
||||||
tray?.setPlayState(playStatus ? "play" : "pause");
|
|
||||||
lyricWin?.webContents.send("play-status-change", playStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 音乐名称更改
|
|
||||||
ipcMain.on("play-song-change", (_, title) => {
|
|
||||||
if (!title) return;
|
|
||||||
// 更改标题
|
|
||||||
win?.setTitle(title);
|
|
||||||
tray?.setTitle(title);
|
|
||||||
tray?.setPlayName(title);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 播放模式切换
|
|
||||||
ipcMain.on("play-mode-change", (_, mode) => {
|
|
||||||
tray?.setPlayMode(mode);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 喜欢状态切换
|
|
||||||
ipcMain.on("like-status-change", (_, likeStatus: boolean) => {
|
|
||||||
tray?.setLikeState(likeStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 桌面歌词开关
|
|
||||||
ipcMain.on("change-desktop-lyric", (_, val: boolean) => {
|
|
||||||
tray?.setDesktopLyricShow(val);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 锁定/解锁桌面歌词
|
|
||||||
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => {
|
|
||||||
tray?.setDesktopLyricLock(isLock);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// thumbar
|
|
||||||
const initThumbarIpcMain = (thumbar: Thumbar | null): void => {
|
|
||||||
if (!thumbar) return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// store
|
|
||||||
const initStoreIpcMain = (store: Store<StoreType>): void => {
|
|
||||||
if (!store) return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// other
|
|
||||||
const initOtherIpcMain = (mainWin: BrowserWindow | null): void => {
|
|
||||||
// 快捷键是否被注册
|
|
||||||
ipcMain.handle("is-shortcut-registered", (_, shortcut: string) => isShortcutRegistered(shortcut));
|
|
||||||
|
|
||||||
// 注册快捷键
|
|
||||||
ipcMain.handle("register-all-shortcut", (_, allShortcuts: any): string[] | false => {
|
|
||||||
if (!mainWin || !allShortcuts) return false;
|
|
||||||
// 卸载所有快捷键
|
|
||||||
unregisterShortcuts();
|
|
||||||
// 注册快捷键
|
|
||||||
const failedShortcuts: string[] = [];
|
|
||||||
for (const key in allShortcuts) {
|
|
||||||
const shortcut = allShortcuts[key].globalShortcut;
|
|
||||||
if (!shortcut) continue;
|
|
||||||
// 快捷键回调
|
|
||||||
const callback = () => mainWin.webContents.send(key);
|
|
||||||
const isSuccess = registerShortcut(shortcut, callback);
|
|
||||||
if (!isSuccess) failedShortcuts.push(shortcut);
|
|
||||||
}
|
|
||||||
return failedShortcuts;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 卸载所有快捷键
|
|
||||||
ipcMain.on("unregister-all-shortcut", () => unregisterShortcuts());
|
|
||||||
};
|
|
||||||
|
|
||||||
export default initIpcMain;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// 日志输出
|
|
||||||
import { join } from "path";
|
|
||||||
import { app } from "electron";
|
|
||||||
import { isDev } from "./utils";
|
|
||||||
import log from "electron-log";
|
|
||||||
|
|
||||||
// 绑定事件
|
|
||||||
Object.assign(console, log.functions);
|
|
||||||
|
|
||||||
// 日志配置
|
|
||||||
log.transports.file.level = "info";
|
|
||||||
log.transports.file.maxSize = 2 * 1024 * 1024;
|
|
||||||
if (log.transports.ipc) log.transports.ipc.level = false;
|
|
||||||
|
|
||||||
// 控制台输出
|
|
||||||
log.transports.console.useStyles = true;
|
|
||||||
|
|
||||||
// 文件输出
|
|
||||||
log.transports.file.format = "{h}:{i}:{s}:{ms} {text}";
|
|
||||||
|
|
||||||
// 本地输出
|
|
||||||
if (!isDev) {
|
|
||||||
log.transports.file.resolvePathFn = () =>
|
|
||||||
join(app.getPath("documents"), "/SPlayer/SPlayer-log.txt");
|
|
||||||
} else {
|
|
||||||
log.transports.file.level = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("📃 logger initialized");
|
|
||||||
|
|
||||||
export default log;
|
|
||||||
47
electron/main/logger/index.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// 日志输出
|
||||||
|
import { existsSync, mkdirSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { app } from "electron";
|
||||||
|
import log from "electron-log";
|
||||||
|
|
||||||
|
// 日志文件路径
|
||||||
|
const logDir = join(app.getPath("logs"));
|
||||||
|
|
||||||
|
// 是否存在日志目录
|
||||||
|
if (!existsSync(logDir)) mkdirSync(logDir);
|
||||||
|
|
||||||
|
// 获取日期 - YYYY-MM-DD
|
||||||
|
const dateString = new Date().toISOString().slice(0, 10);
|
||||||
|
const logFilePath = join(logDir, `${dateString}.log`);
|
||||||
|
|
||||||
|
// 配置日志系统
|
||||||
|
log.transports.console.useStyles = true; // 颜色输出
|
||||||
|
log.transports.file.level = "info"; // 仅记录 info 及以上级别
|
||||||
|
log.transports.file.resolvePathFn = (): string => logFilePath; // 日志文件路径
|
||||||
|
log.transports.file.maxSize = 2 * 1024 * 1024; // 文件最大 2MB
|
||||||
|
|
||||||
|
// 日志格式化
|
||||||
|
// log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}] [{level}] [{scope}] {text}";
|
||||||
|
|
||||||
|
// 绑定默认事件
|
||||||
|
const defaultLog = log.scope("default");
|
||||||
|
console.log = defaultLog.log;
|
||||||
|
console.info = defaultLog.info;
|
||||||
|
console.warn = defaultLog.warn;
|
||||||
|
console.error = defaultLog.error;
|
||||||
|
|
||||||
|
// 分作用域导出
|
||||||
|
export { defaultLog };
|
||||||
|
export const ipcLog = log.scope("ipc");
|
||||||
|
export const trayLog = log.scope("tray");
|
||||||
|
export const thumbarLog = log.scope("thumbar");
|
||||||
|
export const storeLog = log.scope("store");
|
||||||
|
export const updateLog = log.scope("update");
|
||||||
|
export const systemLog = log.scope("system");
|
||||||
|
export const configLog = log.scope("config");
|
||||||
|
export const windowsLog = log.scope("windows");
|
||||||
|
export const processLog = log.scope("process");
|
||||||
|
export const preloadLog = log.scope("preload");
|
||||||
|
export const rendererLog = log.scope("renderer");
|
||||||
|
export const shortcutLog = log.scope("shortcut");
|
||||||
|
export const serverLog = log.scope("server");
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { BrowserWindow, globalShortcut } from "electron";
|
|
||||||
import { isDev } from "./utils";
|
|
||||||
import log from "../main/logger";
|
|
||||||
|
|
||||||
// 注册快捷键并检查
|
|
||||||
export const registerShortcut = (shortcut: string, callback: () => void): boolean => {
|
|
||||||
try {
|
|
||||||
const success = globalShortcut.register(shortcut, callback);
|
|
||||||
if (!success) {
|
|
||||||
log.error(`❌ Failed to register shortcut: ${shortcut}`);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
log.info(`✅ Shortcut registered: ${shortcut}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error(`ℹ️ Error registering shortcut ${shortcut}:`, error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查快捷键是否被注册
|
|
||||||
export const isShortcutRegistered = (shortcut: string): boolean => {
|
|
||||||
return globalShortcut.isRegistered(shortcut);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 卸载所有快捷键
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
30
electron/main/shortcut/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { globalShortcut } from "electron";
|
||||||
|
import { shortcutLog } from "../logger";
|
||||||
|
|
||||||
|
// 注册快捷键并检查
|
||||||
|
export const registerShortcut = (shortcut: string, callback: () => void): boolean => {
|
||||||
|
try {
|
||||||
|
const success = globalShortcut.register(shortcut, callback);
|
||||||
|
if (!success) {
|
||||||
|
shortcutLog.error(`❌ Failed to register shortcut: ${shortcut}`);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
shortcutLog.info(`✅ Shortcut registered: ${shortcut}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
shortcutLog.error(`ℹ️ Error registering shortcut ${shortcut}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查快捷键是否被注册
|
||||||
|
export const isShortcutRegistered = (shortcut: string): boolean => {
|
||||||
|
return globalShortcut.isRegistered(shortcut);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 卸载所有快捷键
|
||||||
|
export const unregisterShortcuts = () => {
|
||||||
|
globalShortcut.unregisterAll();
|
||||||
|
shortcutLog.info("🚫 All shortcuts unregistered.");
|
||||||
|
};
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import Store from "electron-store";
|
|
||||||
import log from "./logger";
|
|
||||||
|
|
||||||
log.info("🌱 Store init");
|
|
||||||
|
|
||||||
export interface StoreType {
|
|
||||||
window: {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
x?: number;
|
|
||||||
y?: number;
|
|
||||||
};
|
|
||||||
lyric: {
|
|
||||||
fontSize: number;
|
|
||||||
mainColor: string;
|
|
||||||
shadowColor: string;
|
|
||||||
// 窗口位置
|
|
||||||
x?: number;
|
|
||||||
y?: number;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
};
|
|
||||||
proxy: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化仓库
|
|
||||||
const store = new Store<StoreType>({
|
|
||||||
defaults: {
|
|
||||||
window: {
|
|
||||||
width: 1280,
|
|
||||||
height: 800,
|
|
||||||
},
|
|
||||||
lyric: {
|
|
||||||
fontSize: 30,
|
|
||||||
mainColor: "#fff",
|
|
||||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 800,
|
|
||||||
height: 180,
|
|
||||||
},
|
|
||||||
proxy: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default store;
|
|
||||||
52
electron/main/store/index.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { screen } from "electron";
|
||||||
|
import { storeLog } from "../logger";
|
||||||
|
import type { LyricConfig } from "../../../src/types/desktop-lyric";
|
||||||
|
import defaultLyricConfig from "../../../src/assets/data/lyricConfig";
|
||||||
|
import Store from "electron-store";
|
||||||
|
|
||||||
|
storeLog.info("🌱 Store init");
|
||||||
|
|
||||||
|
export interface StoreType {
|
||||||
|
window: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
maximized?: boolean;
|
||||||
|
};
|
||||||
|
lyric: {
|
||||||
|
// 窗口位置
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
// 配置
|
||||||
|
config?: LyricConfig;
|
||||||
|
};
|
||||||
|
proxy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 Store
|
||||||
|
* @returns Store<StoreType>
|
||||||
|
*/
|
||||||
|
export const useStore = () => {
|
||||||
|
// 获取主屏幕
|
||||||
|
const screenData = screen.getPrimaryDisplay();
|
||||||
|
return new Store<StoreType>({
|
||||||
|
defaults: {
|
||||||
|
window: {
|
||||||
|
width: 1280,
|
||||||
|
height: 800,
|
||||||
|
},
|
||||||
|
lyric: {
|
||||||
|
x: screenData.workAreaSize.width / 2 - 400,
|
||||||
|
y: screenData.workAreaSize.height - 90,
|
||||||
|
width: 800,
|
||||||
|
height: 136,
|
||||||
|
config: defaultLyricConfig,
|
||||||
|
},
|
||||||
|
proxy: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BrowserWindow, nativeImage, nativeTheme, ThumbarButton } from "electron";
|
import { BrowserWindow, nativeImage, nativeTheme, ThumbarButton } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { isWin } from "./utils";
|
import { isWin } from "../utils/config";
|
||||||
import log from "./logger";
|
import { thumbarLog } from "../logger";
|
||||||
|
|
||||||
enum ThumbarKeys {
|
enum ThumbarKeys {
|
||||||
Play = "play",
|
Play = "play",
|
||||||
@@ -14,8 +14,12 @@ type ThumbarMap = Map<ThumbarKeys, ThumbarButton>;
|
|||||||
|
|
||||||
export interface Thumbar {
|
export interface Thumbar {
|
||||||
clearThumbar(): void;
|
clearThumbar(): void;
|
||||||
|
updateThumbar(playing: boolean, clean?: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 缩略图单例
|
||||||
|
let thumbar: Thumbar | null = null;
|
||||||
|
|
||||||
// 工具栏图标
|
// 工具栏图标
|
||||||
const thumbarIcon = (filename: string) => {
|
const thumbarIcon = (filename: string) => {
|
||||||
// 是否为暗色
|
// 是否为暗色
|
||||||
@@ -32,12 +36,12 @@ const createThumbarButtons = (win: BrowserWindow): ThumbarMap => {
|
|||||||
.set(ThumbarKeys.Prev, {
|
.set(ThumbarKeys.Prev, {
|
||||||
tooltip: "上一曲",
|
tooltip: "上一曲",
|
||||||
icon: thumbarIcon("prev"),
|
icon: thumbarIcon("prev"),
|
||||||
click: () => win.webContents.send("play-prev"),
|
click: () => win.webContents.send("playPrev"),
|
||||||
})
|
})
|
||||||
.set(ThumbarKeys.Next, {
|
.set(ThumbarKeys.Next, {
|
||||||
tooltip: "下一曲",
|
tooltip: "下一曲",
|
||||||
icon: thumbarIcon("next"),
|
icon: thumbarIcon("next"),
|
||||||
click: () => win.webContents.send("play-next"),
|
click: () => win.webContents.send("playNext"),
|
||||||
})
|
})
|
||||||
.set(ThumbarKeys.Play, {
|
.set(ThumbarKeys.Play, {
|
||||||
tooltip: "播放",
|
tooltip: "播放",
|
||||||
@@ -47,7 +51,7 @@ const createThumbarButtons = (win: BrowserWindow): ThumbarMap => {
|
|||||||
.set(ThumbarKeys.Pause, {
|
.set(ThumbarKeys.Pause, {
|
||||||
tooltip: "暂停",
|
tooltip: "暂停",
|
||||||
icon: thumbarIcon("pause"),
|
icon: thumbarIcon("pause"),
|
||||||
click: () => win.webContents.send("play-pause"),
|
click: () => win.webContents.send("pause"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,6 +66,9 @@ class createThumbar implements Thumbar {
|
|||||||
private _next: ThumbarButton;
|
private _next: ThumbarButton;
|
||||||
private _play: ThumbarButton;
|
private _play: ThumbarButton;
|
||||||
private _pause: ThumbarButton;
|
private _pause: ThumbarButton;
|
||||||
|
// 当前播放状态
|
||||||
|
private _isPlaying: boolean = false;
|
||||||
|
|
||||||
constructor(win: BrowserWindow) {
|
constructor(win: BrowserWindow) {
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
this._win = win;
|
this._win = win;
|
||||||
@@ -73,26 +80,62 @@ class createThumbar implements Thumbar {
|
|||||||
this._next = this._thumbar.get(ThumbarKeys.Next)!;
|
this._next = this._thumbar.get(ThumbarKeys.Next)!;
|
||||||
// 初始化工具栏
|
// 初始化工具栏
|
||||||
this.updateThumbar();
|
this.updateThumbar();
|
||||||
|
// 监听主题变化
|
||||||
|
this.initThemeListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化主题监听器
|
||||||
|
private initThemeListener() {
|
||||||
|
nativeTheme.on("updated", () => {
|
||||||
|
this.refreshThumbarButtons();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新工具栏按钮(主题变化时)
|
||||||
|
private refreshThumbarButtons() {
|
||||||
|
// 重新创建按钮
|
||||||
|
this._thumbar = createThumbarButtons(this._win);
|
||||||
|
this._play = this._thumbar.get(ThumbarKeys.Play)!;
|
||||||
|
this._pause = this._thumbar.get(ThumbarKeys.Pause)!;
|
||||||
|
this._prev = this._thumbar.get(ThumbarKeys.Prev)!;
|
||||||
|
this._next = this._thumbar.get(ThumbarKeys.Next)!;
|
||||||
|
// 更新工具栏
|
||||||
|
this.updateThumbar(this._isPlaying);
|
||||||
|
}
|
||||||
|
|
||||||
// 更新工具栏
|
// 更新工具栏
|
||||||
private updateThumbar(playing: boolean = false, clean: boolean = false) {
|
updateThumbar(playing: boolean = false, clean: boolean = false) {
|
||||||
|
this._isPlaying = playing;
|
||||||
if (clean) return this.clearThumbar();
|
if (clean) return this.clearThumbar();
|
||||||
this._win.setThumbarButtons([this._prev, playing ? this._pause : this._play, this._next]);
|
this._win.setThumbarButtons([this._prev, playing ? this._pause : this._play, this._next]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除工具栏
|
// 清除工具栏
|
||||||
clearThumbar() {
|
clearThumbar() {
|
||||||
this._win.setThumbarButtons([]);
|
this._win.setThumbarButtons([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化缩略图工具栏
|
||||||
|
* @param win 窗口
|
||||||
|
* @returns 缩略图工具栏
|
||||||
|
*/
|
||||||
export const initThumbar = (win: BrowserWindow) => {
|
export const initThumbar = (win: BrowserWindow) => {
|
||||||
try {
|
try {
|
||||||
// 若非 Win
|
// 若非 Win
|
||||||
if (!isWin) return null;
|
if (!isWin) return null;
|
||||||
log.info("🚀 ThumbarButtons Startup");
|
thumbarLog.info("🚀 ThumbarButtons Startup");
|
||||||
return new createThumbar(win);
|
thumbar = new createThumbar(win);
|
||||||
|
return thumbar;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("❌ ThumbarButtons Error", error);
|
thumbarLog.error("❌ ThumbarButtons Error", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缩略图工具栏
|
||||||
|
* @returns 缩略图工具栏
|
||||||
|
*/
|
||||||
|
export const getThumbar = () => thumbar;
|
||||||
@@ -7,9 +7,11 @@ import {
|
|||||||
nativeImage,
|
nativeImage,
|
||||||
nativeTheme,
|
nativeTheme,
|
||||||
} from "electron";
|
} from "electron";
|
||||||
import { isWin, isLinux, isDev, appName } from "./utils";
|
import { isWin, appName } from "../utils/config";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import log from "./logger";
|
import { trayLog } from "../logger";
|
||||||
|
import { useStore } from "../store";
|
||||||
|
import lyricWindow from "../windows/lyric-window";
|
||||||
|
|
||||||
// 播放模式
|
// 播放模式
|
||||||
type PlayMode = "repeat" | "repeat-once" | "shuffle";
|
type PlayMode = "repeat" | "repeat-once" | "shuffle";
|
||||||
@@ -34,20 +36,20 @@ export interface MainTray {
|
|||||||
destroyTray(): void;
|
destroyTray(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 托盘单例
|
||||||
|
let mainTrayInstance: MainTray | null = null;
|
||||||
|
|
||||||
// 托盘图标
|
// 托盘图标
|
||||||
const trayIcon = (filename: string) => {
|
const trayIcon = (filename: string) => {
|
||||||
const rootPath = isDev
|
// const rootPath = isDev
|
||||||
? join(__dirname, "../../public/icons/tray")
|
// ? join(__dirname, "../../public/icons/tray")
|
||||||
: join(app.getAppPath(), "../../public/icons/tray");
|
// : join(app.getAppPath(), "../../public/icons/tray");
|
||||||
return nativeImage.createFromPath(join(rootPath, filename));
|
// return nativeImage.createFromPath(join(rootPath, filename));
|
||||||
// return nativeImage.createFromPath(join(__dirname, `../../public/icons/tray/${filename}`));
|
return nativeImage.createFromPath(join(__dirname, `../../public/icons/tray/${filename}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 托盘菜单
|
// 托盘菜单
|
||||||
const createTrayMenu = (
|
const createTrayMenu = (win: BrowserWindow): MenuItemConstructorOptions[] => {
|
||||||
win: BrowserWindow,
|
|
||||||
lyricWin: BrowserWindow,
|
|
||||||
): MenuItemConstructorOptions[] => {
|
|
||||||
// 区分明暗图标
|
// 区分明暗图标
|
||||||
const showIcon = (iconName: string) => {
|
const showIcon = (iconName: string) => {
|
||||||
const isDark = nativeTheme.shouldUseDarkColors;
|
const isDark = nativeTheme.shouldUseDarkColors;
|
||||||
@@ -143,7 +145,16 @@ const createTrayMenu = (
|
|||||||
label: `${desktopLyricLock ? "解锁" : "锁定"}桌面歌词`,
|
label: `${desktopLyricLock ? "解锁" : "锁定"}桌面歌词`,
|
||||||
icon: showIcon(desktopLyricLock ? "lock" : "unlock"),
|
icon: showIcon(desktopLyricLock ? "lock" : "unlock"),
|
||||||
visible: desktopLyricShow,
|
visible: desktopLyricShow,
|
||||||
click: () => lyricWin.webContents.send("toogleDesktopLyricLock", !desktopLyricLock),
|
click: () => {
|
||||||
|
const store = useStore();
|
||||||
|
// 更新锁定状态
|
||||||
|
store.set("lyric.config", { ...store.get("lyric.config"), isLock: !desktopLyricLock });
|
||||||
|
// 触发窗口更新
|
||||||
|
const config = store.get("lyric.config");
|
||||||
|
const lyricWin = lyricWindow.getWin();
|
||||||
|
if (!lyricWin) return;
|
||||||
|
lyricWin.webContents.send("update-desktop-lyric-option", config);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
@@ -179,14 +190,13 @@ const createTrayMenu = (
|
|||||||
class CreateTray implements MainTray {
|
class CreateTray implements MainTray {
|
||||||
// 窗口
|
// 窗口
|
||||||
private _win: BrowserWindow;
|
private _win: BrowserWindow;
|
||||||
private _lyricWin: BrowserWindow;
|
|
||||||
// 托盘
|
// 托盘
|
||||||
private _tray: Tray;
|
private _tray: Tray;
|
||||||
// 菜单
|
// 菜单
|
||||||
private _menu: MenuItemConstructorOptions[];
|
private _menu: MenuItemConstructorOptions[];
|
||||||
private _contextMenu: Menu;
|
private _contextMenu: Menu;
|
||||||
|
|
||||||
constructor(win: BrowserWindow, lyricWin: BrowserWindow) {
|
constructor(win: BrowserWindow) {
|
||||||
// 托盘图标
|
// 托盘图标
|
||||||
const icon = trayIcon(isWin ? "tray.ico" : "tray@32.png").resize({
|
const icon = trayIcon(isWin ? "tray.ico" : "tray@32.png").resize({
|
||||||
height: 32,
|
height: 32,
|
||||||
@@ -194,9 +204,8 @@ class CreateTray implements MainTray {
|
|||||||
});
|
});
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
this._win = win;
|
this._win = win;
|
||||||
this._lyricWin = lyricWin;
|
|
||||||
this._tray = new Tray(icon);
|
this._tray = new Tray(icon);
|
||||||
this._menu = createTrayMenu(this._win, this._lyricWin);
|
this._menu = createTrayMenu(this._win);
|
||||||
this._contextMenu = Menu.buildFromTemplate(this._menu);
|
this._contextMenu = Menu.buildFromTemplate(this._menu);
|
||||||
// 初始化事件
|
// 初始化事件
|
||||||
this.initTrayMenu();
|
this.initTrayMenu();
|
||||||
@@ -205,7 +214,7 @@ class CreateTray implements MainTray {
|
|||||||
}
|
}
|
||||||
// 托盘菜单
|
// 托盘菜单
|
||||||
private initTrayMenu() {
|
private initTrayMenu() {
|
||||||
this._menu = createTrayMenu(this._win, this._lyricWin);
|
this._menu = createTrayMenu(this._win);
|
||||||
this._contextMenu = Menu.buildFromTemplate(this._menu);
|
this._contextMenu = Menu.buildFromTemplate(this._menu);
|
||||||
this._tray.setContextMenu(this._contextMenu);
|
this._tray.setContextMenu(this._contextMenu);
|
||||||
}
|
}
|
||||||
@@ -219,11 +228,19 @@ class CreateTray implements MainTray {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 设置标题
|
// 设置标题
|
||||||
|
/**
|
||||||
|
* 设置标题
|
||||||
|
* @param title 标题
|
||||||
|
*/
|
||||||
setTitle(title: string) {
|
setTitle(title: string) {
|
||||||
|
this._win.setTitle(title);
|
||||||
this._tray.setTitle(title);
|
this._tray.setTitle(title);
|
||||||
this._tray.setToolTip(title);
|
this._tray.setToolTip(title);
|
||||||
}
|
}
|
||||||
// 设置播放名称
|
/**
|
||||||
|
* 设置播放名称
|
||||||
|
* @param name 播放名称
|
||||||
|
*/
|
||||||
setPlayName(name: string) {
|
setPlayName(name: string) {
|
||||||
// 超长处理
|
// 超长处理
|
||||||
if (name.length > 20) name = name.slice(0, 20) + "...";
|
if (name.length > 20) name = name.slice(0, 20) + "...";
|
||||||
@@ -231,52 +248,80 @@ class CreateTray implements MainTray {
|
|||||||
// 更新菜单
|
// 更新菜单
|
||||||
this.initTrayMenu();
|
this.initTrayMenu();
|
||||||
}
|
}
|
||||||
// 设置播放状态
|
/**
|
||||||
|
* 设置播放状态
|
||||||
|
* @param state 播放状态
|
||||||
|
*/
|
||||||
setPlayState(state: PlayState) {
|
setPlayState(state: PlayState) {
|
||||||
playState = state;
|
playState = state;
|
||||||
// 更新菜单
|
// 更新菜单
|
||||||
this.initTrayMenu();
|
this.initTrayMenu();
|
||||||
}
|
}
|
||||||
// 设置播放模式
|
/**
|
||||||
|
* 设置播放模式
|
||||||
|
* @param mode 播放模式
|
||||||
|
*/
|
||||||
setPlayMode(mode: PlayMode) {
|
setPlayMode(mode: PlayMode) {
|
||||||
playMode = mode;
|
playMode = mode;
|
||||||
// 更新菜单
|
// 更新菜单
|
||||||
this.initTrayMenu();
|
this.initTrayMenu();
|
||||||
}
|
}
|
||||||
// 设置喜欢状态
|
/**
|
||||||
|
* 设置喜欢状态
|
||||||
|
* @param like 喜欢状态
|
||||||
|
*/
|
||||||
setLikeState(like: boolean) {
|
setLikeState(like: boolean) {
|
||||||
likeSong = like;
|
likeSong = like;
|
||||||
// 更新菜单
|
// 更新菜单
|
||||||
this.initTrayMenu();
|
this.initTrayMenu();
|
||||||
}
|
}
|
||||||
// 桌面歌词开关
|
/**
|
||||||
|
* 桌面歌词开关
|
||||||
|
* @param show 桌面歌词开关状态
|
||||||
|
*/
|
||||||
setDesktopLyricShow(show: boolean) {
|
setDesktopLyricShow(show: boolean) {
|
||||||
desktopLyricShow = show;
|
desktopLyricShow = show;
|
||||||
// 更新菜单
|
// 更新菜单
|
||||||
this.initTrayMenu();
|
this.initTrayMenu();
|
||||||
}
|
}
|
||||||
// 锁定桌面歌词
|
/**
|
||||||
|
* 锁定桌面歌词
|
||||||
|
* @param lock 锁定桌面歌词状态
|
||||||
|
*/
|
||||||
setDesktopLyricLock(lock: boolean) {
|
setDesktopLyricLock(lock: boolean) {
|
||||||
desktopLyricLock = lock;
|
desktopLyricLock = lock;
|
||||||
// 更新菜单
|
// 更新菜单
|
||||||
this.initTrayMenu();
|
this.initTrayMenu();
|
||||||
}
|
}
|
||||||
// 销毁托盘
|
/**
|
||||||
|
* 销毁托盘
|
||||||
|
*/
|
||||||
destroyTray() {
|
destroyTray() {
|
||||||
this._tray.destroy();
|
this._tray.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initTray = (win: BrowserWindow, lyricWin: BrowserWindow) => {
|
/**
|
||||||
|
* 初始化托盘
|
||||||
|
* @param win 主窗口
|
||||||
|
* @param lyricWin 歌词窗口
|
||||||
|
* @returns 托盘实例
|
||||||
|
*/
|
||||||
|
export const initTray = (win: BrowserWindow) => {
|
||||||
try {
|
try {
|
||||||
// 若为 MacOS
|
trayLog.info("🚀 Tray Process Startup");
|
||||||
if (isWin || isLinux || isDev) {
|
const tray = new CreateTray(win);
|
||||||
log.info("🚀 Tray Process Startup");
|
// 保存单例实例
|
||||||
return new CreateTray(win, lyricWin);
|
mainTrayInstance = tray;
|
||||||
}
|
return tray;
|
||||||
return null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("❌ Tray Process Error", error);
|
trayLog.error("❌ Tray Process Error", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取托盘实例
|
||||||
|
* @returns 托盘实例
|
||||||
|
*/
|
||||||
|
export const getMainTray = (): MainTray | null => mainTrayInstance;
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
import { type BrowserWindow } from "electron";
|
import { app, type BrowserWindow } from "electron";
|
||||||
|
import { updateLog } from "../logger";
|
||||||
import electronUpdater from "electron-updater";
|
import electronUpdater from "electron-updater";
|
||||||
import log from "./logger";
|
import { isDev } from "../utils/config";
|
||||||
|
|
||||||
// import
|
// import
|
||||||
const { autoUpdater } = electronUpdater;
|
const { autoUpdater } = electronUpdater;
|
||||||
|
|
||||||
|
// 开发环境启用
|
||||||
|
if (isDev) {
|
||||||
|
Object.defineProperty(app, "isPackaged", {
|
||||||
|
get: () => true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 更新源
|
// 更新源
|
||||||
autoUpdater.setFeedURL({
|
autoUpdater.setFeedURL({
|
||||||
provider: "github",
|
provider: "github",
|
||||||
@@ -28,19 +36,19 @@ const initUpdaterListeners = (win: BrowserWindow) => {
|
|||||||
// 当有新版本可用时
|
// 当有新版本可用时
|
||||||
autoUpdater.on("update-available", (info) => {
|
autoUpdater.on("update-available", (info) => {
|
||||||
win.webContents.send("update-available", info);
|
win.webContents.send("update-available", info);
|
||||||
log.info(`🚀 New version available: ${info.version}`);
|
updateLog.info(`🚀 New version available: ${info.version}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新下载进度
|
// 更新下载进度
|
||||||
autoUpdater.on("download-progress", (progress) => {
|
autoUpdater.on("download-progress", (progress) => {
|
||||||
win.webContents.send("download-progress", progress);
|
win.webContents.send("download-progress", progress);
|
||||||
log.info(`🚀 Downloading: ${progress.percent}%`);
|
updateLog.info(`🚀 Downloading: ${progress.percent}%`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 当下载完成时
|
// 当下载完成时
|
||||||
autoUpdater.on("update-downloaded", (info) => {
|
autoUpdater.on("update-downloaded", (info) => {
|
||||||
win.webContents.send("update-downloaded", info);
|
win.webContents.send("update-downloaded", info);
|
||||||
log.info(`🚀 Update downloaded: ${info.version}`);
|
updateLog.info(`🚀 Update downloaded: ${info.version}`);
|
||||||
// 安装更新
|
// 安装更新
|
||||||
autoUpdater.quitAndInstall();
|
autoUpdater.quitAndInstall();
|
||||||
});
|
});
|
||||||
@@ -48,13 +56,13 @@ const initUpdaterListeners = (win: BrowserWindow) => {
|
|||||||
// 当没有新版本时
|
// 当没有新版本时
|
||||||
autoUpdater.on("update-not-available", (info) => {
|
autoUpdater.on("update-not-available", (info) => {
|
||||||
if (isShowTip) win.webContents.send("update-not-available", info);
|
if (isShowTip) win.webContents.send("update-not-available", info);
|
||||||
log.info(`✅ No new version available: ${info.version}`);
|
updateLog.info(`✅ No new version available: ${info.version}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新错误
|
// 更新错误
|
||||||
autoUpdater.on("error", (err) => {
|
autoUpdater.on("error", (err) => {
|
||||||
win.webContents.send("update-error", err);
|
win.webContents.send("update-error", err);
|
||||||
log.error(`❌ Update error: ${err.message}`);
|
updateLog.error(`❌ Update error: ${err.message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
isInit = true;
|
isInit = true;
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { app } from "electron";
|
|
||||||
import { is } from "@electron-toolkit/utils";
|
|
||||||
import fs from "fs/promises";
|
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
// 系统判断
|
|
||||||
export const isDev = is.dev;
|
|
||||||
export const isWin = process.platform === "win32";
|
|
||||||
export const isMac = process.platform === "darwin";
|
|
||||||
export const isLinux = process.platform === "linux";
|
|
||||||
|
|
||||||
// 程序名称
|
|
||||||
export const appName = app.getName() || "SPlayer";
|
|
||||||
|
|
||||||
// 生成唯一ID
|
|
||||||
export const getFileID = (filePath: string): number => {
|
|
||||||
// SHA-256
|
|
||||||
const hash = crypto.createHash("sha256");
|
|
||||||
hash.update(filePath);
|
|
||||||
const digest = hash.digest("hex");
|
|
||||||
// 将哈希值的前 16 位转换为十进制数字
|
|
||||||
const uniqueId = parseInt(digest.substring(0, 16), 16);
|
|
||||||
return Number(uniqueId.toString().padStart(16, "0"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生成文件 MD5
|
|
||||||
export const getFileMD5 = async (path: string): Promise<string> => {
|
|
||||||
const data = await fs.readFile(path);
|
|
||||||
const hash = crypto.createHash("md5");
|
|
||||||
hash.update(data);
|
|
||||||
return hash.digest("hex");
|
|
||||||
};
|
|
||||||
60
electron/main/utils/config.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { is } from "@electron-toolkit/utils";
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为开发环境
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export const isDev = is.dev;
|
||||||
|
|
||||||
|
/** 是否为 Windows 系统 */
|
||||||
|
export const isWin = process.platform === "win32";
|
||||||
|
/** 是否为 macOS 系统 */
|
||||||
|
export const isMac = process.platform === "darwin";
|
||||||
|
/** 是否为 Linux 系统 */
|
||||||
|
export const isLinux = process.platform === "linux";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软件版本
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
export const appVersion = app.getVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 程序名称
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
export const appName = app.getName() || "SPlayer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器端口
|
||||||
|
* @returns number
|
||||||
|
*/
|
||||||
|
export const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主窗口加载地址
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
export const mainWinUrl =
|
||||||
|
isDev && process.env["ELECTRON_RENDERER_URL"]
|
||||||
|
? process.env["ELECTRON_RENDERER_URL"]
|
||||||
|
: `http://localhost:${port}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌词窗口加载地址
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
export const lyricWinUrl =
|
||||||
|
isDev && process.env["ELECTRON_RENDERER_URL"]
|
||||||
|
? `${process.env["ELECTRON_RENDERER_URL"]}/#/desktop-lyric`
|
||||||
|
: `http://localhost:${port}/#/desktop-lyric`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载窗口地址
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
export const loadWinUrl =
|
||||||
|
isDev && process.env["ELECTRON_RENDERER_URL"]
|
||||||
|
? `${process.env["ELECTRON_RENDERER_URL"]}/web/loading/index.html`
|
||||||
|
: `http://localhost:${port}/web/loading/index.html`;
|
||||||
58
electron/main/utils/helper.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { createHash } from "crypto";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成文件唯一ID
|
||||||
|
* @param filePath 文件路径
|
||||||
|
* @returns 唯一ID
|
||||||
|
*/
|
||||||
|
export const getFileID = (filePath: string): number => {
|
||||||
|
// SHA-256
|
||||||
|
const hash = createHash("sha256");
|
||||||
|
hash.update(filePath);
|
||||||
|
const digest = hash.digest("hex");
|
||||||
|
// 将哈希值的前 16 位转换为十进制数字
|
||||||
|
const uniqueId = parseInt(digest.substring(0, 16), 16);
|
||||||
|
return Number(uniqueId.toString().padStart(16, "0"));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成文件 MD5
|
||||||
|
* @param path 文件路径
|
||||||
|
* @returns MD5值
|
||||||
|
*/
|
||||||
|
export const getFileMD5 = async (path: string): Promise<string> => {
|
||||||
|
const data = await readFile(path);
|
||||||
|
const hash = createHash("md5");
|
||||||
|
hash.update(data);
|
||||||
|
return hash.digest("hex");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 music-metadata 库中的歌词数组转换为LRC格式字符串
|
||||||
|
* @param lyrics 歌词数组,每个元素包含时间戳(毫秒)和歌词文本
|
||||||
|
* @returns LRC格式的字符串
|
||||||
|
*/
|
||||||
|
export const metaDataLyricsArrayToLrc = (
|
||||||
|
lyrics: {
|
||||||
|
text: string;
|
||||||
|
timestamp?: number;
|
||||||
|
}[],
|
||||||
|
): string => {
|
||||||
|
return lyrics
|
||||||
|
.map(({ timestamp, text }) => {
|
||||||
|
if (!timestamp) return "";
|
||||||
|
const totalSeconds = Math.floor(timestamp / 1000);
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
const centiseconds = Math.floor((timestamp % 1000) / 10);
|
||||||
|
|
||||||
|
// 格式化为两位数字
|
||||||
|
const mm = String(minutes).padStart(2, "0");
|
||||||
|
const ss = String(seconds).padStart(2, "0");
|
||||||
|
const cs = String(centiseconds).padStart(2, "0");
|
||||||
|
|
||||||
|
return `[${mm}:${ss}.${cs}]${text}`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
};
|
||||||
25
electron/main/utils/single-lock.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { app } from "electron";
|
||||||
|
import { systemLog } from "../logger";
|
||||||
|
import mainWindow from "../windows/main-window";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化单实例锁
|
||||||
|
* @returns 如果当前实例获得了锁,返回 true;否则返回 false
|
||||||
|
*/
|
||||||
|
export const initSingleLock = (): boolean => {
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
// 如果未获得锁,退出当前实例
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit();
|
||||||
|
systemLog.warn("❌ 已有一个实例正在运行");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 当第二个实例启动时触发
|
||||||
|
else {
|
||||||
|
app.on("second-instance", () => {
|
||||||
|
systemLog.warn("❌ 第二个实例将要启动");
|
||||||
|
mainWindow.getWin()?.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
43
electron/main/windows/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
||||||
|
import { windowsLog } from "../logger";
|
||||||
|
import { appName } from "../utils/config";
|
||||||
|
import { join } from "path";
|
||||||
|
import icon from "../../../public/icons/favicon.png?asset";
|
||||||
|
|
||||||
|
export const createWindow = (
|
||||||
|
options: BrowserWindowConstructorOptions = {},
|
||||||
|
): BrowserWindow | null => {
|
||||||
|
try {
|
||||||
|
const defaultOptions: BrowserWindowConstructorOptions = {
|
||||||
|
title: appName,
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
frame: false, // 是否显示窗口边框
|
||||||
|
center: true, // 窗口居中
|
||||||
|
icon, // 窗口图标
|
||||||
|
autoHideMenuBar: true, // 隐藏菜单栏
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, "../preload/index.mjs"),
|
||||||
|
// 禁用渲染器沙盒
|
||||||
|
sandbox: false,
|
||||||
|
// 禁用同源策略
|
||||||
|
webSecurity: false,
|
||||||
|
// 允许 HTTP
|
||||||
|
allowRunningInsecureContent: true,
|
||||||
|
// 禁用拼写检查
|
||||||
|
spellcheck: false,
|
||||||
|
// 启用 Node.js
|
||||||
|
nodeIntegration: true,
|
||||||
|
nodeIntegrationInWorker: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// 合并参数
|
||||||
|
options = Object.assign(defaultOptions, options);
|
||||||
|
// 创建窗口
|
||||||
|
const win = new BrowserWindow(options);
|
||||||
|
return win;
|
||||||
|
} catch (error) {
|
||||||
|
windowsLog.error(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
60
electron/main/windows/load-window.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
import { createWindow } from "./index";
|
||||||
|
import { loadWinUrl } from "../utils/config";
|
||||||
|
|
||||||
|
class LoadWindow {
|
||||||
|
private win: BrowserWindow | null = null;
|
||||||
|
private winURL: string;
|
||||||
|
constructor() {
|
||||||
|
this.winURL = loadWinUrl;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 主窗口事件
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
private event(): void {
|
||||||
|
if (!this.win) return;
|
||||||
|
// 准备好显示
|
||||||
|
this.win.on("ready-to-show", () => {
|
||||||
|
this.win?.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 创建窗口
|
||||||
|
* @returns BrowserWindow | null
|
||||||
|
*/
|
||||||
|
create(): BrowserWindow | null {
|
||||||
|
this.win = createWindow({
|
||||||
|
width: 800,
|
||||||
|
height: 560,
|
||||||
|
maxWidth: 800,
|
||||||
|
maxHeight: 560,
|
||||||
|
resizable: false,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
// 不在任务栏显示
|
||||||
|
skipTaskbar: true,
|
||||||
|
// 窗口不能最小化
|
||||||
|
minimizable: false,
|
||||||
|
// 窗口不能最大化
|
||||||
|
maximizable: false,
|
||||||
|
// 窗口不能进入全屏状态
|
||||||
|
fullscreenable: false,
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
if (!this.win) return null;
|
||||||
|
// 加载地址
|
||||||
|
this.win.loadURL(this.winURL);
|
||||||
|
// 窗口事件
|
||||||
|
this.event();
|
||||||
|
return this.win;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取窗口
|
||||||
|
* @returns BrowserWindow | null
|
||||||
|
*/
|
||||||
|
getWin(): BrowserWindow | null {
|
||||||
|
return this.win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LoadWindow();
|
||||||
99
electron/main/windows/login-window.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { app, BrowserWindow, session } from "electron";
|
||||||
|
import { createWindow } from "./index";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
class LoginWindow {
|
||||||
|
private win: BrowserWindow | null = null;
|
||||||
|
private loginTimer: NodeJS.Timeout | null = null;
|
||||||
|
private loginSession: Electron.Session | null = null;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
private getLoginSession(): Electron.Session {
|
||||||
|
if (!this.loginSession) {
|
||||||
|
this.loginSession = session.fromPartition("persist:login");
|
||||||
|
}
|
||||||
|
return this.loginSession;
|
||||||
|
}
|
||||||
|
// 事件绑定
|
||||||
|
private event(mainWin: BrowserWindow): void {
|
||||||
|
if (!this.win) return;
|
||||||
|
// 阻止新窗口创建
|
||||||
|
this.win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
||||||
|
// 加载完成后显示并开始轮询登录状态
|
||||||
|
this.win.webContents.once("did-finish-load", () => {
|
||||||
|
this.win?.show();
|
||||||
|
this.loginTimer = setInterval(() => this.checkLogin(mainWin), 1000);
|
||||||
|
this.win?.on("closed", () => {
|
||||||
|
if (this.loginTimer) clearInterval(this.loginTimer);
|
||||||
|
this.loginTimer = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已登录
|
||||||
|
private async checkLogin(mainWin: BrowserWindow) {
|
||||||
|
if (!this.win) return;
|
||||||
|
try {
|
||||||
|
this.win.webContents.executeJavaScript(
|
||||||
|
"document.title = '登录网易云音乐( 若遇到无响应请关闭后重试 )'",
|
||||||
|
);
|
||||||
|
// 判断 MUSIC_U
|
||||||
|
const MUSIC_U = await this.getLoginSession().cookies.get({ name: "MUSIC_U" });
|
||||||
|
if (MUSIC_U && MUSIC_U.length > 0) {
|
||||||
|
if (this.loginTimer) clearInterval(this.loginTimer);
|
||||||
|
this.loginTimer = null;
|
||||||
|
const value = `MUSIC_U=${MUSIC_U[0].value};`;
|
||||||
|
// 发送回主进程
|
||||||
|
mainWin?.webContents.send("send-cookies", value);
|
||||||
|
this.win.destroy();
|
||||||
|
this.win = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建登录窗口
|
||||||
|
async create(mainWin: BrowserWindow): Promise<BrowserWindow | null> {
|
||||||
|
await app.whenReady();
|
||||||
|
const loginSession = this.getLoginSession();
|
||||||
|
// 清理登录会话存储
|
||||||
|
await loginSession.clearStorageData({
|
||||||
|
storages: ["cookies", "localstorage"],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.win = createWindow({
|
||||||
|
parent: mainWin,
|
||||||
|
title: "登录网易云音乐( 若遇到无响应请关闭后重试 )",
|
||||||
|
width: 1280,
|
||||||
|
height: 800,
|
||||||
|
center: true,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, "../preload/index.mjs"),
|
||||||
|
sandbox: false,
|
||||||
|
webSecurity: false,
|
||||||
|
allowRunningInsecureContent: true,
|
||||||
|
spellcheck: false,
|
||||||
|
nodeIntegration: true,
|
||||||
|
nodeIntegrationInWorker: true,
|
||||||
|
session: loginSession,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.win) return null;
|
||||||
|
// 加载登录地址
|
||||||
|
this.win.loadURL("https://music.163.com/#/login/");
|
||||||
|
// 绑定事件
|
||||||
|
this.event(mainWin);
|
||||||
|
return this.win;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取窗口
|
||||||
|
getWin(): BrowserWindow | null {
|
||||||
|
return this.win;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LoginWindow();
|
||||||
89
electron/main/windows/lyric-window.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
import { createWindow } from "./index";
|
||||||
|
import { useStore } from "../store";
|
||||||
|
import { lyricWinUrl } from "../utils/config";
|
||||||
|
import mainWindow from "./main-window";
|
||||||
|
|
||||||
|
class LyricWindow {
|
||||||
|
private win: BrowserWindow | null = null;
|
||||||
|
constructor() {}
|
||||||
|
/**
|
||||||
|
* 主窗口事件
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
private event(): void {
|
||||||
|
if (!this.win) return;
|
||||||
|
// 准备好显示
|
||||||
|
this.win.on("ready-to-show", () => {
|
||||||
|
this.win?.show();
|
||||||
|
});
|
||||||
|
// 歌词窗口缩放
|
||||||
|
this.win?.on("resized", () => {
|
||||||
|
const store = useStore();
|
||||||
|
const bounds = this.win?.getBounds();
|
||||||
|
if (bounds) {
|
||||||
|
const { width, height } = bounds;
|
||||||
|
store.set("lyric", { ...store.get("lyric"), width, height });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 歌词窗口关闭
|
||||||
|
this.win?.on("close", () => {
|
||||||
|
this.win = null;
|
||||||
|
const mainWin = mainWindow?.getWin();
|
||||||
|
if (mainWin) {
|
||||||
|
mainWin?.webContents.send("closeDesktopLyric");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 创建主窗口
|
||||||
|
* @returns BrowserWindow | null
|
||||||
|
*/
|
||||||
|
create(): BrowserWindow | null {
|
||||||
|
const store = useStore();
|
||||||
|
const { width, height, x, y } = store.get("lyric");
|
||||||
|
this.win = createWindow({
|
||||||
|
width: width || 800,
|
||||||
|
height: height || 180,
|
||||||
|
minWidth: 640,
|
||||||
|
minHeight: 140,
|
||||||
|
maxWidth: 1400,
|
||||||
|
maxHeight: 360,
|
||||||
|
// 没有指定位置时居中显示
|
||||||
|
center: !(x && y),
|
||||||
|
// 窗口位置
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
transparent: true,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||||
|
alwaysOnTop: true,
|
||||||
|
resizable: true,
|
||||||
|
movable: true,
|
||||||
|
show: false,
|
||||||
|
// 不在任务栏显示
|
||||||
|
skipTaskbar: true,
|
||||||
|
// 窗口不能最小化
|
||||||
|
minimizable: false,
|
||||||
|
// 窗口不能最大化
|
||||||
|
maximizable: false,
|
||||||
|
// 窗口不能进入全屏状态
|
||||||
|
fullscreenable: false,
|
||||||
|
});
|
||||||
|
if (!this.win) return null;
|
||||||
|
// 加载地址
|
||||||
|
this.win.loadURL(lyricWinUrl);
|
||||||
|
// 窗口事件
|
||||||
|
this.event();
|
||||||
|
return this.win;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取窗口
|
||||||
|
* @returns BrowserWindow | null
|
||||||
|
*/
|
||||||
|
getWin(): BrowserWindow | null {
|
||||||
|
if (this.win && !this.win?.isDestroyed()) return this.win;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LyricWindow();
|
||||||
136
electron/main/windows/main-window.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { BrowserWindow, shell } from "electron";
|
||||||
|
import { createWindow } from "./index";
|
||||||
|
import { mainWinUrl } from "../utils/config";
|
||||||
|
import { useStore } from "../store";
|
||||||
|
import { isLinux } from "../utils/config";
|
||||||
|
|
||||||
|
class MainWindow {
|
||||||
|
private win: BrowserWindow | null = null;
|
||||||
|
private winURL: string;
|
||||||
|
constructor() {
|
||||||
|
this.winURL = mainWinUrl;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 保存窗口大小和状态
|
||||||
|
*/
|
||||||
|
private saveBounds() {
|
||||||
|
if (this.win?.isFullScreen()) return;
|
||||||
|
const store = useStore();
|
||||||
|
const bounds = this.win?.getBounds();
|
||||||
|
if (bounds) {
|
||||||
|
const maximized = this.win?.isMaximized();
|
||||||
|
store.set("window", { ...bounds, maximized });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 主窗口事件
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
private event(): void {
|
||||||
|
if (!this.win) return;
|
||||||
|
const store = useStore();
|
||||||
|
// 配置网络代理
|
||||||
|
if (store.get("proxy")) {
|
||||||
|
this.win.webContents.session.setProxy({ proxyRules: store.get("proxy") });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口打开处理程序
|
||||||
|
this.win.webContents.setWindowOpenHandler((details) => {
|
||||||
|
const { url } = details;
|
||||||
|
if (url.startsWith("https://") || url.startsWith("http://")) {
|
||||||
|
shell.openExternal(url);
|
||||||
|
}
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
|
// 窗口显示时
|
||||||
|
this.win?.on("show", () => {
|
||||||
|
// this.mainWindow?.webContents.send("lyricsScroll");
|
||||||
|
});
|
||||||
|
// 窗口获得焦点时
|
||||||
|
this.win?.on("focus", () => {
|
||||||
|
this.saveBounds();
|
||||||
|
});
|
||||||
|
// 窗口大小改变时
|
||||||
|
this.win?.on("resized", () => {
|
||||||
|
// 若处于全屏则不保存
|
||||||
|
if (this.win?.isFullScreen()) return;
|
||||||
|
this.saveBounds();
|
||||||
|
});
|
||||||
|
// 窗口位置改变时
|
||||||
|
this.win?.on("moved", () => {
|
||||||
|
this.saveBounds();
|
||||||
|
});
|
||||||
|
// 窗口最大化时
|
||||||
|
this.win?.on("maximize", () => {
|
||||||
|
this.saveBounds();
|
||||||
|
this.win?.webContents.send("win-state-change", true);
|
||||||
|
});
|
||||||
|
// 窗口取消最大化时
|
||||||
|
this.win?.on("unmaximize", () => {
|
||||||
|
this.saveBounds();
|
||||||
|
this.win?.webContents.send("win-state-change", false);
|
||||||
|
});
|
||||||
|
// Linux 无法使用 resized 和 moved
|
||||||
|
if (isLinux) {
|
||||||
|
this.win?.on("resize", () => {
|
||||||
|
// 若处于全屏则不保存
|
||||||
|
if (this.win?.isFullScreen()) return;
|
||||||
|
this.saveBounds();
|
||||||
|
});
|
||||||
|
this.win?.on("move", () => {
|
||||||
|
this.saveBounds();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 窗口关闭
|
||||||
|
this.win?.on("close", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.win?.show();
|
||||||
|
this.win?.focus();
|
||||||
|
this.win?.webContents.send("win-will-close");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 创建窗口
|
||||||
|
* @returns BrowserWindow | null
|
||||||
|
*/
|
||||||
|
create(): BrowserWindow | null {
|
||||||
|
const store = useStore();
|
||||||
|
const { width, height } = store.get("window");
|
||||||
|
this.win = createWindow({
|
||||||
|
// 菜单栏
|
||||||
|
titleBarStyle: "customButtonsOnHover",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
minHeight: 600,
|
||||||
|
minWidth: 800,
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
if (!this.win) return null;
|
||||||
|
// 加载地址
|
||||||
|
this.win.loadURL(this.winURL);
|
||||||
|
// 窗口事件
|
||||||
|
this.event();
|
||||||
|
return this.win;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取窗口
|
||||||
|
* @returns BrowserWindow | null
|
||||||
|
*/
|
||||||
|
getWin(): BrowserWindow | null {
|
||||||
|
if (this.win && !this.win.isDestroyed()) {
|
||||||
|
return this.win;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 显示主窗口
|
||||||
|
*/
|
||||||
|
showWindow() {
|
||||||
|
if (this.win) {
|
||||||
|
this.win.show();
|
||||||
|
if (this.win.isMinimized()) this.win.restore();
|
||||||
|
this.win.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default new MainWindow();
|
||||||
11
electron/preload/index.d.ts
vendored
@@ -1,8 +1,17 @@
|
|||||||
import { ElectronAPI } from "@electron-toolkit/preload";
|
import { ElectronAPI } from "@electron-toolkit/preload";
|
||||||
|
import type { StoreType } from "../main/store";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electron: ElectronAPI;
|
electron: ElectronAPI;
|
||||||
api: unknown;
|
api: {
|
||||||
|
store: {
|
||||||
|
get<K extends keyof StoreType>(key: K): Promise<StoreType[K]>;
|
||||||
|
set<K extends keyof StoreType>(key: K, value: StoreType[K]): Promise<boolean>;
|
||||||
|
has(key: keyof StoreType): Promise<boolean>;
|
||||||
|
delete(key: keyof StoreType): Promise<boolean>;
|
||||||
|
reset(keys?: (keyof StoreType)[]): Promise<boolean>;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { contextBridge } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
import { electronAPI } from "@electron-toolkit/preload";
|
import { electronAPI } from "@electron-toolkit/preload";
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
@@ -7,10 +7,17 @@ import { electronAPI } from "@electron-toolkit/preload";
|
|||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld("electron", electronAPI);
|
contextBridge.exposeInMainWorld("electron", electronAPI);
|
||||||
|
// Expose store API via preload
|
||||||
|
contextBridge.exposeInMainWorld("api", {
|
||||||
|
store: {
|
||||||
|
get: (key: string) => ipcRenderer.invoke("store-get", key),
|
||||||
|
set: (key: string, value: unknown) => ipcRenderer.invoke("store-set", key, value),
|
||||||
|
has: (key: string) => ipcRenderer.invoke("store-has", key),
|
||||||
|
delete: (key: string) => ipcRenderer.invoke("store-delete", key),
|
||||||
|
reset: (keys?: string[]) => ipcRenderer.invoke("store-reset", keys),
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// @ts-expect-error (define in dts)
|
|
||||||
window.electron = electronAPI;
|
|
||||||
}
|
}
|
||||||
|
|||||||
187
electron/server/control/index.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { FastifyInstance } from "fastify";
|
||||||
|
import mainWindow from "../../main/windows/main-window";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放控制接口
|
||||||
|
* @param fastify Fastify 实例
|
||||||
|
*/
|
||||||
|
export const initControlAPI = async (fastify: FastifyInstance) => {
|
||||||
|
// 播放控制路由前缀
|
||||||
|
await fastify.register(
|
||||||
|
async (fastify) => {
|
||||||
|
// 播放
|
||||||
|
fastify.get("/play", async (_request, reply) => {
|
||||||
|
try {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "主窗口未找到",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWin.webContents.send("play");
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
code: 200,
|
||||||
|
message: "播放命令已发送",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "播放失败",
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 暂停
|
||||||
|
fastify.get("/pause", async (_request, reply) => {
|
||||||
|
try {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "主窗口未找到",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWin.webContents.send("pause");
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
code: 200,
|
||||||
|
message: "暂停命令已发送",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "暂停失败",
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 播放/暂停切换
|
||||||
|
fastify.get("/toggle", async (_request, reply) => {
|
||||||
|
try {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "主窗口未找到",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里可以根据当前播放状态来决定发送 play 还是 pause
|
||||||
|
// 暂时先发送 toggle 事件,如果渲染进程支持的话
|
||||||
|
mainWin.webContents.send("toggle");
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
code: 200,
|
||||||
|
message: "播放/暂停切换命令已发送",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "播放/暂停切换失败",
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下一曲
|
||||||
|
fastify.get("/next", async (_request, reply) => {
|
||||||
|
try {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "主窗口未找到",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWin.webContents.send("playNext");
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
code: 200,
|
||||||
|
message: "下一曲命令已发送",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "下一曲失败",
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 上一曲
|
||||||
|
fastify.get("/prev", async (_request, reply) => {
|
||||||
|
try {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "主窗口未找到",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWin.webContents.send("playPrev");
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
code: 200,
|
||||||
|
message: "上一曲命令已发送",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "上一曲失败",
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取播放状态(可选功能)
|
||||||
|
fastify.get("/status", async (_request, reply) => {
|
||||||
|
try {
|
||||||
|
const mainWin = mainWindow.getWin();
|
||||||
|
if (!mainWin) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "主窗口未找到",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里可以通过 IPC 获取当前播放状态
|
||||||
|
// 暂时返回基本信息
|
||||||
|
return reply.send({
|
||||||
|
code: 200,
|
||||||
|
message: "获取状态成功",
|
||||||
|
data: {
|
||||||
|
connected: true,
|
||||||
|
window: "available",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: "获取状态失败",
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ prefix: "/control" },
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,25 +1,28 @@
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { isDev } from "../main/utils";
|
import { isDev } from "../main/utils/config";
|
||||||
import initNcmAPI from "./netease";
|
import { serverLog } from "../main/logger";
|
||||||
import initUnblockAPI from "./unblock";
|
import { initNcmAPI } from "./netease";
|
||||||
|
import { initUnblockAPI } from "./unblock";
|
||||||
|
import { initControlAPI } from "./control";
|
||||||
import fastifyCookie from "@fastify/cookie";
|
import fastifyCookie from "@fastify/cookie";
|
||||||
import fastifyMultipart from "@fastify/multipart";
|
import fastifyMultipart from "@fastify/multipart";
|
||||||
import fastifyStatic from "@fastify/static";
|
import fastifyStatic from "@fastify/static";
|
||||||
import fastify from "fastify";
|
import fastify from "fastify";
|
||||||
import log from "../main/logger";
|
|
||||||
|
|
||||||
const initAppServer = async () => {
|
const initAppServer = async () => {
|
||||||
try {
|
try {
|
||||||
const server = fastify({
|
const server = fastify({
|
||||||
// 忽略尾随斜杠
|
routerOptions: {
|
||||||
ignoreTrailingSlash: true,
|
// 忽略尾随斜杠
|
||||||
|
ignoreTrailingSlash: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
// 注册插件
|
// 注册插件
|
||||||
server.register(fastifyCookie);
|
server.register(fastifyCookie);
|
||||||
server.register(fastifyMultipart);
|
server.register(fastifyMultipart);
|
||||||
// 生产环境启用静态文件
|
// 生产环境启用静态文件
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
log.info("📂 Serving static files from /renderer");
|
serverLog.info("📂 Serving static files from /renderer");
|
||||||
server.register(fastifyStatic, {
|
server.register(fastifyStatic, {
|
||||||
root: join(__dirname, "../renderer"),
|
root: join(__dirname, "../renderer"),
|
||||||
});
|
});
|
||||||
@@ -45,13 +48,14 @@ const initAppServer = async () => {
|
|||||||
// 注册接口
|
// 注册接口
|
||||||
server.register(initNcmAPI, { prefix: "/api" });
|
server.register(initNcmAPI, { prefix: "/api" });
|
||||||
server.register(initUnblockAPI, { prefix: "/api" });
|
server.register(initUnblockAPI, { prefix: "/api" });
|
||||||
|
server.register(initControlAPI, { 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 });
|
await server.listen({ port });
|
||||||
log.info(`🌐 Starting AppServer on port ${port}`);
|
serverLog.info(`🌐 Starting AppServer on port ${port}`);
|
||||||
return server;
|
return server;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("🚫 AppServer failed to start");
|
serverLog.error("🚫 AppServer failed to start");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||||
import { pathCase } from "change-case";
|
import { pathCase } from "change-case";
|
||||||
import NeteaseCloudMusicApi from "NeteaseCloudMusicApi";
|
import { serverLog } from "../../main/logger";
|
||||||
import log from "../../main/logger";
|
import NeteaseCloudMusicApi from "@neteasecloudmusicapienhanced/api";
|
||||||
|
|
||||||
// 获取数据
|
// 获取数据
|
||||||
const getHandler = (name: string, neteaseApi: (params: any) => any) => {
|
const getHandler = (name: string, neteaseApi: (params: any) => any) => {
|
||||||
@@ -9,7 +9,7 @@ const getHandler = (name: string, neteaseApi: (params: any) => any) => {
|
|||||||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) => {
|
) => {
|
||||||
log.info("🌐 Request NcmAPI:", name);
|
serverLog.log("🌐 Request NcmAPI:", name);
|
||||||
// 获取 NcmAPI 数据
|
// 获取 NcmAPI 数据
|
||||||
try {
|
try {
|
||||||
const result = await neteaseApi({
|
const result = await neteaseApi({
|
||||||
@@ -19,7 +19,7 @@ const getHandler = (name: string, neteaseApi: (params: any) => any) => {
|
|||||||
});
|
});
|
||||||
return reply.send(result.body);
|
return reply.send(result.body);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log.error("❌ NcmAPI Error:", error);
|
serverLog.error("❌ NcmAPI Error:", error);
|
||||||
if ([400, 301].includes(error.status)) {
|
if ([400, 301].includes(error.status)) {
|
||||||
return reply.status(error.status).send(error.body);
|
return reply.status(error.status).send(error.body);
|
||||||
}
|
}
|
||||||
@@ -29,16 +29,16 @@ const getHandler = (name: string, neteaseApi: (params: any) => any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 初始化 NcmAPI
|
// 初始化 NcmAPI
|
||||||
const initNcmAPI = async (fastify: FastifyInstance) => {
|
export const initNcmAPI = async (fastify: FastifyInstance) => {
|
||||||
// 主信息
|
// 主信息
|
||||||
fastify.get("/netease", (_, reply) => {
|
fastify.get("/netease", (_, reply) => {
|
||||||
reply.send({
|
reply.send({
|
||||||
name: "NeteaseCloudMusicApi",
|
name: "@neteaseapireborn/api",
|
||||||
version: "4.20.0",
|
version: "4.29.2",
|
||||||
description: "网易云音乐 Node.js API service",
|
description: "网易云音乐 API Enhanced",
|
||||||
author: "@binaryify",
|
author: "@MoeFurina",
|
||||||
license: "MIT",
|
license: "MIT",
|
||||||
url: "https://gitlab.com/Binaryify/neteasecloudmusicapi",
|
url: "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +60,28 @@ const initNcmAPI = async (fastify: FastifyInstance) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("🌐 Register NcmAPI successfully");
|
// 获取 TTML 歌词
|
||||||
};
|
fastify.get(
|
||||||
|
"/netease/lyric/ttml",
|
||||||
|
async (req: FastifyRequest<{ Querystring: { id: string } }>, reply: FastifyReply) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
if (!id) {
|
||||||
|
return reply.status(400).send({ error: "id is required" });
|
||||||
|
}
|
||||||
|
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return reply.send(null);
|
||||||
|
}
|
||||||
|
const data = await response.text();
|
||||||
|
return reply.send(data);
|
||||||
|
} catch (error) {
|
||||||
|
serverLog.error("❌ TTML Lyric Fetch Error:", error);
|
||||||
|
return reply.send(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default initNcmAPI;
|
serverLog.info("🌐 Register NcmAPI successfully");
|
||||||
|
};
|
||||||
|
|||||||
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;
|
||||||
161
electron/server/unblock/bodian.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { SongUrlResult } from "./unblock";
|
||||||
|
import { serverLog } from "../../main/logger";
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机设备 ID
|
||||||
|
* @returns 随机设备 ID
|
||||||
|
*/
|
||||||
|
const getRandomDeviceId = () => {
|
||||||
|
const min = 0;
|
||||||
|
const max = 100000000000;
|
||||||
|
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
return randomNum.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 随机设备 ID */
|
||||||
|
const deviceId = getRandomDeviceId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化歌曲信息
|
||||||
|
* @param song 歌曲信息
|
||||||
|
* @returns 格式化后的歌曲信息
|
||||||
|
*/
|
||||||
|
const format = (song: any) => ({
|
||||||
|
id: song.MUSICRID.split("_").pop(),
|
||||||
|
name: song.SONGNAME,
|
||||||
|
duration: song.DURATION * 1000,
|
||||||
|
album: { id: song.ALBUMID, name: song.ALBUM },
|
||||||
|
artists: song.ARTIST.split("&").map((name: any, index: any) => ({
|
||||||
|
id: index ? null : song.ARTISTID,
|
||||||
|
name,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成签名
|
||||||
|
* @param str 请求字符串
|
||||||
|
* @returns 包含签名的请求字符串
|
||||||
|
*/
|
||||||
|
const generateSign = (str: string) => {
|
||||||
|
const url = new URL(str);
|
||||||
|
|
||||||
|
const currentTime = Date.now();
|
||||||
|
str += `×tamp=${currentTime}`;
|
||||||
|
|
||||||
|
const filteredChars = str
|
||||||
|
.substring(str.indexOf("?") + 1)
|
||||||
|
.replace(/[^a-zA-Z0-9]/g, "")
|
||||||
|
.split("")
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
const dataToEncrypt = `kuwotest${filteredChars.join("")}${url.pathname}`;
|
||||||
|
const md5 = createHash("md5").update(dataToEncrypt).digest("hex");
|
||||||
|
return `${str}&sign=${md5}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索歌曲
|
||||||
|
* @param keyword 搜索关键词
|
||||||
|
* @returns 歌曲 ID 或 null
|
||||||
|
*/
|
||||||
|
const search = async (info: string): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const keyword = encodeURIComponent(info.replace(" - ", " "));
|
||||||
|
const url =
|
||||||
|
"http://search.kuwo.cn/r.s?&correct=1&vipver=1&stype=comprehensive&encoding=utf8" +
|
||||||
|
"&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
|
||||||
|
keyword;
|
||||||
|
const result = await axios.get(url);
|
||||||
|
if (
|
||||||
|
!result.data ||
|
||||||
|
result.data.content.length < 2 ||
|
||||||
|
!result.data.content[1].musicpage ||
|
||||||
|
result.data.content[1].musicpage.abslist.length < 1
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 获取歌曲信息
|
||||||
|
const list = result.data.content[1].musicpage.abslist.map(format);
|
||||||
|
if (list[0] && !list[0]?.id) return null;
|
||||||
|
return list[0].id;
|
||||||
|
} catch (error) {
|
||||||
|
serverLog.error("❌ Get BodianSongId Error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送广告免费请求
|
||||||
|
* @returns 包含广告免费响应的 Promise
|
||||||
|
*/
|
||||||
|
const sendAdFreeRequest = () => {
|
||||||
|
try {
|
||||||
|
const adurl =
|
||||||
|
"http://bd-api.kuwo.cn/api/service/advert/watch?uid=-1&token=×tamp=1724306124436&sign=15a676d66285117ad714e8c8371691da";
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"user-agent": "Dart/2.19 (dart:io)",
|
||||||
|
plat: "ar",
|
||||||
|
channel: "aliopen",
|
||||||
|
devid: deviceId,
|
||||||
|
ver: "3.9.0",
|
||||||
|
host: "bd-api.kuwo.cn",
|
||||||
|
qimei36: "1e9970cbcdc20a031dee9f37100017e1840e",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = JSON.stringify({
|
||||||
|
type: 5,
|
||||||
|
subType: 5,
|
||||||
|
musicId: 0,
|
||||||
|
adToken: "",
|
||||||
|
});
|
||||||
|
return axios.post(adurl, data, { headers });
|
||||||
|
} catch (error) {
|
||||||
|
serverLog.error("❌ Get Bodian Ad Free Error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取波点音乐歌曲 URL
|
||||||
|
* @param keyword 搜索关键词
|
||||||
|
* @returns 包含歌曲 URL 的结果对象
|
||||||
|
*/
|
||||||
|
const getBodianSongUrl = async (keyword: string): Promise<SongUrlResult> => {
|
||||||
|
try {
|
||||||
|
if (!keyword) return { code: 404, url: null };
|
||||||
|
const songId = await search(keyword);
|
||||||
|
if (!songId) return { code: 404, url: null };
|
||||||
|
// 请求地址
|
||||||
|
const headers = {
|
||||||
|
"user-agent": "Dart/2.19 (dart:io)",
|
||||||
|
plat: "ar",
|
||||||
|
channel: "aliopen",
|
||||||
|
devid: deviceId,
|
||||||
|
ver: "3.9.0",
|
||||||
|
host: "bd-api.kuwo.cn",
|
||||||
|
"X-Forwarded-For": "1.0.1.114",
|
||||||
|
};
|
||||||
|
let audioUrl = `http://bd-api.kuwo.cn/api/play/music/v2/audioUrl?&br=${"320kmp3"}&musicId=${songId}`;
|
||||||
|
// 生成签名
|
||||||
|
audioUrl = generateSign(audioUrl);
|
||||||
|
// 获取广告
|
||||||
|
await sendAdFreeRequest();
|
||||||
|
// 获取歌曲地址
|
||||||
|
const result = await axios.get(audioUrl, { headers });
|
||||||
|
if (typeof result.data === "object") {
|
||||||
|
const urlMatch = result.data.data.audioUrl;
|
||||||
|
serverLog.log("🔗 BodianSong URL:", urlMatch);
|
||||||
|
return { code: 200, url: urlMatch };
|
||||||
|
}
|
||||||
|
return { code: 404, url: null };
|
||||||
|
} catch (error) {
|
||||||
|
serverLog.error("❌ Get BodianSong URL Error:", error);
|
||||||
|
return { code: 404, url: null };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBodianSongUrl;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||||
import { SongUrlResult } from "./unblock";
|
import { SongUrlResult } from "./unblock";
|
||||||
|
import { serverLog } from "../../main/logger";
|
||||||
import getKuwoSongUrl from "./kuwo";
|
import getKuwoSongUrl from "./kuwo";
|
||||||
import log from "../../main/logger";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import getBodianSongUrl from "./bodian";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 直接获取 网易云云盘 链接
|
* 直接获取 网易云云盘 链接
|
||||||
@@ -17,16 +18,16 @@ const getNeteaseSongUrl = async (id: number | string): Promise<SongUrlResult> =>
|
|||||||
params: { types: "url", id },
|
params: { types: "url", id },
|
||||||
});
|
});
|
||||||
const songUrl = result.data.url;
|
const songUrl = result.data.url;
|
||||||
log.info("🔗 NeteaseSongUrl URL:", songUrl);
|
serverLog.log("🔗 NeteaseSongUrl URL:", songUrl);
|
||||||
return { code: 200, url: songUrl };
|
return { code: 200, url: songUrl };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("❌ Get NeteaseSongUrl Error:", error);
|
serverLog.error("❌ Get NeteaseSongUrl Error:", error);
|
||||||
return { code: 404, url: null };
|
return { code: 404, url: null };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化 UnblockAPI
|
// 初始化 UnblockAPI
|
||||||
const UnblockAPI = async (fastify: FastifyInstance) => {
|
export const initUnblockAPI = async (fastify: FastifyInstance) => {
|
||||||
// 主信息
|
// 主信息
|
||||||
fastify.get("/unblock", (_, reply) => {
|
fastify.get("/unblock", (_, reply) => {
|
||||||
reply.send({
|
reply.send({
|
||||||
@@ -61,8 +62,18 @@ const UnblockAPI = async (fastify: FastifyInstance) => {
|
|||||||
return reply.send(result);
|
return reply.send(result);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// bodian
|
||||||
|
fastify.get(
|
||||||
|
"/unblock/bodian",
|
||||||
|
async (
|
||||||
|
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => {
|
||||||
|
const { keyword } = req.query;
|
||||||
|
const result = await getBodianSongUrl(keyword);
|
||||||
|
return reply.send(result);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
log.info("🌐 Register UnblockAPI successfully");
|
serverLog.info("🌐 Register UnblockAPI successfully");
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UnblockAPI;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { encryptQuery } from "./kwDES";
|
import { encryptQuery } from "./kwDES";
|
||||||
import { SongUrlResult } from "./unblock";
|
import { SongUrlResult } from "./unblock";
|
||||||
import log from "../../main/logger";
|
import { serverLog } from "../../main/logger";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
// 获取酷我音乐歌曲 ID
|
// 获取酷我音乐歌曲 ID
|
||||||
@@ -26,7 +26,7 @@ const getKuwoSongId = async (keyword: string): Promise<string | null> => {
|
|||||||
if (songName && !songName?.includes(originalName[0])) return null;
|
if (songName && !songName?.includes(originalName[0])) return null;
|
||||||
return songId.slice("MUSIC_".length);
|
return songId.slice("MUSIC_".length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("❌ Get KuwoSongId Error:", error);
|
serverLog.error("❌ Get KuwoSongId Error:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -53,12 +53,12 @@ const getKuwoSongUrl = async (keyword: string): Promise<SongUrlResult> => {
|
|||||||
});
|
});
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
|
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
|
||||||
log.info("🔗 KuwoSong URL:", urlMatch);
|
serverLog.log("🔗 KuwoSong URL:", urlMatch);
|
||||||
return { code: 200, url: urlMatch };
|
return { code: 200, url: urlMatch };
|
||||||
}
|
}
|
||||||
return { code: 404, url: null };
|
return { code: 404, url: null };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("❌ Get KuwoSong URL Error:", error);
|
serverLog.error("❌ Get KuwoSong URL Error:", error);
|
||||||
return { code: 404, url: null };
|
return { code: 404, url: null };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
65
eslint.config.mjs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
import vue from "eslint-plugin-vue";
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import path from "node:path";
|
||||||
|
import autoEslint from "./auto-eslint.mjs";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"**/node_modules",
|
||||||
|
"**/dist",
|
||||||
|
"**/out",
|
||||||
|
"**/.gitignore",
|
||||||
|
"**/auto-imports.d.ts",
|
||||||
|
"**/components.d.ts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
"@typescript-eslint": typescriptEslint,
|
||||||
|
vue,
|
||||||
|
},
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
...autoEslint.globals,
|
||||||
|
},
|
||||||
|
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/.eslintrc.{js,cjs}"],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.node },
|
||||||
|
ecmaVersion: 5,
|
||||||
|
sourceType: "commonjs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
23
nginx.conf
@@ -15,6 +15,23 @@ server {
|
|||||||
rewrite ^(.*)$ /index.html last;
|
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":"http://music.163.com' '"url":"/music/unblock';
|
||||||
|
sub_filter '"url":"https://music.163.com' '"url":"/music/unblock';
|
||||||
|
sub_filter_types application/json;
|
||||||
|
sub_filter_once off;
|
||||||
|
}
|
||||||
|
|
||||||
location /api/netease/ {
|
location /api/netease/ {
|
||||||
proxy_buffers 16 64k;
|
proxy_buffers 16 64k;
|
||||||
proxy_buffer_size 128k;
|
proxy_buffer_size 128k;
|
||||||
@@ -26,4 +43,10 @@ server {
|
|||||||
proxy_set_header X-NginX-Proxy true;
|
proxy_set_header X-NginX-Proxy true;
|
||||||
proxy_pass http://localhost:3000/;
|
proxy_pass http://localhost:3000/;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /music/unblock/ {
|
||||||
|
proxy_pass https://music.163.com/;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
144
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "splayer",
|
"name": "splayer",
|
||||||
"productName": "SPlayer",
|
"productName": "SPlayer",
|
||||||
"version": "3.0.0-alpha.1",
|
"version": "3.0.0-beta.4",
|
||||||
"description": "A minimalist music player",
|
"description": "A minimalist music player",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "imsyy",
|
"author": "imsyy",
|
||||||
@@ -23,91 +23,113 @@
|
|||||||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"dev": "node scripts/dev.mjs",
|
||||||
"build": "npm run typecheck && electron-vite build",
|
"build": "npx rimraf dist && npm run typecheck && electron-vite build",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"build:web": "npm run build",
|
"build:web": "npm run build",
|
||||||
"build:unpack": "npm run build && electron-builder --dir",
|
"build:unpack": "npm run build && electron-builder --dir",
|
||||||
"build:win": "npm run build && electron-builder --win",
|
"build:win": "npm run build && electron-builder --win --config electron-builder.config.ts",
|
||||||
"build:mac": "npm run build && electron-builder --mac",
|
"build:mac": "npm run build && electron-builder --mac --config electron-builder.config.ts",
|
||||||
"build:linux": "npm run build && electron-builder --linux"
|
"build:linux": "npm run build && electron-builder --linux --config electron-builder.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@applemusic-like-lyrics/core": "^0.1.3",
|
"@applemusic-like-lyrics/core": "^0.1.3",
|
||||||
"@applemusic-like-lyrics/lyric": "^0.2.2",
|
"@applemusic-like-lyrics/lyric": "^0.3.0",
|
||||||
"@applemusic-like-lyrics/vue": "^0.1.5",
|
"@applemusic-like-lyrics/vue": "^0.1.5",
|
||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@imsyy/color-utils": "^1.0.2",
|
"@imsyy/color-utils": "^1.0.2",
|
||||||
"@material/material-color-utilities": "^0.3.0",
|
"@material/material-color-utilities": "^0.3.0",
|
||||||
"@pixi/app": "^7.4.2",
|
"@neteasecloudmusicapienhanced/api": "^4.29.16",
|
||||||
"@pixi/core": "^7.4.2",
|
"@pixi/app": "^7.4.3",
|
||||||
"@pixi/display": "^7.4.2",
|
"@pixi/core": "^7.4.3",
|
||||||
"@pixi/filter-blur": "^7.4.2",
|
"@pixi/display": "^7.4.3",
|
||||||
|
"@pixi/filter-blur": "^7.4.3",
|
||||||
"@pixi/filter-bulge-pinch": "^5.1.1",
|
"@pixi/filter-bulge-pinch": "^5.1.1",
|
||||||
"@pixi/filter-color-matrix": "^7.4.2",
|
"@pixi/filter-color-matrix": "^7.4.3",
|
||||||
"@pixi/sprite": "^7.4.2",
|
"@pixi/sprite": "^7.4.3",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^13.9.0",
|
||||||
"NeteaseCloudMusicApi": "^4.22.0",
|
"axios": "^1.13.2",
|
||||||
"axios": "^1.7.7",
|
"axios-retry": "^4.5.0",
|
||||||
"change-case": "^5.4.4",
|
"change-case": "^5.4.4",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.18",
|
||||||
"electron-dl": "^3.5.2",
|
"electron-dl": "^4.0.0",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^11.0.2",
|
||||||
"electron-updater": "^6.3.4",
|
"electron-updater": "^6.6.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"font-list": "^1.5.1",
|
"font-list": "^2.0.1",
|
||||||
|
"get-port": "^7.1.0",
|
||||||
|
"github-markdown-css": "^5.8.1",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jss": "^10.10.0",
|
"jss": "^10.10.0",
|
||||||
"jss-preset-default": "^10.10.0",
|
"jss-preset-default": "^10.10.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "^14.1.2",
|
"marked": "^16.4.0",
|
||||||
"music-metadata": "7.14.0",
|
"md5": "^2.3.0",
|
||||||
"pinia": "^2.2.2",
|
"music-metadata": "^11.9.0",
|
||||||
"pinia-plugin-persistedstate": "^3.2.3",
|
"pinia": "^3.0.3",
|
||||||
"plyr": "^3.7.8",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"vue-virtual-scroller": "2.0.0-beta.8"
|
"plyr": "^3.8.3",
|
||||||
|
"vue-virt-list": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^4",
|
||||||
"@fastify/cookie": "^9.4.0",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/http-proxy": "^9.5.0",
|
"@fastify/http-proxy": "^11.3.0",
|
||||||
"@fastify/multipart": "^8.3.0",
|
"@fastify/multipart": "^9.2.1",
|
||||||
"@fastify/static": "^7.0.4",
|
"@fastify/static": "^8.2.0",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/howler": "^2.2.11",
|
"@types/howler": "^2.2.12",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^22.5.4",
|
"@types/md5": "^2.3.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
"@types/node": "^24.7.2",
|
||||||
"@typescript-eslint/parser": "^7.18.0",
|
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||||
"@vitejs/plugin-vue": "^5.1.3",
|
"@typescript-eslint/parser": "^8.46.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"electron": "^28.3.3",
|
"electron": "38.2.2",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-log": "^5.2.0",
|
"electron-log": "^5.4.3",
|
||||||
"electron-vite": "^2.3.0",
|
"electron-vite": "^4.0.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^9.37.0",
|
||||||
"eslint-plugin-vue": "^9.28.0",
|
"eslint-plugin-vue": "^10.5.0",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.3",
|
||||||
"fastify": "^4.28.1",
|
"fastify": "^5.6.1",
|
||||||
"naive-ui": "^2.39.0",
|
"naive-ui": "^2.43.1",
|
||||||
"node-taglib-sharp": "^5.2.3",
|
"node-taglib-sharp": "^6.0.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.6.2",
|
||||||
"sass": "^1.78.0",
|
"sass": "^1.93.2",
|
||||||
"terser": "^5.33.0",
|
"terser": "^5.44.0",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.9.3",
|
||||||
"unplugin-auto-import": "^0.18.2",
|
"unplugin-auto-import": "^20.2.0",
|
||||||
"unplugin-vue-components": "^0.27.4",
|
"unplugin-vue-components": "^29.1.0",
|
||||||
"vite": "^5.4.3",
|
"vite": "^7.2.2",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-wasm": "^3.3.0",
|
"vite-plugin-vue-devtools": "^8.0.3",
|
||||||
"vue": "3.4.38",
|
"vite-plugin-wasm": "^3.5.0",
|
||||||
"vue-router": "^4.4.3",
|
"vue": "^3.5.24",
|
||||||
"vue-tsc": "^2.1.6"
|
"vue-router": "^4.5.1",
|
||||||
|
"vue-tsc": "^3.1.3"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"dmg-builder": "26.0.12",
|
||||||
|
"electron-builder-squirrel-windows": "26.0.12"
|
||||||
|
},
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@applemusic-like-lyrics/lyric",
|
||||||
|
"@parcel/watcher",
|
||||||
|
"core-js",
|
||||||
|
"electron",
|
||||||
|
"electron-winstaller",
|
||||||
|
"esbuild",
|
||||||
|
"sharp",
|
||||||
|
"vue-demi"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7284
pnpm-lock.yaml
generated
BIN
public/icons/logo-icon.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/icons/logo.ico
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/logo.ico
Normal file
|
After Width: | Height: | Size: 12 KiB |
90
scripts/dev.mjs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨平台开发启动脚本
|
||||||
|
* 自动检测操作系统并设置相应的字符编码
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import os from "os";
|
||||||
|
|
||||||
|
// 检测操作系统平台
|
||||||
|
const platform = os.platform();
|
||||||
|
const isWindows = platform === "win32";
|
||||||
|
const isMacOS = platform === "darwin";
|
||||||
|
|
||||||
|
console.log(`🚀 检测到操作系统: ${platform}`);
|
||||||
|
|
||||||
|
// 设置环境变量
|
||||||
|
const env = { ...process.env };
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
console.log("Windows 环境 - 正在设置代码页为 UTF-8");
|
||||||
|
// Windows 环境下先执行 chcp 65001
|
||||||
|
const chcp = spawn("chcp", ["65001"], {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: true,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
chcp.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
console.log("✅ 代码页设置成功");
|
||||||
|
startElectronVite();
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ 代码页设置失败,继续启动...");
|
||||||
|
startElectronVite();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// macOS 和 Linux 环境
|
||||||
|
console.log(`🐧 ${isMacOS ? "macOS" : "Linux"} 环境 - 正在设置 UTF-8 编码`);
|
||||||
|
const langVar = env.LC_ALL || env.LANG;
|
||||||
|
if (langVar.endsWith("UTF-8")) {
|
||||||
|
console.log("✅ 当前环境已设置 UTF-8 编码");
|
||||||
|
} else {
|
||||||
|
if (langVar.startsWith("zh_CN")) {
|
||||||
|
env.LC_ALL = "zh_CN.UTF-8";
|
||||||
|
env.LANG = "zh_CN.UTF-8";
|
||||||
|
} else {
|
||||||
|
env.LC_ALL = "en_US.UTF-8";
|
||||||
|
env.LANG = "en_US.UTF-8";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(() => startElectronVite(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const startElectronVite = () => {
|
||||||
|
console.log("🔧 正在启动 Electron Vite 开发服务器...");
|
||||||
|
|
||||||
|
// 设置 Node.js 选项
|
||||||
|
env.NODE_OPTIONS = "--max-old-space-size=4096";
|
||||||
|
|
||||||
|
const electronVite = spawn("electron-vite", ["dev"], {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: true,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
electronVite.on("close", (code) => {
|
||||||
|
console.log(`\n🏁 开发服务器已停止 (退出码: ${code})`);
|
||||||
|
process.exit(code);
|
||||||
|
});
|
||||||
|
|
||||||
|
electronVite.on("error", (err) => {
|
||||||
|
console.error("❌ 启动失败:", err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 优雅退出处理
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
console.log("\n🛑 正在停止开发服务器...");
|
||||||
|
electronVite.kill("SIGINT");
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
console.log("\n🛑 正在停止开发服务器...");
|
||||||
|
electronVite.kill("SIGTERM");
|
||||||
|
});
|
||||||
|
};
|
||||||
148
src/App.vue
@@ -1,151 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Provider>
|
<Provider>
|
||||||
<!-- 主框架 -->
|
<router-view />
|
||||||
<n-layout
|
|
||||||
id="main"
|
|
||||||
:class="{
|
|
||||||
'show-player': musicStore.isHasPlayer && statusStore.showPlayBar,
|
|
||||||
'show-full-player': statusStore.showFullPlayer,
|
|
||||||
}"
|
|
||||||
has-sider
|
|
||||||
>
|
|
||||||
<!-- 侧边栏 -->
|
|
||||||
<n-layout-sider
|
|
||||||
id="main-sider"
|
|
||||||
:style="{
|
|
||||||
height:
|
|
||||||
musicStore.isHasPlayer && statusStore.showPlayBar ? 'calc(100vh - 80px)' : '100vh',
|
|
||||||
}"
|
|
||||||
:content-style="{
|
|
||||||
overflow: 'hidden',
|
|
||||||
height: '100%',
|
|
||||||
padding: '0',
|
|
||||||
}"
|
|
||||||
:native-scrollbar="false"
|
|
||||||
:collapsed="statusStore.menuCollapsed"
|
|
||||||
:collapsed-width="64"
|
|
||||||
:width="240"
|
|
||||||
collapse-mode="width"
|
|
||||||
show-trigger="bar"
|
|
||||||
bordered
|
|
||||||
@collapse="statusStore.menuCollapsed = true"
|
|
||||||
@expand="statusStore.menuCollapsed = false"
|
|
||||||
>
|
|
||||||
<Sider />
|
|
||||||
</n-layout-sider>
|
|
||||||
<n-layout id="main-layout">
|
|
||||||
<!-- 导航栏 -->
|
|
||||||
<Nav id="main-header" />
|
|
||||||
<n-layout
|
|
||||||
ref="contentRef"
|
|
||||||
id="main-content"
|
|
||||||
:native-scrollbar="false"
|
|
||||||
:style="{
|
|
||||||
'--layout-height': contentHeight,
|
|
||||||
}"
|
|
||||||
:content-style="{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateRows: '1fr',
|
|
||||||
minHeight: '100%',
|
|
||||||
padding: '0 24px',
|
|
||||||
}"
|
|
||||||
position="absolute"
|
|
||||||
embedded
|
|
||||||
>
|
|
||||||
<!-- 路由页面 -->
|
|
||||||
<RouterView v-slot="{ Component }">
|
|
||||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
|
||||||
<KeepAlive :max="20" :exclude="['layout']">
|
|
||||||
<component :is="Component" class="router-view" />
|
|
||||||
</KeepAlive>
|
|
||||||
</Transition>
|
|
||||||
</RouterView>
|
|
||||||
<!-- 回顶 -->
|
|
||||||
<n-back-top :right="40" :bottom="120">
|
|
||||||
<SvgIcon :size="22" name="Up" />
|
|
||||||
</n-back-top>
|
|
||||||
</n-layout>
|
|
||||||
</n-layout>
|
|
||||||
</n-layout>
|
|
||||||
<!-- 播放列表 -->
|
|
||||||
<MainPlayList />
|
|
||||||
<!-- 全局播放器 -->
|
|
||||||
<MainPlayer />
|
|
||||||
<!-- 全屏播放器 -->
|
|
||||||
<Teleport to="body">
|
|
||||||
<Transition name="up" mode="out-in">
|
|
||||||
<FullPlayer
|
|
||||||
v-if="
|
|
||||||
statusStore.showFullPlayer ||
|
|
||||||
(statusStore.fullPlayerActive && settingStore.fullPlayerCache)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useMusicStore, useStatusStore, useSettingStore } from "@/stores";
|
|
||||||
import init from "@/utils/init";
|
|
||||||
|
|
||||||
const musicStore = useMusicStore();
|
|
||||||
const statusStore = useStatusStore();
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
// 主内容
|
|
||||||
const contentRef = ref<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
// 主内容高度
|
|
||||||
const { height: contentHeight } = useElementSize(contentRef);
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
statusStore.mainContentHeight = contentHeight.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await init();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
#main {
|
|
||||||
flex: 1;
|
|
||||||
height: 100%;
|
|
||||||
transition:
|
|
||||||
transform 0.3s var(--n-bezier),
|
|
||||||
opacity 0.3s var(--n-bezier);
|
|
||||||
#main-layout {
|
|
||||||
background-color: rgba(var(--background), 0.58);
|
|
||||||
}
|
|
||||||
#main-content {
|
|
||||||
top: 70px;
|
|
||||||
background-color: transparent;
|
|
||||||
transition: bottom 0.3s;
|
|
||||||
.router-view {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
&.n-result {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.show-player {
|
|
||||||
// #main-sider {
|
|
||||||
// margin-bottom: 80px;
|
|
||||||
// }
|
|
||||||
#main-content {
|
|
||||||
bottom: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.show-full-player {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
#main-header {
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -57,3 +57,40 @@ export const uploadCloudSong = (file: File) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 云盘导入歌曲
|
||||||
|
* @param {number} id - 歌曲 id
|
||||||
|
* @param {string} song - 歌曲名称
|
||||||
|
* @param {string} fileType - 歌曲格式
|
||||||
|
* @param {number} fileSize - 歌曲大小
|
||||||
|
* @param {number} bitrate - 歌曲比特率
|
||||||
|
* @param {string} md5 - 歌曲 md5
|
||||||
|
* @param {string} artist - 歌手
|
||||||
|
* @param {string} album - 专辑
|
||||||
|
*/
|
||||||
|
export const importCloudSong = (
|
||||||
|
song: string,
|
||||||
|
fileType: string,
|
||||||
|
fileSize: number,
|
||||||
|
bitrate: number,
|
||||||
|
md5: string,
|
||||||
|
id?: number,
|
||||||
|
artist?: string,
|
||||||
|
album?: string,
|
||||||
|
) => {
|
||||||
|
return request({
|
||||||
|
url: "/cloud/import",
|
||||||
|
method: "POST",
|
||||||
|
params: {id,
|
||||||
|
song,
|
||||||
|
fileType,
|
||||||
|
fileSize,
|
||||||
|
bitrate,
|
||||||
|
md5,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const updateLog = () => {
|
|||||||
return request({
|
return request({
|
||||||
baseURL: "https://api.github.com",
|
baseURL: "https://api.github.com",
|
||||||
withCredentials: false,
|
withCredentials: false,
|
||||||
url: "/repos/imsyy/SPlayer/releases/latest",
|
url: "/repos/imsyy/SPlayer/releases",
|
||||||
params: { noCookie: true },
|
params: { noCookie: true },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ export const dailyRecommend = (type: "songs" | "resource" = "songs") => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每日推荐 - 不感兴趣
|
||||||
|
*/
|
||||||
|
export const dailyRecommendDislike = (id: number) => {
|
||||||
|
return request({
|
||||||
|
url: "/recommend/songs/dislike",
|
||||||
|
params: { id, timestamp: Date.now() },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 推荐内容
|
* 推荐内容
|
||||||
* @param {string} [type] - 推荐类型
|
* @param {string} [type] - 推荐类型
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isElectron } from "@/utils/env";
|
||||||
import { songLevelData } from "@/utils/meta";
|
import { songLevelData } from "@/utils/meta";
|
||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
|
|
||||||
@@ -46,7 +47,11 @@ export const songUrl = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取解锁歌曲 URL
|
// 获取解锁歌曲 URL
|
||||||
export const unlockSongUrl = (id: number, keyword: string, server: "netease" | "kuwo") => {
|
export const unlockSongUrl = (
|
||||||
|
id: number,
|
||||||
|
keyword: string,
|
||||||
|
server: "netease" | "kuwo" | "bodian",
|
||||||
|
) => {
|
||||||
const params = server === "netease" ? { id } : { keyword };
|
const params = server === "netease" ? { id } : { keyword };
|
||||||
return request({
|
return request({
|
||||||
baseURL: "/api/unblock",
|
baseURL: "/api/unblock",
|
||||||
@@ -65,6 +70,29 @@ export const songLyric = (id: number) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取歌曲 TTML 歌词
|
||||||
|
* @param id 音乐 id
|
||||||
|
* @returns TTML 格式歌词
|
||||||
|
*/
|
||||||
|
export const songLyricTTML = async (id: number) => {
|
||||||
|
if (isElectron) {
|
||||||
|
return request({ url: "/lyric/ttml", params: { id, noCookie: true } });
|
||||||
|
} else {
|
||||||
|
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response === null || response.status !== 200) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = await response.text();
|
||||||
|
return data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取歌曲下载链接
|
* 获取歌曲下载链接
|
||||||
* @param id 音乐 id
|
* @param id 音乐 id
|
||||||
@@ -109,3 +137,25 @@ export const matchSong = (
|
|||||||
params: { title, artist, album, duration, md5 },
|
params: { title, artist, album, duration, md5 },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌曲动态封面
|
||||||
|
* @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 },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
|
|
||||||
// 获取账号详情
|
/**
|
||||||
|
* 获取用户账号信息
|
||||||
|
*/
|
||||||
export const userAccount = () => {
|
export const userAccount = () => {
|
||||||
return request({
|
return request({
|
||||||
url: "/user/account",
|
url: "/user/account",
|
||||||
|
|||||||
@@ -66,12 +66,13 @@ export const keywords = [
|
|||||||
"缩混",
|
"缩混",
|
||||||
"音乐总监",
|
"音乐总监",
|
||||||
"音乐制作",
|
"音乐制作",
|
||||||
"OP",
|
|
||||||
"SP",
|
|
||||||
"op",
|
|
||||||
"sp",
|
|
||||||
"Talkbox",
|
"Talkbox",
|
||||||
"Producers",
|
"Producers",
|
||||||
"Producer",
|
"Producer",
|
||||||
"Produced",
|
"Produced",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const regexes = [
|
||||||
|
/^[Oo][Pp]\s*[::]/,
|
||||||
|
/^[Ss][Pp]\s*[::]/,
|
||||||
|
].map((regex) => regex.source);
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
"id": 3136952023,
|
"id": 3136952023,
|
||||||
"name": "私人雷达"
|
"name": "私人雷达"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": 8402996200,
|
||||||
|
"name": "会员雷达"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": 5320167908,
|
"id": 5320167908,
|
||||||
"name": "时光雷达"
|
"name": "时光雷达"
|
||||||
|
|||||||
18
src/assets/data/lyricConfig.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { LyricConfig } from "../../types/desktop-lyric";
|
||||||
|
|
||||||
|
const config: LyricConfig = {
|
||||||
|
isLock: false,
|
||||||
|
playedColor: "#fe7971",
|
||||||
|
unplayedColor: "#ccc",
|
||||||
|
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
fontFamily: "system-ui",
|
||||||
|
fontSize: 24,
|
||||||
|
fontIsBold: false,
|
||||||
|
showTran: true,
|
||||||
|
showYrc: true,
|
||||||
|
isDoubleLine: true,
|
||||||
|
position: "both",
|
||||||
|
limitBounds: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
src/assets/icons/Close.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m12 13.4l-4.9 4.9q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7l4.9-4.9l-4.9-4.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l4.9 4.9l4.9-4.9q.275-.275.7-.275t.7.275t.275.7t-.275.7L13.4 12l4.9 4.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275z"/></svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
1
src/assets/icons/Collapse.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M9 15H6q-.425 0-.712-.288T5 14t.288-.712T6 13h4q.425 0 .713.288T11 14v4q0 .425-.288.713T10 19t-.712-.288T9 18zm6-6h3q.425 0 .713.288T19 10t-.288.713T18 11h-4q-.425 0-.712-.288T13 10V6q0-.425.288-.712T14 5t.713.288T15 6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 455 B |
1
src/assets/icons/Controls.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from MingCute Icon by MingCute Design - https://github.com/Richard9394/MingCute/blob/main/LICENSE --><g fill="none" fill-rule="evenodd"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M18 4a1 1 0 1 0-2 0v1H4a1 1 0 0 0 0 2h12v1a1 1 0 1 0 2 0V7h2a1 1 0 1 0 0-2h-2zM4 11a1 1 0 1 0 0 2h2v1a1 1 0 1 0 2 0v-1h12a1 1 0 1 0 0-2H8v-1a1 1 0 0 0-2 0v1zm-1 7a1 1 0 0 1 1-1h12v-1a1 1 0 1 1 2 0v1h2a1 1 0 1 1 0 2h-2v1a1 1 0 1 1-2 0v-1H4a1 1 0 0 1-1-1"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
6
src/assets/icons/DesktopLyric2.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg t="1760890100888" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1549"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<path
|
||||||
|
d="M192 240a112 112 0 0 1 111.616 102.784l0.384 9.216V832a16 16 0 0 0 12.352 15.552L320 848h66.816a48 48 0 0 1 6.528 95.552l-6.528 0.448H320a112 112 0 0 1-111.616-102.784L208 832V352a16 16 0 0 0-12.352-15.552L192 336H128a48 48 0 0 1-6.528-95.552L128 240h64z m640-157.568a112 112 0 0 1 111.616 102.848l0.384 9.152V832a112 112 0 0 1-102.784 111.616L832 944h-67.84a48 48 0 0 1-6.464-95.552l6.464-0.448H832a16 16 0 0 0 15.552-12.352L848 832V194.432a16 16 0 0 0-12.352-15.552L832 178.432H480a48 48 0 0 1-6.528-95.552l6.528-0.448H832z m-160 315.136c61.824 0 112 50.112 112 112v147.648a112 112 0 0 1-112 112h-128a112 112 0 0 1-112-112V509.568c0-61.888 50.176-112 112-112z m0 96h-128a16 16 0 0 0-16 16v147.648c0 8.832 7.168 16 16 16h128a16 16 0 0 0 16-16V509.568a16 16 0 0 0-16-16z m64-253.568a48 48 0 0 1 6.528 95.552l-6.528 0.448h-256a48 48 0 0 1-6.528-95.552L480 240h256zM256 82.432a48 48 0 0 1 6.528 95.616L256 178.432H128a48 48 0 0 1-6.528-95.552L128 82.432h128z"
|
||||||
|
fill="currentColor" p-id="1550"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
src/assets/icons/Eq.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M7 17V7q0-.425.288-.712T8 6t.713.288T9 7v10q0 .425-.288.713T8 18t-.712-.288T7 17m4 4V3q0-.425.288-.712T12 2t.713.288T13 3v18q0 .425-.288.713T12 22t-.712-.288T11 21m-8-8v-2q0-.425.288-.712T4 10t.713.288T5 11v2q0 .425-.288.713T4 14t-.712-.288T3 13m12 4V7q0-.425.288-.712T16 6t.713.288T17 7v10q0 .425-.288.713T16 18t-.712-.288T15 17m4-4v-2q0-.425.288-.712T20 10t.713.288T21 11v2q0 .425-.288.713T20 14t-.712-.288T19 13"/></svg>
|
||||||
|
After Width: | Height: | Size: 650 B |
1
src/assets/icons/Expand.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M7 17h3q.425 0 .713.288T11 18t-.288.713T10 19H6q-.425 0-.712-.288T5 18v-4q0-.425.288-.712T6 13t.713.288T7 14zM17 7h-3q-.425 0-.712-.288T13 6t.288-.712T14 5h4q.425 0 .713.288T19 6v4q0 .425-.288.713T18 11t-.712-.288T17 10z"/></svg>
|
||||||
|
After Width: | Height: | Size: 456 B |
1
src/assets/icons/FolderPlus.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M13 19c0 .34.04.67.09 1H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h8a2 2 0 0 1 2 2v5.81c-.88-.51-1.9-.81-3-.81c-3.31 0-6 2.69-6 6m7-1v-3h-2v3h-3v2h3v3h2v-3h3v-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 279 B |
1
src/assets/icons/HeartBroken.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12.025 20.35q-.35 0-.687-.125t-.613-.375Q8 17.45 6.3 15.812t-2.662-2.874t-1.3-2.263T2 8.5q0-2.3 1.6-3.9T7.5 3q1.65 0 2.9.637t.9 1.838l-.925 3.25q-.125.5.163.888t.787.387H13l-.65 6.35q-.025.2.163.225t.237-.15L14.6 10.3q.15-.5-.15-.9t-.8-.4H12l1.525-4.525Q13.8 3.6 14.675 3.3T16.5 3q2.3 0 3.9 1.6T22 8.5q0 1.1-.4 2.175t-1.388 2.375t-2.65 2.938t-4.212 3.862q-.275.25-.625.375t-.7.125"/></svg>
|
||||||
|
After Width: | Height: | Size: 617 B |
1
src/assets/icons/Help.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M11.95 18q.525 0 .888-.363t.362-.887t-.362-.888t-.888-.362t-.887.363t-.363.887t.363.888t.887.362m.05 4q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m.1-14.3q.625 0 1.088.4t.462 1q0 .55-.337.975t-.763.8q-.575.5-1.012 1.1t-.438 1.35q0 .35.263.588t.612.237q.375 0 .638-.25t.337-.625q.1-.525.45-.937t.75-.788q.575-.55.988-1.2t.412-1.45q0-1.275-1.037-2.087T12.1 6q-.95 0-1.812.4T8.975 7.625q-.175.3-.112.638t.337.512q.35.2.725.125t.625-.425q.275-.375.688-.575t.862-.2"/></svg>
|
||||||
|
After Width: | Height: | Size: 699 B |
4
src/assets/icons/Lock.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2M9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9zm9 14H6V10h12zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2s-2 .9-2 2s.9 2 2 2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 349 B |
1
src/assets/icons/LockOpen.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M6 20h12V10H6zm6-3q.825 0 1.413-.587T14 15t-.587-1.412T12 13t-1.412.588T10 15t.588 1.413T12 17m-6 3V10zm0 2q-.825 0-1.412-.587T4 20V10q0-.825.588-1.412T6 8h7V6q0-2.075 1.463-3.537T18 1q1.775 0 3.1 1.075t1.75 2.7q.125.425-.162.825T22 6q-.425 0-.7-.175t-.4-.575q-.275-.95-1.062-1.6T18 3q-1.25 0-2.125.875T15 6v2h3q.825 0 1.413.588T20 10v10q0 .825-.587 1.413T18 22z"/></svg>
|
||||||
|
After Width: | Height: | Size: 598 B |
1
src/assets/icons/Logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg class="prefix__icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M511.764 131.708A446.146 446.146 0 10957.91 577.854a446.146 446.146 0 00-446.146-446.146zm0 519.76a71.83 71.83 0 1171.83-70.937 72.276 72.276 0 01-71.83 70.937z" fill="#F55E55"/><path d="M802.205.541l-168.197 37.03a67.814 67.814 0 00-53.091 66.03v120.013l3.569 349.779h114.213V223.614h108.86a26.323 26.323 0 0026.769-26.322V26.864A26.769 26.769 0 00802.205.54z" fill="#F9BBB8"/><path d="M511.764 386.457A186.935 186.935 0 10698.7 572.947a186.935 186.935 0 00-186.935-186.49zm0 264.565a71.383 71.383 0 1171.383-71.383 71.383 71.383 0 01-71.383 71.383z" fill="#F9BBB8"/></svg>
|
||||||
|
After Width: | Height: | Size: 693 B |
1
src/assets/icons/PlayRate.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M10.45 15.5q.625.625 1.575.588T13.4 15.4L19 7l-8.4 5.6q-.65.45-.712 1.362t.562 1.538M5.1 20q-.55 0-1.012-.238t-.738-.712q-.65-1.175-1-2.437T2 14q0-2.075.788-3.9t2.137-3.175T8.1 4.788T12 4q2.05 0 3.85.775T19 6.888t2.15 3.125t.825 3.837q.025 1.375-.312 2.688t-1.038 2.512q-.275.475-.737.713T18.874 20z"/></svg>
|
||||||
|
After Width: | Height: | Size: 535 B |
4
src/assets/icons/TextSizeAdd.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M8.5 7h2L16 21h-2.4l-1.1-3H6.3l-1.1 3H3zm-1.4 9h4.8L9.5 9.7zM22 5v2h-3v3h-2V7h-3V5h3V2h2v3z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 227 B |
3
src/assets/icons/TextSizeReduce.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M10.5 7h-2L3 21h2.2l1.1-3h6.2l1.1 3H16zm-3.4 9l2.4-6.3l2.4 6.3zM22 7h-8V5h8z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 204 B |
1
src/assets/icons/TimeAuto.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M10.425 15.25h3.1l.375 1.15q.1.275.325.438t.5.162q.475 0 .738-.387t.112-.813l-2.35-6.625q-.1-.275-.35-.45t-.55-.175h-.65q-.3 0-.55.175t-.35.45L8.4 15.85q-.15.425.113.788t.712.362q.275 0 .5-.162t.325-.438zm.475-1.45l1.1-3.3l1.075 3.3zM10 3q-.425 0-.712-.288T9 2t.288-.712T10 1h4q.425 0 .713.288T15 2t-.288.713T14 3zm2 19q-1.85 0-3.488-.712T5.65 19.35t-1.937-2.863T3 13t.713-3.488T5.65 6.65t2.863-1.937T12 4q1.55 0 2.975.5t2.675 1.45l.7-.7q.275-.275.7-.275t.7.275t.275.7t-.275.7l-.7.7Q20 8.6 20.5 10.025T21 13q0 1.85-.713 3.488T18.35 19.35t-2.863 1.938T12 22"/></svg>
|
||||||
|
After Width: | Height: | Size: 792 B |
441
src/components/Card/SongCard.vue
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
<template>
|
||||||
|
<div class="song-card">
|
||||||
|
<div :class="['song-content', { play: musicStore.playSong.id === song.id }]">
|
||||||
|
<!-- 序号 -->
|
||||||
|
<div class="num" @dblclick.stop>
|
||||||
|
<n-text v-if="musicStore.playSong.id !== song.id" depth="3">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</n-text>
|
||||||
|
<SvgIcon v-else :size="22" name="Music" />
|
||||||
|
<!-- 播放暂停 -->
|
||||||
|
<SvgIcon
|
||||||
|
:size="28"
|
||||||
|
:name="statusStore.playStatus ? 'Pause' : 'Play'"
|
||||||
|
class="status"
|
||||||
|
@click="player.playOrPause()"
|
||||||
|
/>
|
||||||
|
<!-- 播放 -->
|
||||||
|
<SvgIcon :size="28" name="Play" class="play" @click="player.addNextSong(song, true)" />
|
||||||
|
</div>
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="title">
|
||||||
|
<!-- 封面 -->
|
||||||
|
<s-image
|
||||||
|
v-if="!hiddenCover"
|
||||||
|
:key="song.cover"
|
||||||
|
:src="song.path ? song.cover : song.coverSize?.s || song.cover"
|
||||||
|
class="cover"
|
||||||
|
@update:show="localCover"
|
||||||
|
/>
|
||||||
|
<!-- 信息 -->
|
||||||
|
<div class="info">
|
||||||
|
<!-- 名称 -->
|
||||||
|
<div class="name">
|
||||||
|
<n-ellipsis
|
||||||
|
:line-clamp="1"
|
||||||
|
:tooltip="{
|
||||||
|
placement: 'top',
|
||||||
|
width: 'trigger',
|
||||||
|
}"
|
||||||
|
class="name-text"
|
||||||
|
>
|
||||||
|
{{ song?.name || "未知曲目" }}
|
||||||
|
</n-ellipsis>
|
||||||
|
<!-- 音质 -->
|
||||||
|
<n-tag
|
||||||
|
v-if="song?.path && song?.quality"
|
||||||
|
:bordered="false"
|
||||||
|
:type="song.quality === 'Hi-Res' ? 'warning' : 'info'"
|
||||||
|
class="quality"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{ song.quality }}
|
||||||
|
</n-tag>
|
||||||
|
<!-- 特权 -->
|
||||||
|
<n-tag v-if="song.originCoverType === 1" :bordered="false" type="primary" round>
|
||||||
|
原
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-if="song.originCoverType === 2" :bordered="false" type="info" round>
|
||||||
|
翻唱
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-if="song.free === 1" :bordered="false" type="error" round> VIP </n-tag>
|
||||||
|
<n-tag v-if="song.free === 4" :bordered="false" type="error" round> EP </n-tag>
|
||||||
|
<!-- 云盘 -->
|
||||||
|
<n-tag v-if="song?.pc" :bordered="false" class="cloud" type="info" round>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon name="Cloud" />
|
||||||
|
</template>
|
||||||
|
</n-tag>
|
||||||
|
<!-- MV -->
|
||||||
|
<n-tag
|
||||||
|
v-if="song?.mv"
|
||||||
|
:bordered="false"
|
||||||
|
class="mv"
|
||||||
|
type="warning"
|
||||||
|
round
|
||||||
|
@click.stop="
|
||||||
|
router.push({
|
||||||
|
name: 'video',
|
||||||
|
query: { id: song.mv },
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
MV
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
<!-- 歌手 -->
|
||||||
|
<div v-if="Array.isArray(song.artists)" class="artists text-hidden">
|
||||||
|
<n-text
|
||||||
|
v-for="ar in song.artists"
|
||||||
|
:key="ar.id"
|
||||||
|
class="ar"
|
||||||
|
@click="openJumpArtist(song.artists)"
|
||||||
|
>
|
||||||
|
{{ ar.name }}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="song.type === 'radio'" class="artists">
|
||||||
|
<n-text class="ar"> 电台节目 </n-text>
|
||||||
|
</div>
|
||||||
|
<div v-else class="artists text-hidden" @click="openJumpArtist(song.artists)">
|
||||||
|
<n-text class="ar"> {{ song.artists || "未知艺术家" }} </n-text>
|
||||||
|
</div>
|
||||||
|
<!-- 别名 -->
|
||||||
|
<n-text v-if="song.alia" class="alia text-hidden" depth="3">{{ song.alia }}</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 专辑 -->
|
||||||
|
<div v-if="song.type !== 'radio' && !hiddenAlbum" class="album text-hidden">
|
||||||
|
<n-text
|
||||||
|
v-if="isObject(song.album)"
|
||||||
|
class="album-text"
|
||||||
|
@click="
|
||||||
|
router.push({
|
||||||
|
name: 'album',
|
||||||
|
query: { id: song.album?.id },
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ song.album?.name || "未知专辑" }}
|
||||||
|
</n-text>
|
||||||
|
<n-text v-else class="album-text">
|
||||||
|
{{ song.album || "未知专辑" }}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
<!-- 操作 -->
|
||||||
|
<div v-if="song.type !== 'radio'" class="actions" @click.stop @dblclick.stop>
|
||||||
|
<!-- 喜欢歌曲 -->
|
||||||
|
<SvgIcon
|
||||||
|
:name="dataStore.isLikeSong(song.id) ? 'Favorite' : 'FavoriteBorder'"
|
||||||
|
:size="20"
|
||||||
|
@click.stop="toLikeSong(song, !dataStore.isLikeSong(song.id))"
|
||||||
|
@delclick.stop
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 更新日期 -->
|
||||||
|
<n-text v-if="song.type === 'radio'" class="meta date" depth="3">
|
||||||
|
{{ formatTimestamp(song.updateTime) }}
|
||||||
|
</n-text>
|
||||||
|
<!-- 播放量 -->
|
||||||
|
<n-text v-if="song.type === 'radio'" class="meta" depth="3">
|
||||||
|
{{ formatNumber(song.playCount || 0) }}
|
||||||
|
</n-text>
|
||||||
|
<!-- 时长 -->
|
||||||
|
<n-text class="meta" depth="3">{{ msToTime(song.duration) }}</n-text>
|
||||||
|
<!-- 大小 -->
|
||||||
|
<n-text v-if="song.path && song.size && !hiddenSize" class="meta size" depth="3">
|
||||||
|
{{ song.size }}M
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SongType } from "@/types/main";
|
||||||
|
import { useStatusStore, useMusicStore, useDataStore } from "@/stores";
|
||||||
|
import { formatNumber } from "@/utils/helper";
|
||||||
|
import { openJumpArtist } from "@/utils/modal";
|
||||||
|
import { toLikeSong } from "@/utils/auth";
|
||||||
|
import { isObject } from "lodash-es";
|
||||||
|
import { formatTimestamp, msToTime } from "@/utils/time";
|
||||||
|
import player from "@/utils/player";
|
||||||
|
import blob from "@/utils/blob";
|
||||||
|
import { isElectron } from "@/utils/env";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
// 歌曲
|
||||||
|
song: SongType;
|
||||||
|
// 索引
|
||||||
|
index: number;
|
||||||
|
// 隐藏信息
|
||||||
|
hiddenCover?: boolean;
|
||||||
|
hiddenAlbum?: boolean;
|
||||||
|
hiddenSize?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const dataStore = useDataStore();
|
||||||
|
const musicStore = useMusicStore();
|
||||||
|
const statusStore = useStatusStore();
|
||||||
|
|
||||||
|
// 歌曲数据
|
||||||
|
const song = toRef(props, "song");
|
||||||
|
|
||||||
|
// 加载本地歌曲封面
|
||||||
|
const localCover = async (show: boolean) => {
|
||||||
|
if (!isElectron || !show || !song.value.path) return;
|
||||||
|
if (song.value.cover || song.value.cover === "/images/song.jpg?assest") return;
|
||||||
|
// 获取封面
|
||||||
|
const coverData = await window.electron.ipcRenderer.invoke("get-music-cover", song.value.path);
|
||||||
|
if (!coverData) return;
|
||||||
|
const { data, format } = coverData;
|
||||||
|
const blobURL = blob.createBlobURL(data, format, song.value.path);
|
||||||
|
if (blobURL) song.value.cover = blobURL;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.song-card {
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
.song-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
flex: 1;
|
||||||
|
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);
|
||||||
|
&.play {
|
||||||
|
border-color: rgba(var(--primary), 0.58);
|
||||||
|
background-color: rgba(var(--primary), 0.28);
|
||||||
|
}
|
||||||
|
// &:active {
|
||||||
|
// transform: scale(0.99);
|
||||||
|
// }
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(var(--primary), 0.58);
|
||||||
|
.num {
|
||||||
|
.n-text,
|
||||||
|
.n-icon {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.play {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.play {
|
||||||
|
.num {
|
||||||
|
.play {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.num {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 12px;
|
||||||
|
.n-icon {
|
||||||
|
transition:
|
||||||
|
opacity 0.3s,
|
||||||
|
transform 0.3s;
|
||||||
|
:deep(.svg-container) {
|
||||||
|
color: var(--primary-hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.status,
|
||||||
|
.play {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
transition:
|
||||||
|
opacity 0.3s,
|
||||||
|
transform 0.3s;
|
||||||
|
&:active {
|
||||||
|
opacity: 0.6 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 20px 4px 0;
|
||||||
|
.cover {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
min-width: 50px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-right: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
:deep(.name-text) {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.n-tag {
|
||||||
|
--n-height: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 6px;
|
||||||
|
pointer-events: none;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.quality {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.cloud {
|
||||||
|
padding: 0 10px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
:deep(.n-tag__icon) {
|
||||||
|
margin-right: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.n-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--n-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mv {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.artists {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
.ar {
|
||||||
|
display: inline-flex;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: pointer;
|
||||||
|
&::after {
|
||||||
|
content: "/";
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alia {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sort {
|
||||||
|
margin-left: 6px;
|
||||||
|
&::after {
|
||||||
|
content: " )";
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
content: "( ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.album {
|
||||||
|
flex: 1;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
padding-right: 20px;
|
||||||
|
&:hover {
|
||||||
|
.album-text {
|
||||||
|
color: var(--primary-hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
.n-icon {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
width: 50px;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
&.size {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
&.date {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.header {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
.n-text {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 20px 0 0;
|
||||||
|
&.has-sort {
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
top: 0;
|
||||||
|
left: -8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgba(var(--primary), 0.08);
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
&::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -49,8 +49,12 @@ const settingStore = useSettingStore();
|
|||||||
// 操作系统主题
|
// 操作系统主题
|
||||||
const osTheme = useOsTheme();
|
const osTheme = useOsTheme();
|
||||||
|
|
||||||
// 全局主题
|
// 全局主题(使用 shallowRef 避免深层追踪开销)
|
||||||
const themeOverrides = ref<GlobalThemeOverrides>({});
|
const themeOverrides = shallowRef<GlobalThemeOverrides>({});
|
||||||
|
// 轻量的 rgba 构造器
|
||||||
|
const toRGBA = (rgb: string, alpha: number) => `rgba(${rgb}, ${alpha})`;
|
||||||
|
// 主题缓存键
|
||||||
|
let lastThemeCacheKey: string | null = null;
|
||||||
|
|
||||||
// 获取明暗模式
|
// 获取明暗模式
|
||||||
const theme = computed(() => {
|
const theme = computed(() => {
|
||||||
@@ -88,118 +92,133 @@ const changeGlobalTheme = () => {
|
|||||||
themeOverrides.value = {};
|
themeOverrides.value = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 修改主题
|
// 构造主题缓存 Key
|
||||||
themeOverrides.value = settingStore.themeGlobalColor
|
const themeModeLabel = theme.value ? "dark" : "light";
|
||||||
? {
|
const themeCacheKey = `${themeModeLabel}|${settingStore.themeGlobalColor ? 1 : 0}|${settingStore.globalFont}|${colorSchemes.primary}|${colorSchemes.background}|${colorSchemes["surface-container"]}`;
|
||||||
common: {
|
if (lastThemeCacheKey === themeCacheKey) return;
|
||||||
fontFamily: `${settingStore.globalFont === "default" ? "v-sans" : settingStore.globalFont}, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
|
lastThemeCacheKey = themeCacheKey;
|
||||||
primaryColor: `rgb(${colorSchemes.primary})`,
|
|
||||||
primaryColorHover: `rgba(${colorSchemes.primary}, 0.78)`,
|
// 关键颜色
|
||||||
primaryColorPressed: `rgba(${colorSchemes.primary}, 0.26)`,
|
const primaryRGB = colorSchemes.primary as string;
|
||||||
primaryColorSuppl: `rgba(${colorSchemes.primary}, 0.12)`,
|
const surfaceContainerRGB = colorSchemes["surface-container"] as string;
|
||||||
textColorBase: colorSchemes.primary,
|
|
||||||
textColor1: `rgb(${colorSchemes.primary})`,
|
// 全局字体
|
||||||
textColor2: `rgba(${colorSchemes.primary}, 0.82)`,
|
const fontFamily = `${settingStore.globalFont === "default" ? "v-sans" : settingStore.globalFont}, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
|
||||||
textColor3: `rgba(${colorSchemes.primary}, 0.52)`,
|
|
||||||
bodyColor: `rgb(${colorSchemes.background})`,
|
// 通用样式基座
|
||||||
cardColor: `rgb(${colorSchemes["surface-container"]})`,
|
const commonBase = {
|
||||||
tagColor: `rgb(${colorSchemes["surface-container"]})`,
|
fontFamily,
|
||||||
modalColor: `rgb(${colorSchemes["surface-container"]})`,
|
primaryColor: `rgb(${primaryRGB})`,
|
||||||
popoverColor: `rgb(${colorSchemes["surface-container"]})`,
|
primaryColorHover: toRGBA(primaryRGB, 0.78),
|
||||||
buttonColor2: `rgba(${colorSchemes.primary}, 0.08)`,
|
primaryColorPressed: toRGBA(primaryRGB, 0.26),
|
||||||
buttonColor2Hover: `rgba(${colorSchemes.primary}, 0.12)`,
|
primaryColorSuppl: toRGBA(primaryRGB, 0.12),
|
||||||
buttonColor2Pressed: `rgba(${colorSchemes.primary}, 0.08)`,
|
} as GlobalThemeOverrides["common"];
|
||||||
iconColor: `rgb(${colorSchemes.primary})`,
|
|
||||||
iconColorHover: `rgba(${colorSchemes.primary}, 0.475)`,
|
if (settingStore.themeGlobalColor) {
|
||||||
closeIconColor: `rgba(${colorSchemes.primary}, 0.58)`,
|
themeOverrides.value = {
|
||||||
hoverColor: `rgba(${colorSchemes.primary}, 0.09)`,
|
common: {
|
||||||
borderColor: `rgba(${colorSchemes.primary}, 0.09)`,
|
...commonBase,
|
||||||
textColorDisabled: `rgba(${colorSchemes.primary}, 0.3)`,
|
textColorBase: primaryRGB,
|
||||||
placeholderColorDisabled: `rgba(${colorSchemes.primary}, 0.3)`,
|
textColor1: `rgb(${primaryRGB})`,
|
||||||
iconColorDisabled: `rgba(${colorSchemes.primary}, 0.3)`,
|
textColor2: toRGBA(primaryRGB, 0.82),
|
||||||
},
|
textColor3: toRGBA(primaryRGB, 0.52),
|
||||||
Card: {
|
bodyColor: `rgb(${colorSchemes.background})`,
|
||||||
borderColor: `rgba(${colorSchemes.primary}, 0.09)`,
|
cardColor: `rgb(${surfaceContainerRGB})`,
|
||||||
},
|
tagColor: `rgb(${surfaceContainerRGB})`,
|
||||||
Button: {
|
modalColor: `rgb(${surfaceContainerRGB})`,
|
||||||
textColorHover: `rgba(${colorSchemes.primary}, 0.78)`,
|
popoverColor: `rgb(${surfaceContainerRGB})`,
|
||||||
textColorFocus: `rgba(${colorSchemes.primary}, 0.58)`,
|
buttonColor2: toRGBA(primaryRGB, 0.08),
|
||||||
colorPrimary: `rgba(${colorSchemes.primary}, 0.9)`,
|
buttonColor2Hover: toRGBA(primaryRGB, 0.12),
|
||||||
colorHoverPrimary: `rgb(${colorSchemes.primary})`,
|
buttonColor2Pressed: toRGBA(primaryRGB, 0.08),
|
||||||
colorPressedPrimary: `rgba(${colorSchemes.primary}, 0.8)`,
|
iconColor: `rgb(${primaryRGB})`,
|
||||||
colorFocusPrimary: `rgb(${colorSchemes.primary})`,
|
iconColorHover: toRGBA(primaryRGB, 0.475),
|
||||||
},
|
closeIconColor: toRGBA(primaryRGB, 0.58),
|
||||||
Slider: {
|
hoverColor: toRGBA(primaryRGB, 0.09),
|
||||||
handleColor: `rgb(${colorSchemes.primary})`,
|
borderColor: toRGBA(primaryRGB, 0.09),
|
||||||
fillColor: `rgb(${colorSchemes.primary})`,
|
textColorDisabled: toRGBA(primaryRGB, 0.3),
|
||||||
fillColorHover: `rgb(${colorSchemes.primary})`,
|
placeholderColorDisabled: toRGBA(primaryRGB, 0.3),
|
||||||
railColor: `rgba(${colorSchemes.primary}, 0.2)`,
|
iconColorDisabled: toRGBA(primaryRGB, 0.3),
|
||||||
railColorHover: `rgba(${colorSchemes.primary}, 0.3)`,
|
},
|
||||||
},
|
Card: {
|
||||||
Switch: {
|
borderColor: toRGBA(primaryRGB, 0.09),
|
||||||
railColorActive: `rgba(${colorSchemes.primary}, 0.8)`,
|
},
|
||||||
},
|
Button: {
|
||||||
Input: {
|
textColorHover: toRGBA(primaryRGB, 0.78),
|
||||||
color: `rgba(${colorSchemes.primary}, 0.1)`,
|
textColorFocus: toRGBA(primaryRGB, 0.58),
|
||||||
colorFocus: `rgb(${colorSchemes["surface-container"]})`,
|
colorPrimary: toRGBA(primaryRGB, 0.9),
|
||||||
placeholderColor: `rgba(${colorSchemes.primary}, 0.58)`,
|
colorHoverPrimary: `rgb(${primaryRGB})`,
|
||||||
border: `1px solid rgba(${colorSchemes.primary}, 0.1)`,
|
colorPressedPrimary: toRGBA(primaryRGB, 0.8),
|
||||||
clearColor: `rgba(${colorSchemes.primary}, 0.38)`,
|
colorFocusPrimary: `rgb(${primaryRGB})`,
|
||||||
clearColorHover: `rgba(${colorSchemes.primary}, 0.48)`,
|
},
|
||||||
clearColorPressed: `rgba(${colorSchemes.primary}, 0.3)`,
|
Slider: {
|
||||||
},
|
handleColor: `rgb(${primaryRGB})`,
|
||||||
Icon: {
|
fillColor: `rgb(${primaryRGB})`,
|
||||||
color: `rgb(${colorSchemes.primary})`,
|
fillColorHover: `rgb(${primaryRGB})`,
|
||||||
},
|
railColor: toRGBA(primaryRGB, 0.2),
|
||||||
Empty: {
|
railColorHover: toRGBA(primaryRGB, 0.3),
|
||||||
textColor: `rgba(${colorSchemes.primary}, 0.38)`,
|
},
|
||||||
},
|
Switch: {
|
||||||
Divider: {
|
railColorActive: toRGBA(primaryRGB, 0.8),
|
||||||
color: `rgba(${colorSchemes.primary}, 0.09)`,
|
},
|
||||||
},
|
Input: {
|
||||||
Dropdown: {
|
color: toRGBA(primaryRGB, 0.1),
|
||||||
dividerColor: `rgba(${colorSchemes.primary}, 0.09)`,
|
colorFocus: `rgb(${surfaceContainerRGB})`,
|
||||||
},
|
placeholderColor: toRGBA(primaryRGB, 0.58),
|
||||||
Layout: {
|
border: `1px solid ${toRGBA(primaryRGB, 0.1)}`,
|
||||||
siderBorderColor: `rgba(${colorSchemes.primary}, 0.09)`,
|
clearColor: toRGBA(primaryRGB, 0.38),
|
||||||
},
|
clearColorHover: toRGBA(primaryRGB, 0.48),
|
||||||
Tabs: {
|
clearColorPressed: toRGBA(primaryRGB, 0.3),
|
||||||
colorSegment: `rgba(${colorSchemes.primary}, 0.08)`,
|
},
|
||||||
tabColorSegment: `rgba(${colorSchemes.primary}, 0.12)`,
|
Icon: {
|
||||||
},
|
color: `rgb(${primaryRGB})`,
|
||||||
Drawer: {
|
},
|
||||||
headerBorderBottom: `1px solid rgba(${colorSchemes.primary}, 0.09)`,
|
Empty: {
|
||||||
footerBorderTop: `1px solid rgba(${colorSchemes.primary}, 0.09)`,
|
textColor: toRGBA(primaryRGB, 0.38),
|
||||||
},
|
},
|
||||||
Menu: {
|
Divider: {
|
||||||
dividerColor: `rgba(${colorSchemes.primary}, 0.09)`,
|
color: toRGBA(primaryRGB, 0.09),
|
||||||
},
|
},
|
||||||
Progress: {
|
Dropdown: {
|
||||||
railColor: `rgba(${colorSchemes.primary}, 0.16)`,
|
dividerColor: toRGBA(primaryRGB, 0.09),
|
||||||
},
|
},
|
||||||
Popover: {
|
Layout: {
|
||||||
color: `rgb(${colorSchemes["surface-container"]})`,
|
siderBorderColor: toRGBA(primaryRGB, 0.09),
|
||||||
},
|
},
|
||||||
}
|
Tabs: {
|
||||||
: {
|
colorSegment: toRGBA(primaryRGB, 0.08),
|
||||||
common: {
|
tabColorSegment: toRGBA(primaryRGB, 0.12),
|
||||||
fontFamily: `${settingStore.globalFont === "default" ? "v-sans" : settingStore.globalFont}, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
|
},
|
||||||
primaryColor: `rgb(${colorSchemes.primary})`,
|
Drawer: {
|
||||||
primaryColorHover: `rgba(${colorSchemes.primary}, 0.78)`,
|
headerBorderBottom: `1px solid ${toRGBA(primaryRGB, 0.09)}`,
|
||||||
primaryColorPressed: `rgba(${colorSchemes.primary}, 0.26)`,
|
footerBorderTop: `1px solid ${toRGBA(primaryRGB, 0.09)}`,
|
||||||
primaryColorSuppl: `rgba(${colorSchemes.primary}, 0.12)`,
|
},
|
||||||
},
|
Menu: {
|
||||||
Icon: {
|
dividerColor: toRGBA(primaryRGB, 0.09),
|
||||||
color: `rgb(${colorSchemes.primary})`,
|
},
|
||||||
},
|
Progress: {
|
||||||
Slider: {
|
railColor: toRGBA(primaryRGB, 0.16),
|
||||||
handleColor: `rgb(${colorSchemes.primary})`,
|
},
|
||||||
fillColor: `rgb(${colorSchemes.primary})`,
|
Popover: {
|
||||||
fillColorHover: `rgb(${colorSchemes.primary})`,
|
color: `rgb(${surfaceContainerRGB})`,
|
||||||
railColor: `rgba(${colorSchemes.primary}, 0.2)`,
|
},
|
||||||
railColorHover: `rgba(${colorSchemes.primary}, 0.3)`,
|
};
|
||||||
},
|
} else {
|
||||||
};
|
themeOverrides.value = {
|
||||||
|
common: {
|
||||||
|
...commonBase,
|
||||||
|
},
|
||||||
|
Icon: {
|
||||||
|
color: `rgb(${primaryRGB})`,
|
||||||
|
},
|
||||||
|
Slider: {
|
||||||
|
handleColor: `rgb(${primaryRGB})`,
|
||||||
|
fillColor: `rgb(${primaryRGB})`,
|
||||||
|
fillColorHover: `rgb(${primaryRGB})`,
|
||||||
|
railColor: toRGBA(primaryRGB, 0.2),
|
||||||
|
railColorHover: toRGBA(primaryRGB, 0.3),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
themeOverrides.value = {};
|
themeOverrides.value = {};
|
||||||
console.error("切换主题色出现错误:", error);
|
console.error("切换主题色出现错误:", error);
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<!-- 全局图标 -->
|
<!-- 全局图标 -->
|
||||||
<template>
|
<template>
|
||||||
<n-icon v-if="name" :size="size" :color="color" :depth="depth" v-html="svgContent" />
|
<n-icon v-if="name" :size="size" :color="color" :depth="depth">
|
||||||
|
<div
|
||||||
|
ref="svgContainer"
|
||||||
|
:style="{ transform: offset ? `translateY(${offset}px)` : undefined }"
|
||||||
|
class="svg-container"
|
||||||
|
/>
|
||||||
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -8,18 +14,21 @@ const props = defineProps<{
|
|||||||
name: string;
|
name: string;
|
||||||
size?: string | number;
|
size?: string | number;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
offset?: number;
|
||||||
depth?: 1 | 2 | 3 | 4 | 5;
|
depth?: 1 | 2 | 3 | 4 | 5;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const svgContent = ref("");
|
const svgContent = ref<string>("");
|
||||||
|
const svgContainer = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
// 加载图标
|
// 加载图标
|
||||||
const loadSVG = async (name: string) => {
|
const loadSVG = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
const svg = await import(`../../assets/icons/${name}.svg?raw`);
|
const svg = await import(`../../assets/icons/${name}.svg?raw`);
|
||||||
svgContent.value = svg.default || svg;
|
svgContent.value = svg.default || svg;
|
||||||
|
if (svgContainer.value) svgContainer.value.innerHTML = svgContent.value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Could not load SVG for icon name: ${name}`);
|
console.error(`Could not load SVG for icon name: ${name}`, error);
|
||||||
svgContent.value = "";
|
svgContent.value = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -37,6 +46,12 @@ onMounted(() => loadSVG(props.name));
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
// transition: all 0.3s;
|
// transition: all 0.3s;
|
||||||
color: var(--primary-hex);
|
// color: var(--primary-hex);
|
||||||
|
.svg-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
// color: var(--primary-hex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const updateScroll = () => {
|
|||||||
|
|
||||||
// 滚动动画定时器
|
// 滚动动画定时器
|
||||||
let animationId: number | null = null;
|
let animationId: number | null = null;
|
||||||
let scrollTimeoutId: NodeJS.Timeout | null = null;
|
let scrollTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
// 开始滚动
|
// 开始滚动
|
||||||
const startScrolling = () => {
|
const startScrolling = () => {
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ import {
|
|||||||
import type { CoverType } from "@/types/main";
|
import type { CoverType } from "@/types/main";
|
||||||
import { useStatusStore, useSettingStore, useDataStore, useMusicStore } from "@/stores";
|
import { useStatusStore, useSettingStore, useDataStore, useMusicStore } from "@/stores";
|
||||||
import { useRouter, RouterLink } from "vue-router";
|
import { useRouter, RouterLink } from "vue-router";
|
||||||
import { isElectron, renderIcon } from "@/utils/helper";
|
import { renderIcon } from "@/utils/helper";
|
||||||
import { openCreatePlaylist } from "@/utils/modal";
|
import { openCreatePlaylist } from "@/utils/modal";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import player from "@/utils/player";
|
|
||||||
import { isLogin } from "@/utils/auth";
|
import { isLogin } from "@/utils/auth";
|
||||||
|
import { isElectron } from "@/utils/env";
|
||||||
|
import player from "@/utils/player";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const dataStore = useDataStore();
|
const dataStore = useDataStore();
|
||||||
@@ -72,7 +73,7 @@ const menuOptions = computed<MenuOption[] | MenuGroupOption[]>(() => {
|
|||||||
{
|
{
|
||||||
key: "personal-fm",
|
key: "personal-fm",
|
||||||
label: "私人漫游",
|
label: "私人漫游",
|
||||||
show: isLogin(),
|
show: isLogin() !== 0,
|
||||||
icon: renderIcon("Radio", {
|
icon: renderIcon("Radio", {
|
||||||
style: {
|
style: {
|
||||||
transform: "translateY(-1px)",
|
transform: "translateY(-1px)",
|
||||||
@@ -122,7 +123,7 @@ const menuOptions = computed<MenuOption[] | MenuGroupOption[]>(() => {
|
|||||||
key: "cloud",
|
key: "cloud",
|
||||||
link: "cloud",
|
link: "cloud",
|
||||||
label: "我的云盘",
|
label: "我的云盘",
|
||||||
show: isElectron,
|
show: isLogin() === 1,
|
||||||
icon: renderIcon("Cloud"),
|
icon: renderIcon("Cloud"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -176,11 +177,20 @@ const menuOptions = computed<MenuOption[] | MenuGroupOption[]>(() => {
|
|||||||
children: [...likedPlaylist.value],
|
children: [...likedPlaylist.value],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [
|
||||||
|
{
|
||||||
|
key: "local",
|
||||||
|
link: "local",
|
||||||
|
label: "本地歌曲",
|
||||||
|
show: isElectron,
|
||||||
|
icon: renderIcon("FolderMusic"),
|
||||||
|
},
|
||||||
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生成歌单列表
|
// 生成歌单列表
|
||||||
const renderPlaylist = (playlist: CoverType[], showCover: boolean) => {
|
const renderPlaylist = (playlist: CoverType[], showCover: boolean) => {
|
||||||
|
if (!isLogin()) return [];
|
||||||
return playlist.map((playlist) => ({
|
return playlist.map((playlist) => ({
|
||||||
key: playlist.id,
|
key: playlist.id,
|
||||||
label: () =>
|
label: () =>
|
||||||
|
|||||||
@@ -91,8 +91,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DropdownOption } from "naive-ui";
|
import type { DropdownOption } from "naive-ui";
|
||||||
import { useSettingStore } from "@/stores";
|
import { useSettingStore } from "@/stores";
|
||||||
import { isElectron, isDev, renderIcon } from "@/utils/helper";
|
import { renderIcon } from "@/utils/helper";
|
||||||
import { openSetting } from "@/utils/modal";
|
import { openSetting } from "@/utils/modal";
|
||||||
|
import { isDev, isElectron } from "@/utils/env";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
@@ -110,10 +111,8 @@ const min = () => window.electron.ipcRenderer.send("win-min");
|
|||||||
// 最大化或还原
|
// 最大化或还原
|
||||||
const maxOrRes = () => {
|
const maxOrRes = () => {
|
||||||
if (window.electron.ipcRenderer.sendSync("win-state")) {
|
if (window.electron.ipcRenderer.sendSync("win-state")) {
|
||||||
isMax.value = false;
|
|
||||||
window.electron.ipcRenderer.send("win-restore");
|
window.electron.ipcRenderer.send("win-restore");
|
||||||
} else {
|
} else {
|
||||||
isMax.value = true;
|
|
||||||
window.electron.ipcRenderer.send("win-max");
|
window.electron.ipcRenderer.send("win-max");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -125,7 +124,7 @@ const hideOrClose = (action: "hide" | "exit") => {
|
|||||||
settingStore.closeAppMethod = action;
|
settingStore.closeAppMethod = action;
|
||||||
}
|
}
|
||||||
showCloseModal.value = false;
|
showCloseModal.value = false;
|
||||||
window.electron.ipcRenderer.send(action === "hide" ? "win-hide" : "win-close");
|
window.electron.ipcRenderer.send(action === "hide" ? "win-hide" : "quit-app");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 尝试关闭软件
|
// 尝试关闭软件
|
||||||
@@ -198,9 +197,15 @@ const setSelect = (key: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 获取窗口状态
|
// 获取窗口状态并监听主进程的状态变更
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
isMax.value = window.electron.ipcRenderer.sendSync("win-state");
|
isMax.value = window.electron.ipcRenderer.sendSync("win-state");
|
||||||
|
window.electron.ipcRenderer.on("win-state-change", (_event, value: boolean) => {
|
||||||
|
isMax.value = value;
|
||||||
|
});
|
||||||
|
window.electron.ipcRenderer.on("win-will-close", () => {
|
||||||
|
tryClose();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="user-menu" @click="userMenuShow = false">
|
<div class="user-menu" @click="userMenuShow = false">
|
||||||
<!-- 喜欢数量 -->
|
<!-- 喜欢数量 -->
|
||||||
<div class="like-num">
|
<div v-if="dataStore.loginType !== 'uid'" class="like-num">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in userLikeData"
|
v-for="(item, index) in userLikeData"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -44,6 +44,10 @@
|
|||||||
<n-text :depth="3">{{ item.label }}</n-text>
|
<n-text :depth="3">{{ item.label }}</n-text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<n-flex v-else align="center" vertical>
|
||||||
|
<n-text>UID 登录模式</n-text>
|
||||||
|
<n-text :depth="3">部分功能暂不可用</n-text>
|
||||||
|
</n-flex>
|
||||||
<n-divider />
|
<n-divider />
|
||||||
<!-- 退出登录 -->
|
<!-- 退出登录 -->
|
||||||
<n-button :focusable="false" class="logout" strong secondary round @click="isLogout">
|
<n-button :focusable="false" class="logout" strong secondary round @click="isLogout">
|
||||||
@@ -60,7 +64,13 @@
|
|||||||
import { useDataStore } from "@/stores";
|
import { useDataStore } from "@/stores";
|
||||||
import { openUserLogin } from "@/utils/modal";
|
import { openUserLogin } from "@/utils/modal";
|
||||||
import { getLoginState } from "@/api/login";
|
import { getLoginState } from "@/api/login";
|
||||||
import { toLogout, isLogin, refreshLoginData, updateUserData } from "@/utils/auth";
|
import {
|
||||||
|
toLogout,
|
||||||
|
isLogin,
|
||||||
|
refreshLoginData,
|
||||||
|
updateUserData,
|
||||||
|
updateSpecialUserData,
|
||||||
|
} from "@/utils/auth";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const dataStore = useDataStore();
|
const dataStore = useDataStore();
|
||||||
@@ -100,6 +110,11 @@ const userLikeData = computed(() => {
|
|||||||
|
|
||||||
// 检查登录状态
|
// 检查登录状态
|
||||||
const checkLoginStatus = async () => {
|
const checkLoginStatus = async () => {
|
||||||
|
// 若为 UID 登录
|
||||||
|
if (dataStore.loginType === "uid") {
|
||||||
|
await updateSpecialUserData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 获取登录状态
|
// 获取登录状态
|
||||||
const loginState = await getLoginState();
|
const loginState = await getLoginState();
|
||||||
// 登录正常
|
// 登录正常
|
||||||
@@ -113,6 +128,7 @@ const checkLoginStatus = async () => {
|
|||||||
// 若还有用户数据,则登录过期
|
// 若还有用户数据,则登录过期
|
||||||
else if (dataStore.userData.userId !== 0) {
|
else if (dataStore.userData.userId !== 0) {
|
||||||
dataStore.userLoginStatus = false;
|
dataStore.userLoginStatus = false;
|
||||||
|
dataStore.userData.userId = 0;
|
||||||
window.$message.warning("登录已过期,请重新登录", { duration: 2000 });
|
window.$message.warning("登录已过期,请重新登录", { duration: 2000 });
|
||||||
openUserLogin();
|
openUserLogin();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,25 +15,16 @@
|
|||||||
<div class="artist-item">
|
<div class="artist-item">
|
||||||
<!-- 封面 -->
|
<!-- 封面 -->
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<n-image
|
<s-image
|
||||||
:src="item.coverSize?.m || item.cover"
|
:src="item.coverSize?.m || item.cover"
|
||||||
|
default-src="/images/artist.jpg?assest"
|
||||||
class="cover-img"
|
class="cover-img"
|
||||||
preview-disabled
|
/>
|
||||||
lazy
|
|
||||||
@load="coverLoaded"
|
|
||||||
>
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="cover-loading">
|
|
||||||
<img src="/images/artist.jpg?assest" class="loading-img" alt="loading-img" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-image>
|
|
||||||
<!-- 封面背板 -->
|
<!-- 封面背板 -->
|
||||||
<n-image
|
<s-image
|
||||||
class="cover-shadow"
|
|
||||||
preview-disabled
|
|
||||||
lazy
|
|
||||||
:src="item.coverSize?.m || item.cover"
|
:src="item.coverSize?.m || item.cover"
|
||||||
|
default-src="/images/artist.jpg?assest"
|
||||||
|
class="cover-shadow"
|
||||||
/>
|
/>
|
||||||
<!-- 图标 -->
|
<!-- 图标 -->
|
||||||
<SvgIcon name="Artist" />
|
<SvgIcon name="Artist" />
|
||||||
@@ -78,7 +69,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ArtistType } from "@/types/main";
|
import type { ArtistType } from "@/types/main";
|
||||||
import { coverLoaded } from "@/utils/helper";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: ArtistType[];
|
data: ArtistType[];
|
||||||
@@ -124,15 +114,6 @@ const router = useRouter();
|
|||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: border-radius 0.3s;
|
transition: border-radius 0.3s;
|
||||||
:deep(img) {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
transition:
|
|
||||||
opacity 0.3s,
|
|
||||||
filter 0.3s,
|
|
||||||
transform 0.3s;
|
|
||||||
}
|
|
||||||
.cover-img {
|
.cover-img {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -145,6 +126,7 @@ const router = useRouter();
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
top: 20%;
|
top: 20%;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|||||||
@@ -10,32 +10,17 @@
|
|||||||
>
|
>
|
||||||
<!-- 封面 -->
|
<!-- 封面 -->
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<n-image
|
<s-image
|
||||||
|
:key="item.cover"
|
||||||
:src="
|
:src="
|
||||||
type === 'video' ? `${item.cover}?param=464y260` : item.coverSize?.m || item.cover
|
type === 'video' ? `${item.cover}?param=464y260` : item.coverSize?.m || item.cover
|
||||||
"
|
"
|
||||||
|
:default-src="
|
||||||
|
type !== 'video' ? '/images/album.jpg?assest' : '/images/video.jpg?assest'
|
||||||
|
"
|
||||||
class="cover-img"
|
class="cover-img"
|
||||||
preview-disabled
|
once
|
||||||
lazy
|
/>
|
||||||
@load="coverLoaded"
|
|
||||||
>
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="cover-loading">
|
|
||||||
<img
|
|
||||||
v-if="type !== 'video'"
|
|
||||||
src="/images/album.jpg?assest"
|
|
||||||
class="loading-img"
|
|
||||||
alt="loading-img"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
src="/images/video.jpg?assest"
|
|
||||||
class="loading-img"
|
|
||||||
alt="loading-img"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-image>
|
|
||||||
<template v-if="item.playCount">
|
<template v-if="item.playCount">
|
||||||
<!-- 遮罩 -->
|
<!-- 遮罩 -->
|
||||||
<div v-if="type !== 'album'" class="cover-mask" />
|
<div v-if="type !== 'album'" class="cover-mask" />
|
||||||
@@ -129,7 +114,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CoverType, SongType } from "@/types/main";
|
import type { CoverType, SongType } from "@/types/main";
|
||||||
import { albumDetail } from "@/api/album";
|
import { albumDetail } from "@/api/album";
|
||||||
import { coverLoaded, formatNumber } from "@/utils/helper";
|
import { formatNumber } from "@/utils/helper";
|
||||||
import { useMusicStore, useStatusStore } from "@/stores";
|
import { useMusicStore, useStatusStore } from "@/stores";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import { formatSongsList } from "@/utils/format";
|
import { formatSongsList } from "@/utils/format";
|
||||||
@@ -255,7 +240,7 @@ const getListData = async (id: number): Promise<SongType[]> => {
|
|||||||
:deep(img) {
|
:deep(img) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
opacity: 0;
|
// opacity: 0;
|
||||||
transition: opacity 0.35s ease-in-out;
|
transition: opacity 0.35s ease-in-out;
|
||||||
}
|
}
|
||||||
.cover-img {
|
.cover-img {
|
||||||
@@ -378,7 +363,6 @@ const getListData = async (id: number): Promise<SongType[]> => {
|
|||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(var(--primary), 0.12);
|
background-color: rgba(var(--primary), 0.12);
|
||||||
.cover {
|
.cover {
|
||||||
border-radius: 16px 16px 0 0;
|
|
||||||
.cover-img {
|
.cover-img {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
filter: brightness(0.8);
|
filter: brightness(0.8);
|
||||||
|
|||||||
@@ -1,321 +1,204 @@
|
|||||||
|
<!-- 歌曲列表 - 虚拟列表 -->
|
||||||
|
<!-- vue-virt-list https://github.com/keno-lee/vue-virt-list -->
|
||||||
<template>
|
<template>
|
||||||
<Transition name="fade" mode="out-in">
|
<Transition name="fade" mode="out-in">
|
||||||
<div
|
<div
|
||||||
v-if="data.length > 0"
|
v-if="!isEmpty(listData)"
|
||||||
ref="songListRef"
|
ref="songListRef"
|
||||||
:style="{ height: disableVirtualList ? undefined : '100%' }"
|
:class="[
|
||||||
:class="['song-list', { 'no-padding': hiddenPadding }]"
|
'song-list',
|
||||||
|
{
|
||||||
|
'hidden-scrollbar': hiddenScrollbar,
|
||||||
|
},
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<DynamicScroller
|
<Transition name="fade" mode="out-in">
|
||||||
ref="scrollerRef"
|
<VirtList
|
||||||
:items="listData"
|
ref="listRef"
|
||||||
:min-item-size="94"
|
:key="listKey"
|
||||||
:emitUpdate="true"
|
:list="listData"
|
||||||
:style="{ height: disableVirtualList ? undefined : `${songListShowHeight}px` }"
|
:minSize="94"
|
||||||
class="scroller"
|
:buffer="2"
|
||||||
@scroll="onScroll"
|
:offset="offset"
|
||||||
>
|
:style="{ height: height === 'auto' ? 'auto' : `${height || songListHeight}px` }"
|
||||||
<template #before>
|
itemKey="id"
|
||||||
<slot name="header" />
|
@scroll="onScroll"
|
||||||
<div class="song-item header" :style="{ margin }">
|
@toBottom="onToBottom"
|
||||||
<n-text class="num">#</n-text>
|
>
|
||||||
<n-dropdown
|
<!-- 悬浮顶栏 -->
|
||||||
v-if="!disabledSort"
|
<template #stickyHeader>
|
||||||
:options="sortMenuOptions"
|
<div class="list-header song-card">
|
||||||
trigger="click"
|
<n-text class="num">#</n-text>
|
||||||
placement="bottom-start"
|
<n-dropdown
|
||||||
@select="sortSelect"
|
v-if="!disabledSort"
|
||||||
>
|
:options="sortMenuOptions"
|
||||||
<div class="title has-sort">
|
trigger="click"
|
||||||
<n-text>标题</n-text>
|
placement="bottom-start"
|
||||||
<n-text v-if="statusStore.listSort !== 'default'" class="sort" depth="3">
|
@select="sortSelect"
|
||||||
{{ sortOptions[statusStore.listSort].name }}
|
>
|
||||||
</n-text>
|
<div class="title has-sort">
|
||||||
</div>
|
<n-text>标题</n-text>
|
||||||
</n-dropdown>
|
<n-text v-if="statusStore.listSort !== 'default'" class="sort" depth="3">
|
||||||
<n-text v-else class="title">标题</n-text>
|
{{ sortOptions[statusStore.listSort].name }}
|
||||||
<n-text v-if="type !== 'radio' && !hiddenAlbum" class="album">专辑</n-text>
|
</n-text>
|
||||||
<n-text v-if="type !== 'radio'" class="actions">操作</n-text>
|
|
||||||
<n-text v-if="type === 'radio'" class="meta date">更新日期</n-text>
|
|
||||||
<n-text v-if="type === 'radio'" class="meta">播放量</n-text>
|
|
||||||
<n-text class="meta">时长</n-text>
|
|
||||||
<n-text v-if="data?.[0].size && !hiddenSize" class="meta size">大小</n-text>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-slot="{ item, index, active }">
|
|
||||||
<DynamicScrollerItem
|
|
||||||
:item="item"
|
|
||||||
:active="active"
|
|
||||||
:data-index="index"
|
|
||||||
:key="item.id"
|
|
||||||
class="song-item-wrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
:class="['song-item', { play: musicStore.playSong.id === item.id }]"
|
|
||||||
:style="{ margin }"
|
|
||||||
@dblclick.stop="playSong(item)"
|
|
||||||
@contextmenu="songListMenuRef?.openDropdown($event, data, item, index, type)"
|
|
||||||
>
|
|
||||||
<!-- 序号 -->
|
|
||||||
<div class="num" @dblclick.stop>
|
|
||||||
<n-text v-if="musicStore.playSong.id !== item.id" depth="3">
|
|
||||||
{{ index + 1 }}
|
|
||||||
</n-text>
|
|
||||||
<SvgIcon v-else :size="22" name="Music" />
|
|
||||||
<!-- 播放暂停 -->
|
|
||||||
<SvgIcon
|
|
||||||
:size="28"
|
|
||||||
:name="statusStore.playStatus ? 'Pause' : 'Play'"
|
|
||||||
class="status"
|
|
||||||
@click="player.playOrPause()"
|
|
||||||
/>
|
|
||||||
<!-- 播放 -->
|
|
||||||
<SvgIcon
|
|
||||||
:size="28"
|
|
||||||
name="Play"
|
|
||||||
class="play"
|
|
||||||
@click="player.addNextSong(item, true)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- 标题 -->
|
|
||||||
<div class="title">
|
|
||||||
<!-- 封面 -->
|
|
||||||
<n-image
|
|
||||||
v-if="!hiddenCover"
|
|
||||||
:key="item.cover"
|
|
||||||
:src="item.path ? item.cover : item.coverSize?.s || item.cover"
|
|
||||||
fallback-src="/images/song.jpg?assest"
|
|
||||||
class="cover"
|
|
||||||
preview-disabled
|
|
||||||
v-visible="(show: boolean) => localCover(show, item?.path, index)"
|
|
||||||
@load="coverLoaded"
|
|
||||||
>
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="cover-loading">
|
|
||||||
<img src="/images/song.jpg?assest" class="loading-img" alt="loading-img" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-image>
|
|
||||||
<!-- 信息 -->
|
|
||||||
<div class="info">
|
|
||||||
<!-- 名称 -->
|
|
||||||
<div class="name">
|
|
||||||
<n-ellipsis
|
|
||||||
:line-clamp="1"
|
|
||||||
:tooltip="{
|
|
||||||
placement: 'top',
|
|
||||||
width: 'trigger',
|
|
||||||
}"
|
|
||||||
class="name-text"
|
|
||||||
>
|
|
||||||
{{ item?.name || "未知曲目" }}
|
|
||||||
</n-ellipsis>
|
|
||||||
<!-- 音质 -->
|
|
||||||
<n-tag
|
|
||||||
v-if="item?.path && item?.quality"
|
|
||||||
:bordered="false"
|
|
||||||
:type="item.quality === 'Hi-Res' ? 'warning' : 'info'"
|
|
||||||
class="quality"
|
|
||||||
round
|
|
||||||
>
|
|
||||||
{{ item.quality }}
|
|
||||||
</n-tag>
|
|
||||||
<!-- 特权 -->
|
|
||||||
<n-tag v-if="item.originCoverType === 1" :bordered="false" type="primary" round>
|
|
||||||
原
|
|
||||||
</n-tag>
|
|
||||||
<n-tag v-if="item.free === 1" :bordered="false" type="error" round> VIP </n-tag>
|
|
||||||
<n-tag v-if="item.free === 4" :bordered="false" type="error" round> EP </n-tag>
|
|
||||||
<!-- 云盘 -->
|
|
||||||
<n-tag v-if="item?.pc" :bordered="false" class="cloud" type="info" round>
|
|
||||||
<template #icon>
|
|
||||||
<SvgIcon name="Cloud" />
|
|
||||||
</template>
|
|
||||||
</n-tag>
|
|
||||||
<!-- MV -->
|
|
||||||
<n-tag
|
|
||||||
v-if="item?.mv"
|
|
||||||
:bordered="false"
|
|
||||||
class="mv"
|
|
||||||
type="warning"
|
|
||||||
round
|
|
||||||
@click.stop="
|
|
||||||
router.push({
|
|
||||||
name: 'video',
|
|
||||||
query: { id: item.mv },
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
MV
|
|
||||||
</n-tag>
|
|
||||||
</div>
|
|
||||||
<!-- 歌手 -->
|
|
||||||
<div v-if="Array.isArray(item.artists)" class="artists text-hidden">
|
|
||||||
<n-text
|
|
||||||
v-for="ar in item.artists"
|
|
||||||
:key="ar.id"
|
|
||||||
class="ar"
|
|
||||||
@click="openJumpArtist(item.artists)"
|
|
||||||
>
|
|
||||||
{{ ar.name }}
|
|
||||||
</n-text>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="type === 'radio'" class="artists">
|
|
||||||
<n-text class="ar"> 电台节目 </n-text>
|
|
||||||
</div>
|
|
||||||
<div v-else class="artists text-hidden" @click="openJumpArtist(item.artists)">
|
|
||||||
<n-text class="ar"> {{ item.artists || "未知艺术家" }} </n-text>
|
|
||||||
</div>
|
|
||||||
<!-- 别名 -->
|
|
||||||
<n-text v-if="item.alia" class="alia" depth="3">{{ item.alia }}</n-text>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</n-dropdown>
|
||||||
<!-- 专辑 -->
|
<n-text v-else class="title">标题</n-text>
|
||||||
<div v-if="type !== 'radio' && !hiddenAlbum" class="album text-hidden">
|
<n-text v-if="type !== 'radio' && !hiddenAlbum" class="album">专辑</n-text>
|
||||||
<n-text
|
<n-text v-if="type !== 'radio'" class="actions">操作</n-text>
|
||||||
v-if="isObject(item.album)"
|
<n-text v-if="type === 'radio'" class="meta date">更新日期</n-text>
|
||||||
class="album-text"
|
<n-text v-if="type === 'radio'" class="meta">播放量</n-text>
|
||||||
@click="
|
<n-text class="meta">时长</n-text>
|
||||||
router.push({
|
<n-text v-if="data?.[0].size && !hiddenSize" class="meta size">大小</n-text>
|
||||||
name: 'album',
|
|
||||||
query: { id: item.album?.id },
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ item.album?.name || "未知专辑" }}
|
|
||||||
</n-text>
|
|
||||||
<n-text v-else class="album-text">
|
|
||||||
{{ item.album || "未知专辑" }}
|
|
||||||
</n-text>
|
|
||||||
</div>
|
|
||||||
<!-- 操作 -->
|
|
||||||
<div v-if="type !== 'radio'" class="actions" @click.stop @dblclick.stop>
|
|
||||||
<!-- 喜欢歌曲 -->
|
|
||||||
<SvgIcon
|
|
||||||
:name="dataStore.isLikeSong(item.id) ? 'Favorite' : 'FavoriteBorder'"
|
|
||||||
:size="20"
|
|
||||||
@click.stop="toLikeSong(item, !dataStore.isLikeSong(item.id))"
|
|
||||||
@delclick.stop
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- 更新日期 -->
|
|
||||||
<n-text v-if="type === 'radio'" class="meta date" depth="3">
|
|
||||||
{{ formatTimestamp(item.updateTime) }}
|
|
||||||
</n-text>
|
|
||||||
<!-- 播放量 -->
|
|
||||||
<n-text v-if="type === 'radio'" class="meta" depth="3">
|
|
||||||
{{ formatNumber(item.playCount) }}
|
|
||||||
</n-text>
|
|
||||||
<!-- 时长 -->
|
|
||||||
<n-text class="meta" depth="3">{{ msToTime(item.duration) }}</n-text>
|
|
||||||
<!-- 大小 -->
|
|
||||||
<n-text v-if="item.path && item.size && !hiddenSize" class="meta size" depth="3">
|
|
||||||
{{ item.size }}M
|
|
||||||
</n-text>
|
|
||||||
</div>
|
</div>
|
||||||
</DynamicScrollerItem>
|
</template>
|
||||||
</template>
|
<!-- 主内容 -->
|
||||||
<template #after>
|
<template #default="{ itemData, index }">
|
||||||
<div class="list-after">
|
<SongCard
|
||||||
<n-flex v-if="loadMore && loading">
|
:song="itemData"
|
||||||
<n-spin size="small" />
|
:index="index"
|
||||||
<n-text>{{ loadingText || "努力加载中" }}</n-text>
|
:hiddenCover="hiddenCover"
|
||||||
</n-flex>
|
:hiddenAlbum="hiddenAlbum"
|
||||||
<n-divider v-else dashed> 没有更多啦 ~ </n-divider>
|
:hiddenSize="hiddenSize"
|
||||||
</div>
|
@dblclick.stop="
|
||||||
</template>
|
doubleClickAction === 'add'
|
||||||
</DynamicScroller>
|
? player.addNextSong(itemData, true)
|
||||||
|
: player.updatePlayList(listData, itemData, playListId)
|
||||||
|
"
|
||||||
|
@contextmenu.stop="
|
||||||
|
songListMenuRef?.openDropdown(
|
||||||
|
$event,
|
||||||
|
listData,
|
||||||
|
itemData,
|
||||||
|
index,
|
||||||
|
type,
|
||||||
|
playListId,
|
||||||
|
isDailyRecommend,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- 加载更多 -->
|
||||||
|
<template #footer>
|
||||||
|
<div class="load-more">
|
||||||
|
<n-flex v-if="loadMore && loading">
|
||||||
|
<n-spin size="small" />
|
||||||
|
<n-text>{{ loadingText || "努力加载中" }}</n-text>
|
||||||
|
</n-flex>
|
||||||
|
<n-divider v-else dashed> 没有更多啦 ~ </n-divider>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VirtList>
|
||||||
|
</Transition>
|
||||||
<!-- 右键菜单 -->
|
<!-- 右键菜单 -->
|
||||||
<SongListMenu ref="songListMenuRef" />
|
<SongListMenu ref="songListMenuRef" @removeSong="removeSong" />
|
||||||
<!-- 列表操作 -->
|
<!-- 列表操作 -->
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="fade" mode="out-in">
|
<Transition name="fade" mode="out-in">
|
||||||
<n-float-button-group v-if="floatToolShow && !disableVirtualList" class="list-button">
|
<n-float-button-group v-if="floatToolShow" class="list-menu">
|
||||||
<Transition name="fade" mode="out-in">
|
<Transition name="fade" mode="out-in">
|
||||||
<n-float-button v-if="songListScrollTop > 100" width="42" @click="scrollTo(0)">
|
<n-float-button v-if="scrollTop > 100" width="42" @click="listRef?.scrollToTop()">
|
||||||
<SvgIcon :size="22" name="Up" />
|
<SvgIcon :size="22" name="Up" />
|
||||||
</n-float-button>
|
</n-float-button>
|
||||||
</Transition>
|
</Transition>
|
||||||
<n-float-button v-if="hasPlaySong >= 0" width="42" @click="scrollTo(hasPlaySong)">
|
<n-float-button
|
||||||
|
v-if="hasPlaySong >= 0"
|
||||||
|
width="42"
|
||||||
|
@click="listRef?.scrollToIndex(hasPlaySong)"
|
||||||
|
>
|
||||||
<SvgIcon :size="22" name="Location" />
|
<SvgIcon :size="22" name="Location" />
|
||||||
</n-float-button>
|
</n-float-button>
|
||||||
</n-float-button-group>
|
</n-float-button-group>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
<!-- 加载动画 -->
|
<!-- 列表加载 - 骨架屏 -->
|
||||||
<div v-else-if="loading" class="song-list loading">
|
<div v-else-if="loading" class="song-list loading">
|
||||||
<n-skeleton :repeat="10" text />
|
<n-skeleton :repeat="10" text />
|
||||||
</div>
|
</div>
|
||||||
<!-- 空列表 -->
|
<!-- 空列表 -->
|
||||||
<n-empty v-else description="空空如也,怎么一首歌都没有啊" size="large" class="song-list" />
|
<n-empty v-else description="列表光秃秃的,啥都没有哦" size="large" class="song-list empty" />
|
||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SongType, SortType } from "@/types/main";
|
|
||||||
import type { DropdownOption } from "naive-ui";
|
import type { DropdownOption } from "naive-ui";
|
||||||
import { useStatusStore, useMusicStore, useDataStore } from "@/stores";
|
import type { SongType, SortType } from "@/types/main";
|
||||||
import { isObject, entries, cloneDeep } from "lodash-es";
|
import { useMusicStore, useStatusStore } from "@/stores";
|
||||||
import { openJumpArtist } from "@/utils/modal";
|
import { VirtList } from "vue-virt-list";
|
||||||
import { formatNumber, isElectron, sortOptions } from "@/utils/helper";
|
import { entries, isEmpty } from "lodash-es";
|
||||||
import { toLikeSong } from "@/utils/auth";
|
import { sortOptions } from "@/utils/meta";
|
||||||
import { formatTimestamp, msToTime } from "@/utils/time";
|
import { renderIcon } from "@/utils/helper";
|
||||||
import SongListMenu from "@/components/Menu/SongListMenu.vue";
|
import SongListMenu from "@/components/Menu/SongListMenu.vue";
|
||||||
import player from "@/utils/player";
|
import player from "@/utils/player";
|
||||||
import blob from "@/utils/blob";
|
|
||||||
|
|
||||||
const router = useRouter();
|
const props = withDefaults(
|
||||||
const dataStore = useDataStore();
|
defineProps<{
|
||||||
const musicStore = useMusicStore();
|
// 列表数据
|
||||||
const statusStore = useStatusStore();
|
data: SongType[];
|
||||||
|
// 列表类型
|
||||||
interface Props {
|
type?: "song" | "radio";
|
||||||
data: SongType[];
|
// 列表高度
|
||||||
type?: "song" | "radio";
|
height?: number | "auto"; // px
|
||||||
loadMore?: boolean;
|
// 是否加载
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
loadingText?: string;
|
// 加载更多
|
||||||
hiddenAlbum?: boolean;
|
loadMore?: boolean;
|
||||||
hiddenCover?: boolean;
|
loadingText?: string;
|
||||||
hiddenPadding?: boolean;
|
// 隐藏元素
|
||||||
hiddenSize?: boolean;
|
hiddenAlbum?: boolean;
|
||||||
margin?: string;
|
hiddenCover?: boolean;
|
||||||
height?: number;
|
hiddenSize?: boolean;
|
||||||
// 禁用排序
|
// 隐藏滚动条
|
||||||
disabledSort?: boolean;
|
hiddenScrollbar?: boolean;
|
||||||
// 播放歌单 ID
|
// 禁用排序
|
||||||
playListId?: number;
|
disabledSort?: boolean;
|
||||||
// 禁用虚拟列表
|
// 播放歌单 ID
|
||||||
disableVirtualList?: boolean;
|
playListId?: number;
|
||||||
}
|
// 是否为每日推荐
|
||||||
|
isDailyRecommend?: boolean;
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
// 双击播放操作
|
||||||
type: "song",
|
doubleClickAction?: "all" | "add";
|
||||||
});
|
}>(),
|
||||||
|
{
|
||||||
|
type: "song",
|
||||||
|
loadingText: "努力加载中...",
|
||||||
|
playListId: 0,
|
||||||
|
isDailyRecommend: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
// 触底
|
// 触底
|
||||||
reachBottom: [];
|
reachBottom: [e: Event];
|
||||||
// 滚动
|
// 滚动
|
||||||
scroll: [e: Event];
|
scroll: [e: Event];
|
||||||
|
// 删除歌曲
|
||||||
|
removeSong: [id: number[]];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const musicStore = useMusicStore();
|
||||||
|
const statusStore = useStatusStore();
|
||||||
|
|
||||||
|
// 列表状态
|
||||||
|
const offset = ref<number>(0);
|
||||||
|
const scrollTop = ref<number>(0);
|
||||||
|
|
||||||
|
// 列表元素
|
||||||
|
const listRef = ref<InstanceType<typeof VirtList> | null>(null);
|
||||||
|
const songListRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
// 悬浮工具
|
||||||
|
const floatToolShow = ref<boolean>(true);
|
||||||
|
|
||||||
// 右键菜单
|
// 右键菜单
|
||||||
const songListMenuRef = ref<InstanceType<typeof SongListMenu> | null>(null);
|
const songListMenuRef = ref<InstanceType<typeof SongListMenu> | null>(null);
|
||||||
|
|
||||||
// 列表元素
|
|
||||||
const scrollerRef = ref<any | null>(null);
|
|
||||||
const songListRef = ref<HTMLElement | null>(null);
|
|
||||||
const songListScrollTop = ref<number>(0);
|
|
||||||
|
|
||||||
// 悬浮工具
|
|
||||||
const floatToolShow = ref<boolean>(false);
|
|
||||||
|
|
||||||
// 列表数据
|
// 列表数据
|
||||||
const listData = computed<SongType[]>(() => {
|
const listData = computed<SongType[]>(() => {
|
||||||
const data = cloneDeep(props.data);
|
if (props.disabledSort) return props.data;
|
||||||
if (props.disabledSort) return data;
|
// 创建副本用于排序(避免修改原数组)
|
||||||
|
const data = [...props.data];
|
||||||
// 排序
|
// 排序
|
||||||
switch (statusStore.listSort) {
|
switch (statusStore.listSort) {
|
||||||
case "titleAZ":
|
case "titleAZ":
|
||||||
@@ -347,27 +230,46 @@ const listData = computed<SongType[]>(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 列表元素高度
|
// 虚拟列表 key
|
||||||
const { height: songListHeight, stop: stopHeight } = useElementSize(songListRef);
|
const listKey = computed(() => {
|
||||||
|
// 每日推荐
|
||||||
// 应该展示的列表高度
|
if (props.isDailyRecommend) {
|
||||||
const songListShowHeight = computed(() => props.height ?? songListHeight.value);
|
return musicStore.dailySongsData.timestamp || 0;
|
||||||
|
}
|
||||||
|
// 其他列表长度(检测增删操作)
|
||||||
|
return listData.value?.length || 0;
|
||||||
|
});
|
||||||
|
|
||||||
// 列表是否具有播放歌曲
|
// 列表是否具有播放歌曲
|
||||||
const hasPlaySong = computed(() => {
|
const hasPlaySong = computed(() => {
|
||||||
return listData.value.findIndex((item) => item.id === musicStore.playSong.id);
|
return listData.value.findIndex((item) => item.id === musicStore.playSong.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 列表元素高度
|
||||||
|
const { height: songListHeight, stop: stopCalcHeight } = useElementSize(songListRef);
|
||||||
|
|
||||||
// 列表排序菜单
|
// 列表排序菜单
|
||||||
const sortMenuOptions = computed<DropdownOption[]>(() =>
|
const sortMenuOptions = computed<DropdownOption[]>(() =>
|
||||||
entries(sortOptions).map(([key, { name, show, icon }]) => ({
|
entries(sortOptions).map(([key, { name, show, icon }]) => ({
|
||||||
key,
|
key,
|
||||||
label: name,
|
label: name,
|
||||||
show: show === "all" ? true : show === props.type ? true : false,
|
show: show === "all" ? true : show === props.type ? true : false,
|
||||||
icon,
|
icon: renderIcon(icon),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 列表滚动
|
||||||
|
const onScroll = (e: Event) => {
|
||||||
|
emit("scroll", e);
|
||||||
|
scrollTop.value = (e.target as HTMLElement).scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 列表触底
|
||||||
|
const onToBottom = (e: Event) => {
|
||||||
|
if (props.loading) return;
|
||||||
|
emit("reachBottom", e);
|
||||||
|
};
|
||||||
|
|
||||||
// 排序更改
|
// 排序更改
|
||||||
const sortSelect = (key: SortType) => {
|
const sortSelect = (key: SortType) => {
|
||||||
statusStore.listSort = key;
|
statusStore.listSort = key;
|
||||||
@@ -379,107 +281,55 @@ const sortSelect = (key: SortType) => {
|
|||||||
scrobble: false,
|
scrobble: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 滚动到顶部
|
||||||
|
listRef.value?.scrollToIndex(hasPlaySong.value || 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 滚动至播放歌曲
|
// 删除指定索引
|
||||||
const scrollTo = (index: number) => {
|
const removeSong = (id: number[]) => emit("removeSong", id);
|
||||||
if (index === 0) songListScrollTop.value = 0;
|
|
||||||
scrollerRef.value?.scrollToItem(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 封面加载完成
|
// keep-alive 处理
|
||||||
const coverLoaded = (e: Event) => {
|
onBeforeRouteLeave(() => {
|
||||||
const target = e.target as HTMLElement | null;
|
offset.value = listRef.value?.getOffset() || 0;
|
||||||
if (target && target.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
target.style.opacity = "1";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 加载本地歌曲封面
|
|
||||||
const localCover = async (show: boolean, path: string, index: number) => {
|
|
||||||
if (!isElectron || !show || !path) return;
|
|
||||||
if (listData.value[index].cover || listData.value[index].cover === "/images/song.jpg?assest")
|
|
||||||
return;
|
|
||||||
// 获取封面
|
|
||||||
const coverData = await window.electron.ipcRenderer.invoke("get-music-cover", path);
|
|
||||||
if (!coverData) return;
|
|
||||||
const { data, format } = coverData;
|
|
||||||
const blobURL = blob.createBlobURL(data, format, path);
|
|
||||||
if (blobURL) listData.value[index].cover = blobURL;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 播放列表歌曲
|
|
||||||
const playSong = (song: SongType) => {
|
|
||||||
console.log(song);
|
|
||||||
// 更改播放列表
|
|
||||||
player.updatePlayList(listData.value, song, props.playListId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 列表滚动
|
|
||||||
const onScroll = (e: Event) => {
|
|
||||||
const target = e.target as HTMLElement | null;
|
|
||||||
// 获取高度
|
|
||||||
if (target && target.scrollTop) {
|
|
||||||
songListScrollTop.value = target.scrollTop;
|
|
||||||
}
|
|
||||||
// 是否触底
|
|
||||||
const offset: number = 300;
|
|
||||||
if (target && target.scrollTop + target.clientHeight >= target.scrollHeight - offset) {
|
|
||||||
if (props.loadMore && !props.loading) emit("reachBottom");
|
|
||||||
}
|
|
||||||
// 滚动事件
|
|
||||||
emit("scroll", e);
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeactivated(() => {
|
|
||||||
floatToolShow.value = false;
|
floatToolShow.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
floatToolShow.value = true;
|
floatToolShow.value = true;
|
||||||
if (props.disableVirtualList) stopHeight();
|
if (props.height === "auto") stopCalcHeight();
|
||||||
|
if (offset.value > 0) listRef.value?.scrollToOffset(offset.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopCalcHeight();
|
||||||
|
floatToolShow.value = false;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.song-list {
|
.song-list {
|
||||||
.scroller {
|
height: 100%;
|
||||||
padding-bottom: 14px;
|
border-radius: 12px 0 0 12px;
|
||||||
transition: height 0.3s;
|
overflow: hidden;
|
||||||
-webkit-overflow-scrolling: touch;
|
.song-card {
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(var(--primary), 0.28);
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.song-item-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 94px;
|
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
|
// padding-right: 4px;
|
||||||
}
|
}
|
||||||
.song-item {
|
// 悬浮顶栏
|
||||||
|
.list-header {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
// min-height: 70px;
|
|
||||||
height: 100%;
|
|
||||||
flex: 1;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border: 2px solid rgba(var(--primary), 0.12);
|
// margin-right: 4px;
|
||||||
background-color: var(--surface-container-hex);
|
border: 1px solid transparent;
|
||||||
transition:
|
background-color: var(--background-hex);
|
||||||
background-color 0.3s var(--n-bezier),
|
.n-text {
|
||||||
border-color 0.3s var(--n-bezier);
|
opacity: 0.6;
|
||||||
cursor: pointer;
|
}
|
||||||
.num {
|
.num {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -489,112 +339,14 @@ onActivated(() => {
|
|||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
.n-icon {
|
|
||||||
transition:
|
|
||||||
opacity 0.3s,
|
|
||||||
transform 0.3s;
|
|
||||||
}
|
|
||||||
.status,
|
|
||||||
.play {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.8);
|
|
||||||
&:active {
|
|
||||||
opacity: 0.6 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 20px 4px 0;
|
padding: 4px 20px 4px 0;
|
||||||
.cover {
|
cursor: pointer;
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
min-width: 50px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-right: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
:deep(img) {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.35s ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.name {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 16px;
|
|
||||||
:deep(.name-text) {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
.n-tag {
|
|
||||||
--n-height: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-right: 6px;
|
|
||||||
pointer-events: none;
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.quality {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.cloud {
|
|
||||||
padding: 0 10px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
:deep(.n-tag__icon) {
|
|
||||||
margin-right: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.n-icon {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--n-text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.mv {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.artists {
|
|
||||||
margin-top: 2px;
|
|
||||||
font-size: 13px;
|
|
||||||
.ar {
|
|
||||||
display: inline-flex;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: pointer;
|
|
||||||
&::after {
|
|
||||||
content: "/";
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
&::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alia {
|
|
||||||
margin-top: 2px;
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sort {
|
.sort {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
&::after {
|
&::after {
|
||||||
@@ -604,37 +356,38 @@ onActivated(() => {
|
|||||||
content: "( ";
|
content: "( ";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.has-sort {
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
top: 0;
|
||||||
|
left: -8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgba(var(--primary), 0.08);
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
&::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.album {
|
.album {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
line-clamp: 2;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
&:hover {
|
|
||||||
.album-text {
|
|
||||||
color: var(--primary-hex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
.n-icon {
|
|
||||||
transition: transform 0.3s;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
transform: scale(1.15);
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.meta {
|
.meta {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
font-size: 13px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
&.size {
|
&.size {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
@@ -643,74 +396,45 @@ onActivated(() => {
|
|||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.play {
|
}
|
||||||
border-color: rgba(var(--primary), 0.58);
|
// 滚动条
|
||||||
background-color: rgba(var(--primary), 0.28);
|
.virt-list__client {
|
||||||
}
|
transition:
|
||||||
&.header {
|
height 0.3s,
|
||||||
border: none;
|
width 0.3s,
|
||||||
|
opacity 0.3s;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
.n-text {
|
}
|
||||||
opacity: 0.6;
|
&::-webkit-scrollbar-track {
|
||||||
}
|
background: transparent;
|
||||||
.title {
|
}
|
||||||
position: relative;
|
&::-webkit-scrollbar-thumb {
|
||||||
padding: 0 20px 0 0;
|
background-color: rgba(var(--primary), 0.28);
|
||||||
&.has-sort {
|
border-radius: 12px;
|
||||||
&::after {
|
}
|
||||||
content: "";
|
}
|
||||||
position: absolute;
|
&.hidden-scrollbar {
|
||||||
opacity: 0;
|
.list-header {
|
||||||
top: 0;
|
padding: 8px 12px;
|
||||||
left: -8px;
|
}
|
||||||
width: 100%;
|
.song-card {
|
||||||
height: 100%;
|
padding-right: 0;
|
||||||
border-radius: 8px;
|
}
|
||||||
background-color: rgba(var(--primary), 0.08);
|
.virt-list__client {
|
||||||
transition: opacity 0.3s;
|
&::-webkit-scrollbar {
|
||||||
}
|
display: none;
|
||||||
&:hover {
|
|
||||||
&::after {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
:deep(.vue-recycle-scroller__item-view) {
|
// 加载更多
|
||||||
&.hover {
|
.load-more {
|
||||||
.song-item {
|
|
||||||
border-color: rgba(var(--primary), 0.58);
|
|
||||||
.num {
|
|
||||||
.n-text,
|
|
||||||
.n-icon {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.play {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.play {
|
|
||||||
.num {
|
|
||||||
.play {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.list-after {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 20px 0;
|
margin: 20px 0 40px;
|
||||||
.n-spin-body {
|
.n-spin-body {
|
||||||
--n-size: 20px;
|
--n-size: 20px;
|
||||||
}
|
}
|
||||||
@@ -720,14 +444,21 @@ onActivated(() => {
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.no-padding {
|
// 加载
|
||||||
margin: 0 -24px;
|
&.loading {
|
||||||
.song-item {
|
margin-top: 20px;
|
||||||
margin: 0px 21px 0px 26px;
|
:deep(.n-skeleton) {
|
||||||
|
height: 72px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 空列表
|
||||||
|
&.empty {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.list-button {
|
.list-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
bottom: 120px;
|
bottom: 120px;
|
||||||
@@ -736,15 +467,4 @@ onActivated(() => {
|
|||||||
border: 1px solid rgba(var(--primary), 0.28);
|
border: 1px solid rgba(var(--primary), 0.28);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.loading {
|
|
||||||
margin-top: 20px !important;
|
|
||||||
:deep(.n-skeleton) {
|
|
||||||
height: 72px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.n-empty {
|
|
||||||
margin-top: 60px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||