Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dab799939 | ||
|
|
1ddbe6f24e | ||
|
|
4d5bcba6c7 | ||
|
|
f833306b60 | ||
|
|
4d92ed9963 | ||
|
|
a22285156a | ||
|
|
d1029f16d6 | ||
|
|
4908555635 | ||
|
|
750cf7a484 | ||
|
|
a334743f6f | ||
|
|
14747cac10 | ||
|
|
cc239aeaba | ||
|
|
eeda296589 | ||
|
|
edb7ea201c | ||
|
|
17d20fa299 | ||
|
|
f8d421c9b1 | ||
|
|
dfdf02a17f | ||
|
|
abdb2bcd50 |
@@ -1,13 +0,0 @@
|
|||||||
snapshot*
|
|
||||||
dist
|
|
||||||
lib
|
|
||||||
es
|
|
||||||
esm
|
|
||||||
node_modules
|
|
||||||
static
|
|
||||||
cypress
|
|
||||||
script/test/cypress
|
|
||||||
_site
|
|
||||||
temp*
|
|
||||||
static/
|
|
||||||
!.prettierrc.js
|
|
||||||
140
.eslintrc
@@ -1,140 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": [
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"eslint-config-airbnb-base",
|
|
||||||
"@vue/typescript/recommended",
|
|
||||||
"plugin:vue/vue3-recommended",
|
|
||||||
"plugin:vue-scoped-css/base",
|
|
||||||
"plugin:prettier/recommended"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true,
|
|
||||||
"jest": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"defineProps": "readonly",
|
|
||||||
"defineEmits": "readonly"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"vue",
|
|
||||||
"@typescript-eslint",
|
|
||||||
"simple-import-sort"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"sourceType": "module",
|
|
||||||
"allowImportExportEverywhere": true,
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"import/extensions": [
|
|
||||||
".js",
|
|
||||||
".jsx",
|
|
||||||
".ts",
|
|
||||||
".tsx"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"no-nested-ternary": "off",
|
|
||||||
"no-console": "off",
|
|
||||||
"no-continue": "off",
|
|
||||||
"no-restricted-syntax": "off",
|
|
||||||
"no-return-assign": "off",
|
|
||||||
"no-unused-expressions": "off",
|
|
||||||
"no-return-await": "off",
|
|
||||||
"no-plusplus": "off",
|
|
||||||
"no-param-reassign": "off",
|
|
||||||
"no-shadow": "off",
|
|
||||||
"guard-for-in": "off",
|
|
||||||
"import/extensions": "off",
|
|
||||||
"import/no-unresolved": "off",
|
|
||||||
"import/no-extraneous-dependencies": "off",
|
|
||||||
"import/prefer-default-export": "off",
|
|
||||||
"import/first": "off", // https://github.com/vuejs/vue-eslint-parser/issues/58
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
||||||
"vue/first-attribute-linebreak": 0,
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"argsIgnorePattern": "^_",
|
|
||||||
"varsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"argsIgnorePattern": "^_",
|
|
||||||
"varsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-use-before-define": "off",
|
|
||||||
"@typescript-eslint/no-use-before-define": "off",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
|
||||||
"@typescript-eslint/ban-types": "off",
|
|
||||||
"class-methods-use-this": "off", // 因为AxiosCancel必须实例化而能静态化所以加的规则,如果有办法解决可以取消
|
|
||||||
"simple-import-sort/imports": "error",
|
|
||||||
"simple-import-sort/exports": "error"
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"*.vue"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"vue/component-name-in-template-casing": [
|
|
||||||
2,
|
|
||||||
"kebab-case"
|
|
||||||
],
|
|
||||||
"vue/require-default-prop": 0,
|
|
||||||
"vue/multi-word-component-names": 0,
|
|
||||||
"vue/no-reserved-props": 0,
|
|
||||||
"vue/no-v-html": 0,
|
|
||||||
"vue-scoped-css/enforce-style-type": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"allows": [
|
|
||||||
"scoped"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"*.ts",
|
|
||||||
"*.tsx"
|
|
||||||
], // https://github.com/typescript-eslint eslint-recommended
|
|
||||||
"rules": {
|
|
||||||
"constructor-super": "off", // ts(2335) & ts(2377)
|
|
||||||
"getter-return": "off", // ts(2378)
|
|
||||||
"no-const-assign": "off", // ts(2588)
|
|
||||||
"no-dupe-args": "off", // ts(2300)
|
|
||||||
"no-dupe-class-members": "off", // ts(2393) & ts(2300)
|
|
||||||
"no-dupe-keys": "off", // ts(1117)
|
|
||||||
"no-func-assign": "off", // ts(2539)
|
|
||||||
"no-import-assign": "off", // ts(2539) & ts(2540)
|
|
||||||
"no-new-symbol": "off", // ts(2588)
|
|
||||||
"no-obj-calls": "off", // ts(2349)
|
|
||||||
"no-redeclare": "off", // ts(2451)
|
|
||||||
"no-setter-return": "off", // ts(2408)
|
|
||||||
"no-this-before-super": "off", // ts(2376)
|
|
||||||
"no-undef": "off", // ts(2304)
|
|
||||||
"no-unreachable": "off", // ts(7027)
|
|
||||||
"no-unsafe-negation": "off", // ts(2365) & ts(2360) & ts(2358)
|
|
||||||
"no-var": "error", // ts transpiles let/const to var, so no need for vars any more
|
|
||||||
"prefer-const": "error", // ts provides better types with const
|
|
||||||
"prefer-rest-params": "error", // ts provides better types with rest args over arguments
|
|
||||||
"prefer-spread": "error", // ts transpiles spread to apply, so no need for manual apply
|
|
||||||
"valid-typeof": "off", // ts(2367)
|
|
||||||
"consistent-return": "off",
|
|
||||||
"no-promise-executor-return": "off",
|
|
||||||
"prefer-promise-reject-errors": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
87
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
name: Build and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
# MacOS Build
|
||||||
|
- name: Build MacOS
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
export ELECTRON_BUILDER_EXTRA_ARGS="--universal"
|
||||||
|
npm run build:mac
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||||
|
DEBUG: electron-builder
|
||||||
|
|
||||||
|
# Windows Build
|
||||||
|
- name: Build Windows
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: npm run build:win
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Linux Build
|
||||||
|
- name: Build Linux
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
npm run build:linux
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Get version from tag
|
||||||
|
- name: Get version from tag
|
||||||
|
id: get_version
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# Read release notes
|
||||||
|
- name: Read release notes
|
||||||
|
id: release_notes
|
||||||
|
run: |
|
||||||
|
NOTES=$(awk "/## \[v${{ env.VERSION }}\]/{p=1;print;next} /## \[v/{p=0}p" CHANGELOG.md)
|
||||||
|
echo "NOTES<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$NOTES" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# Upload artifacts
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
dist/*.dmg
|
||||||
|
dist/*.exe
|
||||||
|
dist/*.deb
|
||||||
|
dist/*.AppImage
|
||||||
|
dist/latest*.yml
|
||||||
|
dist/*.blockmap
|
||||||
|
body: ${{ env.NOTES }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
4
.gitignore
vendored
@@ -17,4 +17,6 @@ dist.zip
|
|||||||
|
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
|
out
|
||||||
2
.npmrc
@@ -1,2 +0,0 @@
|
|||||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
|
||||||
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
// 一行最多 120 字符..
|
|
||||||
printWidth: 120,
|
|
||||||
// 使用 2 个空格缩进
|
|
||||||
tabWidth: 2,
|
|
||||||
// 不使用缩进符,而使用空格
|
|
||||||
useTabs: false,
|
|
||||||
// 行尾需要有分号
|
|
||||||
semi: true,
|
|
||||||
// 使用单引号
|
|
||||||
singleQuote: true,
|
|
||||||
// 对象的 key 仅在必要时用引号
|
|
||||||
quoteProps: 'as-needed',
|
|
||||||
// jsx 不使用单引号,而使用双引号
|
|
||||||
jsxSingleQuote: false,
|
|
||||||
// 末尾需要有逗号
|
|
||||||
trailingComma: 'all',
|
|
||||||
// 大括号内的首尾需要空格
|
|
||||||
bracketSpacing: true,
|
|
||||||
// jsx 标签的反尖括号需要换行
|
|
||||||
jsxBracketSameLine: false,
|
|
||||||
// 箭头函数,只有一个参数的时候,也需要括号
|
|
||||||
arrowParens: 'always',
|
|
||||||
// 每个文件格式化的范围是文件的全部内容
|
|
||||||
rangeStart: 0,
|
|
||||||
rangeEnd: Infinity,
|
|
||||||
// 不需要写文件开头的 @prettier
|
|
||||||
requirePragma: false,
|
|
||||||
// 不需要自动在文件开头插入 @prettier
|
|
||||||
insertPragma: false,
|
|
||||||
// 使用默认的折行标准
|
|
||||||
proseWrap: 'preserve',
|
|
||||||
// 根据显示样式决定 html 要不要折行
|
|
||||||
htmlWhitespaceSensitivity: 'css',
|
|
||||||
// vue 文件中的 script 和 style 内不用缩进
|
|
||||||
vueIndentScriptAndStyle: false,
|
|
||||||
// 换行符使用 lf
|
|
||||||
endOfLine: 'lf',
|
|
||||||
};
|
|
||||||
21
CHANGELOG.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 更新日志
|
||||||
|
|
||||||
|
## [v3.0.0] - 2024-03-21
|
||||||
|
|
||||||
|
### ✨ 新功能
|
||||||
|
- 新增自动更新检测功能
|
||||||
|
- 新增 GitHub Actions 自动构建和发布
|
||||||
|
- 新增主题色切换功能,支持日间/夜间模式 (#19, #21)
|
||||||
|
- 新增随机播放功能 (#20)
|
||||||
|
- 优化主题效果和图片清晰度
|
||||||
|
|
||||||
|
### 🏗️ 架构重构
|
||||||
|
- 重构整个项目架构
|
||||||
|
- 优化打包配置
|
||||||
|
- 修改后台服务为本地运行
|
||||||
|
- 优化项目结构
|
||||||
|
|
||||||
|
### 🐞 问题修复
|
||||||
|
- 修复 web 移动端页面空白问题 (#24)
|
||||||
|
- 修复无用导入问题
|
||||||
|
- 优化错误处理
|
||||||
201
LICENSE
@@ -1,201 +0,0 @@
|
|||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
20
README.md
@@ -2,12 +2,13 @@
|
|||||||
主要功能如下
|
主要功能如下
|
||||||
|
|
||||||
- 音乐推荐
|
- 音乐推荐
|
||||||
- 音乐播放
|
|
||||||
- 网易云登录
|
- 网易云登录
|
||||||
- 播放历史
|
- 播放历史
|
||||||
- 桌面歌词
|
- 桌面歌词
|
||||||
- 歌单 mv 搜索 专辑等功能
|
- 歌单 mv 搜索 专辑等功能
|
||||||
- 识别无法播放歌曲 并代理播放
|
- 识别无法播放歌曲 并解析播放
|
||||||
|
- 主题切换 更新检测
|
||||||
|
- 本地服务 不依赖线上服务
|
||||||
- 可听周杰伦(搜索专辑)
|
- 可听周杰伦(搜索专辑)
|
||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
@@ -19,9 +20,10 @@
|
|||||||
QQ群:789288579
|
QQ群:789288579
|
||||||
|
|
||||||
## 软件截图
|
## 软件截图
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
@@ -30,16 +32,8 @@ QQ群:789288579
|
|||||||
- TypeScript - JavaScript 的超集,添加了类型系统
|
- TypeScript - JavaScript 的超集,添加了类型系统
|
||||||
- Electron - 跨平台桌面应用开发框架
|
- Electron - 跨平台桌面应用开发框架
|
||||||
- Vite - 下一代前端构建工具
|
- Vite - 下一代前端构建工具
|
||||||
|
|
||||||
### UI 框架
|
|
||||||
- Naive UI - 基于 Vue 3 的组件库
|
- Naive UI - 基于 Vue 3 的组件库
|
||||||
|
|
||||||
### 项目特点
|
|
||||||
- 完整的类型支持(TypeScript)
|
|
||||||
- 模块化设计
|
|
||||||
- 自动化组件和 API 导入
|
|
||||||
- 多平台支持(Web、Desktop、Mobile Web)
|
|
||||||
- 构建优化(代码分割、压缩)
|
|
||||||
|
|
||||||
## 咖啡☕️
|
## 咖啡☕️
|
||||||
| 微信 | 支付宝 |
|
| 微信 | 支付宝 |
|
||||||
|
|||||||
148
app.js
@@ -1,148 +0,0 @@
|
|||||||
const { app, BrowserWindow, ipcMain, Tray, Menu, globalShortcut, nativeImage } = require('electron');
|
|
||||||
const path = require('path');
|
|
||||||
const Store = require('electron-store');
|
|
||||||
const setJson = require('./electron/set.json');
|
|
||||||
const { loadLyricWindow } = require('./electron/lyric');
|
|
||||||
const config = require('./electron/config');
|
|
||||||
|
|
||||||
let mainWin = null;
|
|
||||||
function createWindow() {
|
|
||||||
mainWin = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
height: 780,
|
|
||||||
frame: false,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
contextIsolation: true,
|
|
||||||
preload: path.join(__dirname, '/electron/preload.js'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const win = mainWin;
|
|
||||||
win.setMinimumSize(1200, 780);
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
win.webContents.openDevTools({ mode: 'detach' });
|
|
||||||
win.loadURL(`http://localhost:${config.development.mainPort}/`);
|
|
||||||
} else {
|
|
||||||
win.loadURL(`file://${__dirname}/dist/index.html`);
|
|
||||||
}
|
|
||||||
const image = nativeImage
|
|
||||||
.createFromPath(path.join(__dirname, 'public/icon_16x16.png'))
|
|
||||||
.resize({ width: 16, height: 16 });
|
|
||||||
const tray = new Tray(image);
|
|
||||||
|
|
||||||
// 创建一个上下文菜单
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
|
||||||
{
|
|
||||||
label: '显示',
|
|
||||||
click: () => {
|
|
||||||
win.show();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '退出',
|
|
||||||
click: () => {
|
|
||||||
win.destroy();
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 设置系统托盘图标的上下文菜单
|
|
||||||
tray.setContextMenu(contextMenu);
|
|
||||||
|
|
||||||
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
|
|
||||||
tray.on('click', () => {
|
|
||||||
if (win.isVisible()) {
|
|
||||||
win.hide();
|
|
||||||
} else {
|
|
||||||
win.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const set = store.get('set');
|
|
||||||
// store.set('set', setJson)
|
|
||||||
|
|
||||||
if (!set) {
|
|
||||||
store.set('set', setJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadLyricWindow(ipcMain, mainWin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 限制只能启动一个应用
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
|
||||||
if (!gotTheLock) {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.whenReady().then(createWindow);
|
|
||||||
|
|
||||||
app.on('ready', () => {
|
|
||||||
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
|
||||||
if (mainWin.isVisible()) {
|
|
||||||
mainWin.hide();
|
|
||||||
} else {
|
|
||||||
mainWin.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('will-quit', () => {
|
|
||||||
globalShortcut.unregisterAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('minimize-window', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
|
||||||
win.minimize();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('maximize-window', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
|
||||||
if (win.isMaximized()) {
|
|
||||||
win.unmaximize();
|
|
||||||
} else {
|
|
||||||
win.maximize();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('close-window', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
|
||||||
win.destroy();
|
|
||||||
app.quit();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('drag-start', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
|
||||||
win.webContents.beginFrameSubscription((frameBuffer) => {
|
|
||||||
event.reply('frame-buffer', frameBuffer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('mini-tray', (event) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
|
||||||
win.hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 重启
|
|
||||||
ipcMain.on('restart', () => {
|
|
||||||
app.relaunch();
|
|
||||||
app.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = new Store();
|
|
||||||
|
|
||||||
// 定义ipcRenderer监听事件
|
|
||||||
ipcMain.on('setStore', (_, key, value) => {
|
|
||||||
store.set(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('getStore', (_, key) => {
|
|
||||||
const value = store.get(key);
|
|
||||||
_.returnValue = value || '';
|
|
||||||
});
|
|
||||||
139
auto-imports.d.ts
vendored
@@ -6,70 +6,85 @@
|
|||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: (typeof import('vue'))['EffectScope'];
|
||||||
const computed: typeof import('vue')['computed']
|
const computed: (typeof import('vue'))['computed'];
|
||||||
const createApp: typeof import('vue')['createApp']
|
const createApp: (typeof import('vue'))['createApp'];
|
||||||
const customRef: typeof import('vue')['customRef']
|
const customRef: (typeof import('vue'))['customRef'];
|
||||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent'];
|
||||||
const defineComponent: typeof import('vue')['defineComponent']
|
const defineComponent: (typeof import('vue'))['defineComponent'];
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: (typeof import('vue'))['effectScope'];
|
||||||
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 h: typeof import('vue')['h']
|
const h: (typeof import('vue'))['h'];
|
||||||
const inject: typeof import('vue')['inject']
|
const inject: (typeof import('vue'))['inject'];
|
||||||
const isProxy: typeof import('vue')['isProxy']
|
const isProxy: (typeof import('vue'))['isProxy'];
|
||||||
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 markRaw: typeof import('vue')['markRaw']
|
const markRaw: (typeof import('vue'))['markRaw'];
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: (typeof import('vue'))['nextTick'];
|
||||||
const onActivated: typeof import('vue')['onActivated']
|
const onActivated: (typeof import('vue'))['onActivated'];
|
||||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
const onBeforeMount: (typeof import('vue'))['onBeforeMount'];
|
||||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount'];
|
||||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate'];
|
||||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
const onDeactivated: (typeof import('vue'))['onDeactivated'];
|
||||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured'];
|
||||||
const onMounted: typeof import('vue')['onMounted']
|
const onMounted: (typeof import('vue'))['onMounted'];
|
||||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
const onRenderTracked: (typeof import('vue'))['onRenderTracked'];
|
||||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered'];
|
||||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
const onScopeDispose: (typeof import('vue'))['onScopeDispose'];
|
||||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch'];
|
||||||
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 onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup'];
|
||||||
const provide: typeof import('vue')['provide']
|
const provide: (typeof import('vue'))['provide'];
|
||||||
const reactive: typeof import('vue')['reactive']
|
const reactive: (typeof import('vue'))['reactive'];
|
||||||
const readonly: typeof import('vue')['readonly']
|
const readonly: (typeof import('vue'))['readonly'];
|
||||||
const ref: typeof import('vue')['ref']
|
const ref: (typeof import('vue'))['ref'];
|
||||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
const resolveComponent: (typeof import('vue'))['resolveComponent'];
|
||||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
const shallowReactive: (typeof import('vue'))['shallowReactive'];
|
||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReadonly: (typeof import('vue'))['shallowReadonly'];
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: (typeof import('vue'))['shallowRef'];
|
||||||
const toRaw: typeof import('vue')['toRaw']
|
const toRaw: (typeof import('vue'))['toRaw'];
|
||||||
const toRef: typeof import('vue')['toRef']
|
const toRef: (typeof import('vue'))['toRef'];
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
const toRefs: (typeof import('vue'))['toRefs'];
|
||||||
const toValue: typeof import('vue')['toValue']
|
const toValue: (typeof import('vue'))['toValue'];
|
||||||
const triggerRef: typeof import('vue')['triggerRef']
|
const triggerRef: (typeof import('vue'))['triggerRef'];
|
||||||
const unref: typeof import('vue')['unref']
|
const unref: (typeof import('vue'))['unref'];
|
||||||
const useAttrs: typeof import('vue')['useAttrs']
|
const useAttrs: (typeof import('vue'))['useAttrs'];
|
||||||
const useCssModule: typeof import('vue')['useCssModule']
|
const useCssModule: (typeof import('vue'))['useCssModule'];
|
||||||
const useCssVars: typeof import('vue')['useCssVars']
|
const useCssVars: (typeof import('vue'))['useCssVars'];
|
||||||
const useDialog: typeof import('naive-ui')['useDialog']
|
const useDialog: (typeof import('naive-ui'))['useDialog'];
|
||||||
const useId: typeof import('vue')['useId']
|
const useId: (typeof import('vue'))['useId'];
|
||||||
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
const useLoadingBar: (typeof import('naive-ui'))['useLoadingBar'];
|
||||||
const useMessage: typeof import('naive-ui')['useMessage']
|
const useMessage: (typeof import('naive-ui'))['useMessage'];
|
||||||
const useModel: typeof import('vue')['useModel']
|
const useModel: (typeof import('vue'))['useModel'];
|
||||||
const useNotification: typeof import('naive-ui')['useNotification']
|
const useNotification: (typeof import('naive-ui'))['useNotification'];
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: (typeof import('vue'))['useSlots'];
|
||||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
const useTemplateRef: (typeof import('vue'))['useTemplateRef'];
|
||||||
const watch: typeof import('vue')['watch']
|
const watch: (typeof import('vue'))['watch'];
|
||||||
const watchEffect: typeof import('vue')['watchEffect']
|
const watchEffect: (typeof import('vue'))['watchEffect'];
|
||||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
const watchPostEffect: (typeof import('vue'))['watchPostEffect'];
|
||||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect'];
|
||||||
}
|
}
|
||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
export type {
|
||||||
import('vue')
|
Component,
|
||||||
|
ComponentPublicInstance,
|
||||||
|
ComputedRef,
|
||||||
|
DirectiveBinding,
|
||||||
|
ExtractDefaultPropTypes,
|
||||||
|
ExtractPropTypes,
|
||||||
|
ExtractPublicPropTypes,
|
||||||
|
InjectionKey,
|
||||||
|
PropType,
|
||||||
|
Ref,
|
||||||
|
MaybeRef,
|
||||||
|
MaybeRefOrGetter,
|
||||||
|
VNode,
|
||||||
|
WritableComputedRef
|
||||||
|
} from 'vue';
|
||||||
|
import('vue');
|
||||||
}
|
}
|
||||||
|
|||||||
20
build/entitlements.mac.plist
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
build/icon.icns
Normal file
BIN
build/icon.ico
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
build/icon.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
13
build/installer.nsh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 设置 Windows 7 兼容性
|
||||||
|
ManifestDPIAware true
|
||||||
|
ManifestSupportedOS all
|
||||||
|
|
||||||
|
!macro customInit
|
||||||
|
# 检查系统版本
|
||||||
|
${If} ${AtLeastWin7}
|
||||||
|
# Windows 7 或更高版本
|
||||||
|
${Else}
|
||||||
|
MessageBox MB_OK|MB_ICONSTOP "此应用程序需要 Windows 7 或更高版本。"
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"appId": "com.alger.music",
|
|
||||||
"productName": "AlgerMusic",
|
|
||||||
"artifactName": "${productName}_${version}_${arch}.${ext}",
|
|
||||||
"directories": {
|
|
||||||
"output": "dist_electron/mac"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist/**/*",
|
|
||||||
"package.json",
|
|
||||||
"app.js",
|
|
||||||
"electron/**/*",
|
|
||||||
"**/*",
|
|
||||||
"public/**/*",
|
|
||||||
"node_modules/**/*"
|
|
||||||
],
|
|
||||||
"mac": {
|
|
||||||
"icon": "public/icon.icns",
|
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "dmg",
|
|
||||||
"arch": ["x64", "arm64"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"category": "public.app-category.music",
|
|
||||||
"darkModeSupport": true
|
|
||||||
},
|
|
||||||
"dmg": {
|
|
||||||
"title": "${productName} ${version}",
|
|
||||||
"icon": "public/icon.icns",
|
|
||||||
"contents": [
|
|
||||||
{
|
|
||||||
"x": 410,
|
|
||||||
"y": 150,
|
|
||||||
"type": "link",
|
|
||||||
"path": "/Applications"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 130,
|
|
||||||
"y": 150,
|
|
||||||
"type": "file"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"window": {
|
|
||||||
"width": 540,
|
|
||||||
"height": 380
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"appId": "com.alger.music",
|
|
||||||
"productName": "AlgerMusic",
|
|
||||||
"artifactName": "${productName}_${version}_Setup_x86.${ext}",
|
|
||||||
"directories": {
|
|
||||||
"output": "dist_electron/win-x86"
|
|
||||||
},
|
|
||||||
"files": ["dist/**/*", "package.json", "app.js", "electron/**/*"],
|
|
||||||
"win": {
|
|
||||||
"icon": "public/icon.png",
|
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "nsis",
|
|
||||||
"arch": ["ia32"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extraFiles": [
|
|
||||||
{
|
|
||||||
"from": "installer/installer.nsh",
|
|
||||||
"to": "$INSTDIR"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"language": "2052",
|
|
||||||
"allowToChangeInstallationDirectory": true,
|
|
||||||
"differentialPackage": true,
|
|
||||||
"shortcutName": "Alger Music"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"appId": "com.alger.music",
|
|
||||||
"productName": "AlgerMusic",
|
|
||||||
"artifactName": "${productName}_${version}_Setup_x64.${ext}",
|
|
||||||
"directories": {
|
|
||||||
"output": "dist_electron/win-x64"
|
|
||||||
},
|
|
||||||
"files": ["dist/**/*", "package.json", "app.js", "electron/**/*"],
|
|
||||||
"win": {
|
|
||||||
"icon": "public/icon.png",
|
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "nsis",
|
|
||||||
"arch": ["x64"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extraFiles": [
|
|
||||||
{
|
|
||||||
"from": "installer/installer.nsh",
|
|
||||||
"to": "$INSTDIR"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"language": "2052",
|
|
||||||
"allowToChangeInstallationDirectory": true,
|
|
||||||
"differentialPackage": true,
|
|
||||||
"shortcutName": "Alger Music"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"appId": "com.alger.music",
|
|
||||||
"productName": "AlgerMusic",
|
|
||||||
"artifactName": "${productName}_${version}_Setup_arm64.${ext}",
|
|
||||||
"directories": {
|
|
||||||
"output": "dist_electron/win-arm64"
|
|
||||||
},
|
|
||||||
"files": ["dist/**/*", "package.json", "app.js", "electron/**/*", "!node_modules/**/*"],
|
|
||||||
"win": {
|
|
||||||
"icon": "public/icon.png",
|
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "nsis",
|
|
||||||
"arch": ["arm64"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extraFiles": [
|
|
||||||
{
|
|
||||||
"from": "installer/installer.nsh",
|
|
||||||
"to": "$INSTDIR"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"language": "2052",
|
|
||||||
"allowToChangeInstallationDirectory": true,
|
|
||||||
"differentialPackage": true,
|
|
||||||
"shortcutName": "Alger Music"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
21
components.d.ts
vendored
@@ -2,19 +2,13 @@
|
|||||||
// @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
|
||||||
export {}
|
export {};
|
||||||
|
|
||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
Coffee: typeof import('./src/components/Coffee.vue')['default']
|
|
||||||
InstallAppModal: typeof import('./src/components/common/InstallAppModal.vue')['default']
|
|
||||||
MPop: typeof import('./src/components/common/MPop.vue')['default']
|
|
||||||
MusicList: typeof import('./src/components/MusicList.vue')['default']
|
|
||||||
MvPlayer: typeof import('./src/components/MvPlayer.vue')['default']
|
|
||||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
@@ -24,27 +18,16 @@ declare module 'vue' {
|
|||||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NImage: typeof import('naive-ui')['NImage']
|
NImage: typeof import('naive-ui')['NImage']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
NLayout: typeof import('naive-ui')['NLayout']
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
NModal: typeof import('naive-ui')['NModal']
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
NPagination: typeof import('naive-ui')['NPagination']
|
|
||||||
NPopover: typeof import('naive-ui')['NPopover']
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
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']
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
|
||||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
|
||||||
PlayBottom: typeof import('./src/components/common/PlayBottom.vue')['default']
|
|
||||||
PlayListsItem: typeof import('./src/components/common/PlayListsItem.vue')['default']
|
|
||||||
PlaylistType: typeof import('./src/components/PlaylistType.vue')['default']
|
|
||||||
PlayVideo: typeof import('./src/components/common/PlayVideo.vue')['default']
|
|
||||||
RecommendAlbum: typeof import('./src/components/RecommendAlbum.vue')['default']
|
|
||||||
RecommendSinger: typeof import('./src/components/RecommendSinger.vue')['default']
|
|
||||||
RecommendSonglist: typeof import('./src/components/RecommendSonglist.vue')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SearchItem: typeof import('./src/components/common/SearchItem.vue')['default']
|
|
||||||
SongItem: typeof import('./src/components/common/SongItem.vue')['default']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
dev-app-update.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
provider: generic
|
||||||
|
url: https://example.com/auto-updates
|
||||||
|
updaterCacheDirName: electron-lan-file-updater
|
||||||
BIN
docs/image.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
docs/image1.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
docs/image2.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/image3.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 902 KiB |
|
Before Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 478 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 502 KiB |
45
electron-builder.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
appId: com.electron.app
|
||||||
|
productName: electron-lan-file
|
||||||
|
directories:
|
||||||
|
buildResources: build
|
||||||
|
files:
|
||||||
|
- '!**/.vscode/*'
|
||||||
|
- '!src/*'
|
||||||
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
|
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
|
asarUnpack:
|
||||||
|
- resources/**
|
||||||
|
win:
|
||||||
|
executableName: electron-lan-file
|
||||||
|
nsis:
|
||||||
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
|
shortcutName: ${productName}
|
||||||
|
uninstallDisplayName: ${productName}
|
||||||
|
createDesktopShortcut: always
|
||||||
|
mac:
|
||||||
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
|
extendInfo:
|
||||||
|
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
|
notarize: false
|
||||||
|
dmg:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
linux:
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- snap
|
||||||
|
- deb
|
||||||
|
maintainer: electronjs.org
|
||||||
|
category: Utility
|
||||||
|
appImage:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
npmRebuild: false
|
||||||
|
publish:
|
||||||
|
provider: generic
|
||||||
|
url: https://example.com/auto-updates
|
||||||
|
electronDownload:
|
||||||
|
mirror: https://npmmirror.com/mirrors/electron/
|
||||||
60
electron.vite.config.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite';
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import viteCompression from 'vite-plugin-compression';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve('src/renderer'),
|
||||||
|
'@renderer': resolve('src/renderer')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
viteCompression(),
|
||||||
|
// VueDevTools(),
|
||||||
|
AutoImport({
|
||||||
|
imports: [
|
||||||
|
'vue',
|
||||||
|
{
|
||||||
|
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [NaiveUiResolver()]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
// with options
|
||||||
|
[process.env.VITE_API_LOCAL as string]: {
|
||||||
|
target: process.env.VITE_API,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_LOCAL}`), '')
|
||||||
|
},
|
||||||
|
[process.env.VITE_API_MUSIC_PROXY as string]: {
|
||||||
|
target: process.env.VITE_API_MUSIC,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_MUSIC_PROXY}`), '')
|
||||||
|
},
|
||||||
|
[process.env.VITE_API_PROXY_MUSIC as string]: {
|
||||||
|
target: process.env.VITE_API_PROXY,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_PROXY_MUSIC}`), '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
// 开发环境配置
|
|
||||||
development: {
|
|
||||||
mainPort: 4488,
|
|
||||||
lyricPort: 4488,
|
|
||||||
},
|
|
||||||
// 生产环境配置
|
|
||||||
production: {
|
|
||||||
distPath: '../dist',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
|
||||||
|
|
||||||
// 主进程通信
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
|
||||||
minimize: () => ipcRenderer.send('minimize-window'),
|
|
||||||
maximize: () => ipcRenderer.send('maximize-window'),
|
|
||||||
close: () => ipcRenderer.send('close-window'),
|
|
||||||
dragStart: (data) => ipcRenderer.send('drag-start', data),
|
|
||||||
miniTray: () => ipcRenderer.send('mini-tray'),
|
|
||||||
restart: () => ipcRenderer.send('restart'),
|
|
||||||
openLyric: () => ipcRenderer.send('open-lyric'),
|
|
||||||
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 存储相关
|
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
|
||||||
ipcRenderer: {
|
|
||||||
setStoreValue: (key, value) => ipcRenderer.send('setStore', key, value),
|
|
||||||
getStoreValue: (key) => ipcRenderer.sendSync('getStore', key),
|
|
||||||
on: (channel, func) => {
|
|
||||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
|
||||||
},
|
|
||||||
once: (channel, func) => {
|
|
||||||
ipcRenderer.once(channel, (event, ...args) => func(...args));
|
|
||||||
},
|
|
||||||
send: (channel, data) => {
|
|
||||||
ipcRenderer.send(channel, data);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
const { app, BrowserWindow } = require('electron');
|
|
||||||
const axios = require('axios');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const AdmZip = require('adm-zip');
|
|
||||||
|
|
||||||
class Updater {
|
|
||||||
constructor(mainWindow) {
|
|
||||||
this.mainWindow = mainWindow;
|
|
||||||
this.updateUrl = 'http://your-server.com/update'; // 更新服务器地址
|
|
||||||
this.version = app.getVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查更新
|
|
||||||
async checkForUpdates() {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${this.updateUrl}/check`, {
|
|
||||||
params: {
|
|
||||||
version: this.version,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.hasUpdate) {
|
|
||||||
await this.downloadUpdate(response.data.downloadUrl);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('检查更新失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载更新
|
|
||||||
async downloadUpdate(downloadUrl) {
|
|
||||||
try {
|
|
||||||
const response = await axios({
|
|
||||||
url: downloadUrl,
|
|
||||||
method: 'GET',
|
|
||||||
responseType: 'arraybuffer',
|
|
||||||
});
|
|
||||||
|
|
||||||
const tempPath = path.join(app.getPath('temp'), 'update.zip');
|
|
||||||
fs.writeFileSync(tempPath, response.data);
|
|
||||||
|
|
||||||
await this.extractUpdate(tempPath);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('下载更新失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解压更新
|
|
||||||
async extractUpdate(zipPath) {
|
|
||||||
try {
|
|
||||||
const zip = new AdmZip(zipPath);
|
|
||||||
const targetPath = path.join(__dirname, '../dist'); // 前端文件目录
|
|
||||||
|
|
||||||
// 解压文件
|
|
||||||
zip.extractAllTo(targetPath, true);
|
|
||||||
|
|
||||||
// 删除临时文件
|
|
||||||
fs.unlinkSync(zipPath);
|
|
||||||
|
|
||||||
// 刷新页面
|
|
||||||
this.mainWindow.webContents.reload();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解压更新失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Updater;
|
|
||||||
57
index.html
@@ -1,57 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
||||||
|
|
||||||
<!-- SEO 元数据 -->
|
|
||||||
<title>网抑云音乐 | AlgerKong | AlgerMusicPlayer</title>
|
|
||||||
<meta name="description"
|
|
||||||
content="AlgerMusicPlayer 网抑云音乐 基于 网易云音乐API 的一款免费的在线音乐播放器,支持在线播放、歌词显示、音乐下载等功能。提供海量音乐资源,让您随时随地享受音乐。" />
|
|
||||||
<meta name="keywords" content="AlgerMusic, AlgerMusicPlayer, 网抑云, 音乐播放器, 在线音乐, 免费音乐, 歌词显示, 音乐下载, AlgerKong, 网易云音乐" />
|
|
||||||
|
|
||||||
<!-- 作者信息 -->
|
|
||||||
<meta name="author" content="AlgerKong" />
|
|
||||||
<meta name="author-url" content="https://github.com/algerkong" />
|
|
||||||
|
|
||||||
<!-- PWA 相关 -->
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
|
||||||
<meta name="apple-mobile-web-app-title" content="网抑云音乐" />
|
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
||||||
|
|
||||||
<!-- 资源预加载 -->
|
|
||||||
<link rel="preload" href="/icon/iconfont.css" as="style" />
|
|
||||||
<link rel="preload" href="/css/animate.css" as="style" />
|
|
||||||
<link rel="preload" href="/css/base.css" as="style" />
|
|
||||||
|
|
||||||
<!-- 样式表 -->
|
|
||||||
<link rel="stylesheet" href="/icon/iconfont.css" />
|
|
||||||
<link rel="stylesheet" href="/css/animate.css" />
|
|
||||||
<link rel="stylesheet" href="/css/base.css" />
|
|
||||||
<script defer src="https://cn.vercount.one/js"></script>
|
|
||||||
|
|
||||||
<!-- 动画配置 -->
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--animate-delay: 0.5s;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
|
|
||||||
<div style="display: none;">
|
|
||||||
Total Page View <span id="vercount_value_page_pv">Loading</span>
|
|
||||||
Total Visits <span id="vercount_value_site_pv">Loading</span>
|
|
||||||
Site Total Visitors <span id="vercount_value_site_uv">Loading</span>
|
|
||||||
</div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
145
package.json
@@ -1,32 +1,48 @@
|
|||||||
{
|
{
|
||||||
"name": "alger-music",
|
"name": "AlgerMusicPlayer",
|
||||||
"version": "2.4.0",
|
"version": "3.0.0",
|
||||||
"description": "这是一个用于音乐播放的应用程序。",
|
"description": "Alger Music Player",
|
||||||
"author": "Alger <algerkc@qq.com>",
|
"author": "Alger <algerkc@qq.com>",
|
||||||
"main": "app.js",
|
"main": "./out/main/index.js",
|
||||||
|
"homepage": "https://github.com/algerkong/AlgerMusicPlayer",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"format": "prettier --write .",
|
||||||
"build": "cross-env NODE_ENV=production vite build",
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||||
"serve": "vite preview",
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
"start": "cross-env NODE_ENV=development electron .",
|
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||||
"lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"b:win:x64": "cross-env NODE_ENV=production electron-builder --config ./build/win64.json",
|
"start": "electron-vite preview",
|
||||||
"b:win:x86": "cross-env NODE_ENV=production electron-builder --config ./build/win32.json",
|
"dev": "electron-vite dev",
|
||||||
"b:win:arm": "cross-env NODE_ENV=production electron-builder --config ./build/winarm64.json",
|
"build": "npm run typecheck && electron-vite build",
|
||||||
"b:mac": "cross-env NODE_ENV=production npm run build && electron-builder --config ./build/mac.json",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"b:win": "cross-env NODE_ENV=production npm run build && npm run b:win:x64 && npm run b:win:x86 && npm run b:win:arm"
|
"build:unpack": "npm run build && electron-builder --dir",
|
||||||
|
"build:win": "npm run build && electron-builder --win",
|
||||||
|
"build:mac": "npm run build && electron-builder --mac",
|
||||||
|
"build:linux": "npm run build && electron-builder --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/howler": "^2.2.12",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
|
"@unblockneteasemusic/server": "^0.27.8-patch.1",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"howler": "^2.2.4"
|
"electron-updater": "^6.1.7",
|
||||||
|
|
||||||
|
"netease-cloud-music-api-alger": "^4.25.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"marked": "^15.0.4",
|
||||||
|
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||||
|
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
||||||
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
|
"@rushstack/eslint-patch": "^1.10.3",
|
||||||
"@tailwindcss/postcss7-compat": "^2.2.4",
|
"@tailwindcss/postcss7-compat": "^2.2.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@types/howler": "^2.2.12",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@types/node": "^20.14.8",
|
||||||
"@vitejs/plugin-vue": "^5.1.3",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/compiler-sfc": "^3.5.0",
|
"@vue/compiler-sfc": "^3.5.0",
|
||||||
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
"@vue/eslint-config-typescript": "^13.0.0",
|
||||||
"@vue/runtime-core": "^3.5.0",
|
"@vue/runtime-core": "^3.5.0",
|
||||||
"@vueuse/core": "^11.0.3",
|
"@vueuse/core": "^11.0.3",
|
||||||
@@ -34,33 +50,104 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"electron": "^32.2.7",
|
"electron": "^31.0.2",
|
||||||
"electron-builder": "^25.0.5",
|
"electron-builder": "^24.13.3",
|
||||||
"eslint": "^8.56.0",
|
"electron-vite": "^2.3.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||||
"eslint-plugin-vue": "^9.21.1",
|
"eslint-plugin-vue": "^9.26.0",
|
||||||
"eslint-plugin-vue-scoped-css": "^2.7.2",
|
"eslint-plugin-vue-scoped-css": "^2.7.2",
|
||||||
|
"howler": "^2.2.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"naive-ui": "^2.39.0",
|
"naive-ui": "^2.39.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.2",
|
||||||
"remixicon": "^4.2.0",
|
"remixicon": "^4.2.0",
|
||||||
"sass": "^1.82.0",
|
"sass": "^1.82.0",
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.2",
|
||||||
"unplugin-auto-import": "^0.18.2",
|
"unplugin-auto-import": "^0.18.2",
|
||||||
"unplugin-vue-components": "^0.27.4",
|
"unplugin-vue-components": "^0.27.4",
|
||||||
"vfonts": "^0.1.0",
|
"vfonts": "^0.1.0",
|
||||||
"vite": "^5.4.3",
|
"vite": "^5.3.1",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-vue-devtools": "7.4.0",
|
"vite-plugin-vue-devtools": "7.4.0",
|
||||||
"vue": "^3.5.0",
|
"vue": "^3.4.30",
|
||||||
"vue-router": "^4.4.3",
|
"vue-router": "^4.4.3",
|
||||||
"vue-tsc": "^2.1.4",
|
"vue-tsc": "^2.0.22",
|
||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.alger.music",
|
||||||
|
"productName": "AlgerMusicPlayer",
|
||||||
|
"publish": [{
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "algerkong",
|
||||||
|
"repo": "AlgerMusicPlayer"
|
||||||
|
}],
|
||||||
|
"mac": {
|
||||||
|
"icon": "resources/icon.icns",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "dmg",
|
||||||
|
"arch": [
|
||||||
|
"universal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifactName": "${productName}-${version}-mac-${arch}.${ext}",
|
||||||
|
"darkModeSupport": true,
|
||||||
|
"hardenedRuntime": false,
|
||||||
|
"gatekeeperAssess": false,
|
||||||
|
"entitlements": "build/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "build/entitlements.mac.plist",
|
||||||
|
"notarize": false,
|
||||||
|
"identity": null,
|
||||||
|
"type": "distribution",
|
||||||
|
"binaries": [
|
||||||
|
"Contents/MacOS/AlgerMusicPlayer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"icon": "resources/favicon.ico",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "nsis",
|
||||||
|
"arch": ["x64", "ia32"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifactName": "${productName}-${version}-win-${arch}.${ext}",
|
||||||
|
"requestedExecutionLevel": "asInvoker"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"icon": "resources/icon.png",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "AppImage",
|
||||||
|
"arch": ["x64"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "deb",
|
||||||
|
"arch": ["x64"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifactName": "${productName}-${version}-linux-${arch}.${ext}",
|
||||||
|
"category": "Audio",
|
||||||
|
"maintainer": "Alger <algerkc@qq.com>"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
"installerIcon": "resources/favicon.ico",
|
||||||
|
"uninstallerIcon": "resources/favicon.ico",
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true,
|
||||||
|
"shortcutName": "AlgerMusicPlayer",
|
||||||
|
"include": "build/installer.nsh"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {}
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
7
public/css/animate.css
vendored
@@ -1,7 +0,0 @@
|
|||||||
body{
|
|
||||||
background-color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-popover:has(.music-play){
|
|
||||||
border-radius: 1.5rem !important;
|
|
||||||
}
|
|
||||||
BIN
public/icon1.png
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 626 B After Width: | Height: | Size: 626 B |
@@ -1,73 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-drawer :show="show" height="100vh" placement="bottom" :z-index="999999999">
|
|
||||||
<div class="mv-detail">
|
|
||||||
<video :src="url" controls autoplay></video>
|
|
||||||
<div class="mv-detail-title">
|
|
||||||
<div class="title">{{ title }}</div>
|
|
||||||
<button @click="close">
|
|
||||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useStore } from 'vuex';
|
|
||||||
|
|
||||||
import { audioService } from '@/services/audioService';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
show: boolean;
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.show,
|
|
||||||
(val) => {
|
|
||||||
if (val) {
|
|
||||||
store.commit('setIsPlay', false);
|
|
||||||
store.commit('setPlayMusic', false);
|
|
||||||
audioService.getCurrentSound()?.pause();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:show']);
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
emit('update:show', false);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.mv-detail {
|
|
||||||
@apply w-full h-full bg-black relative;
|
|
||||||
|
|
||||||
&-title {
|
|
||||||
@apply absolute w-full left-0 flex justify-between h-16 px-6 py-2 text-xl font-bold items-center z-50 transition-all duration-300 ease-in-out -top-24;
|
|
||||||
background: linear-gradient(0, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%);
|
|
||||||
button .icon-xiasanjiaoxing {
|
|
||||||
@apply text-3xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
@apply text-green-400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
video {
|
|
||||||
@apply w-full h-full;
|
|
||||||
}
|
|
||||||
video:hover + .mv-detail-title {
|
|
||||||
@apply top-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mv-detail-title:hover {
|
|
||||||
@apply top-0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
10
src/electron.d.ts
vendored
@@ -1,10 +0,0 @@
|
|||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
electronAPI: {
|
|
||||||
minimize: () => void;
|
|
||||||
maximize: () => void;
|
|
||||||
close: () => void;
|
|
||||||
dragStart: () => void;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/* ./src/index.css */
|
|
||||||
|
|
||||||
/*! @import */
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
.n-image img {
|
|
||||||
background-color: #111111;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-slider-handle-indicator--top {
|
|
||||||
@apply bg-transparent text-[#ffffffdd] text-2xl px-2 py-1 shadow-none mb-0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-el {
|
|
||||||
@apply overflow-ellipsis overflow-hidden whitespace-nowrap;
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<!-- menu -->
|
|
||||||
<div class="app-menu">
|
|
||||||
<div class="app-menu-header">
|
|
||||||
<div class="app-menu-logo">
|
|
||||||
<img src="/icon.png" class="w-9 h-9" alt="logo" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="app-menu-list">
|
|
||||||
<div v-for="(item, index) in menus" :key="item.path" class="app-menu-item">
|
|
||||||
<router-link class="app-menu-item-link" :to="item.path">
|
|
||||||
<i class="iconfont app-menu-item-icon" :style="iconStyle(index)" :class="item.meta.icon"></i>
|
|
||||||
<span v-if="isText" class="app-menu-item-text ml-3">{{ item.meta.title }}</span>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
isText: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: String,
|
|
||||||
default: '26px',
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: '#aaa',
|
|
||||||
},
|
|
||||||
selectColor: {
|
|
||||||
type: String,
|
|
||||||
default: '#10B981',
|
|
||||||
},
|
|
||||||
menus: {
|
|
||||||
type: Array as any,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const path = ref(route.path);
|
|
||||||
watch(
|
|
||||||
() => route.path,
|
|
||||||
async (newParams) => {
|
|
||||||
path.value = newParams;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const iconStyle = (index: number) => {
|
|
||||||
const style = {
|
|
||||||
fontSize: props.size,
|
|
||||||
color: path.value === props.menus[index].path ? props.selectColor : props.color,
|
|
||||||
};
|
|
||||||
return style;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.app-menu {
|
|
||||||
@apply flex-col items-center justify-center px-6;
|
|
||||||
max-width: 100px;
|
|
||||||
}
|
|
||||||
.app-menu-item-link,
|
|
||||||
.app-menu-header {
|
|
||||||
@apply flex items-center justify-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-menu-item-link {
|
|
||||||
@apply mb-6 mt-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-menu-item-icon:hover {
|
|
||||||
color: #10b981 !important;
|
|
||||||
transform: scale(1.05);
|
|
||||||
transition: 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile {
|
|
||||||
.app-menu {
|
|
||||||
max-width: 100%;
|
|
||||||
width: 100vw;
|
|
||||||
position: relative;
|
|
||||||
z-index: 999999;
|
|
||||||
background-color: #000;
|
|
||||||
&-header {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&-list {
|
|
||||||
@apply flex justify-between;
|
|
||||||
}
|
|
||||||
&-item {
|
|
||||||
&-link {
|
|
||||||
@apply my-4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
209
src/main/index.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import { electronApp, is, optimizer } from '@electron-toolkit/utils';
|
||||||
|
import { app, BrowserWindow, globalShortcut, ipcMain, Menu, nativeImage, shell, Tray } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
import { join } from 'path';
|
||||||
|
import set from './set.json';
|
||||||
|
// 导入所有图标
|
||||||
|
const iconPath = join(__dirname, '../../resources');
|
||||||
|
const icon = nativeImage.createFromPath(
|
||||||
|
process.platform === 'darwin'
|
||||||
|
? join(iconPath, 'icon.icns')
|
||||||
|
: process.platform === 'win32'
|
||||||
|
? join(iconPath, 'favicon.ico')
|
||||||
|
: join(iconPath, 'icon.png')
|
||||||
|
);
|
||||||
|
|
||||||
|
import { loadLyricWindow } from './lyric';
|
||||||
|
import { startMusicApi } from './server';
|
||||||
|
|
||||||
|
let mainWindow: BrowserWindow;
|
||||||
|
function createWindow(): void {
|
||||||
|
startMusicApi();
|
||||||
|
// Create the browser window.
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 780,
|
||||||
|
show: false,
|
||||||
|
frame: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
icon,
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false,
|
||||||
|
contextIsolation: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.setMinimumSize(1200, 780);
|
||||||
|
|
||||||
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
|
||||||
|
// HMR for renderer base on electron-vite cli.
|
||||||
|
// Load the remote URL for development or the local html file for production.
|
||||||
|
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||||
|
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建托盘图标
|
||||||
|
const trayIcon = nativeImage.createFromPath(join(iconPath, 'icon_16x16.png')).resize({ width: 16, height: 16 });
|
||||||
|
const tray = new Tray(trayIcon);
|
||||||
|
|
||||||
|
// 创建一个上下文菜单
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: '显示',
|
||||||
|
click: () => {
|
||||||
|
mainWindow.show();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出',
|
||||||
|
click: () => {
|
||||||
|
mainWindow.destroy();
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 设置系统托盘图标的上下文菜单
|
||||||
|
tray.setContextMenu(contextMenu);
|
||||||
|
|
||||||
|
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
|
||||||
|
tray.on('click', () => {
|
||||||
|
if (mainWindow.isVisible()) {
|
||||||
|
mainWindow.hide();
|
||||||
|
} else {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadLyricWindow(ipcMain, mainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method will be called when Electron has finished
|
||||||
|
// initialization and is ready to create browser windows.
|
||||||
|
// Some APIs can only be used after this event occurs.
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
// Set app user model id for windows
|
||||||
|
electronApp.setAppUserModelId('com.alger.music');
|
||||||
|
|
||||||
|
// Default open or close DevTools by F12 in development
|
||||||
|
// and ignore CommandOrControl + R in production.
|
||||||
|
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||||
|
app.on('browser-window-created', (_, window) => {
|
||||||
|
optimizer.watchWindowShortcuts(window);
|
||||||
|
});
|
||||||
|
|
||||||
|
// IPC test
|
||||||
|
ipcMain.on('ping', () => console.log('pong'));
|
||||||
|
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
app.on('activate', function () {
|
||||||
|
// On macOS it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('ready', () => {
|
||||||
|
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
|
||||||
|
if (mainWindow.isVisible()) {
|
||||||
|
mainWindow.hide();
|
||||||
|
} else {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
|
// for applications and their menu bar to stay active until the user quits
|
||||||
|
// explicitly with Cmd + Q.
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('minimize-window', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
win.minimize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('maximize-window', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
if (win.isMaximized()) {
|
||||||
|
win.unmaximize();
|
||||||
|
} else {
|
||||||
|
win.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('close-window', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
win.destroy();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('drag-start', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
win.webContents.beginFrameSubscription((frameBuffer) => {
|
||||||
|
event.reply('frame-buffer', frameBuffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('mini-tray', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (win) {
|
||||||
|
win.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重启
|
||||||
|
ipcMain.on('restart', () => {
|
||||||
|
app.relaunch();
|
||||||
|
app.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = new Store({
|
||||||
|
name: 'config', // 配置文件名
|
||||||
|
defaults: {
|
||||||
|
set: set
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定义ipcRenderer监听事件
|
||||||
|
ipcMain.on('set-store-value', (_, key, value) => {
|
||||||
|
store.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('get-store-value', (_, key) => {
|
||||||
|
const value = store.get(key);
|
||||||
|
_.returnValue = value || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加 IPC 处理程序
|
||||||
|
ipcMain.on('get-arch', (event) => {
|
||||||
|
event.returnValue = process.arch;
|
||||||
|
});
|
||||||
|
|
||||||
|
// In this file you can include the rest of your app"s specific main process
|
||||||
|
// code. You can also put them in separate files and require them here.
|
||||||
@@ -1,23 +1,29 @@
|
|||||||
const { BrowserWindow, screen } = require('electron');
|
import { BrowserWindow, IpcMain, screen } from 'electron';
|
||||||
const path = require('path');
|
import Store from 'electron-store';
|
||||||
const Store = require('electron-store');
|
import path, { join } from 'path';
|
||||||
const config = require('./config');
|
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
let lyricWindow = null;
|
let lyricWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
const createWin = () => {
|
const createWin = () => {
|
||||||
console.log('Creating lyric window');
|
console.log('Creating lyric window');
|
||||||
|
|
||||||
// 获取保存的窗口位置
|
// 获取保存的窗口位置
|
||||||
const windowBounds = store.get('lyricWindowBounds') || {};
|
const windowBounds =
|
||||||
|
(store.get('lyricWindowBounds') as {
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}) || {};
|
||||||
const { x, y, width, height } = windowBounds;
|
const { x, y, width, height } = windowBounds;
|
||||||
|
|
||||||
// 获取屏幕尺寸
|
// 获取屏幕尺寸
|
||||||
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
||||||
|
|
||||||
// 验证保存的位置是否有效
|
// 验证保存的位置是否有效
|
||||||
const validPosition = x !== undefined && y !== undefined && x >= 0 && y >= 0 && x < screenWidth && y < screenHeight;
|
const validPosition =
|
||||||
|
x !== undefined && y !== undefined && x >= 0 && y >= 0 && x < screenWidth && y < screenHeight;
|
||||||
|
|
||||||
lyricWindow = new BrowserWindow({
|
lyricWindow = new BrowserWindow({
|
||||||
width: width || 800,
|
width: width || 800,
|
||||||
@@ -30,63 +36,79 @@ const createWin = () => {
|
|||||||
hasShadow: false,
|
hasShadow: false,
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
contextIsolation: true,
|
sandbox: false,
|
||||||
preload: `${__dirname}/preload.js`,
|
contextIsolation: true
|
||||||
webSecurity: false,
|
}
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听窗口关闭事件
|
// 监听窗口关闭事件
|
||||||
lyricWindow.on('closed', () => {
|
lyricWindow.on('closed', () => {
|
||||||
console.log('Lyric window closed');
|
if (lyricWindow) {
|
||||||
lyricWindow = null;
|
lyricWindow.destroy();
|
||||||
|
lyricWindow = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return lyricWindow;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadLyricWindow = (ipcMain, mainWin) => {
|
export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void => {
|
||||||
ipcMain.on('open-lyric', () => {
|
const showLyricWindow = () => {
|
||||||
console.log('Received open-lyric request');
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
if (lyricWindow) {
|
if (lyricWindow.isMinimized()) {
|
||||||
console.log('Lyric window exists, focusing');
|
lyricWindow.restore();
|
||||||
if (lyricWindow.isMinimized()) lyricWindow.restore();
|
}
|
||||||
lyricWindow.focus();
|
lyricWindow.focus();
|
||||||
lyricWindow.show();
|
lyricWindow.show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcMain.on('open-lyric', () => {
|
||||||
|
console.log('Received open-lyric request');
|
||||||
|
|
||||||
|
if (showLyricWindow()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Creating new lyric window');
|
console.log('Creating new lyric window');
|
||||||
createWin();
|
const win = createWin();
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
lyricWindow.webContents.openDevTools({ mode: 'detach' });
|
if (!win) {
|
||||||
lyricWindow.loadURL(`http://localhost:${config.development.lyricPort}/#/lyric`);
|
console.error('Failed to create lyric window');
|
||||||
} else {
|
return;
|
||||||
const distPath = path.resolve(__dirname, config.production.distPath);
|
|
||||||
lyricWindow.loadURL(`file://${distPath}/index.html#/lyric`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lyricWindow.setMinimumSize(600, 200);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
lyricWindow.setSkipTaskbar(true);
|
win.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/#/lyric`);
|
||||||
|
} else {
|
||||||
|
const distPath = path.resolve(__dirname, '../renderer');
|
||||||
|
win.loadURL(`file://${distPath}/index.html#/lyric`);
|
||||||
|
}
|
||||||
|
|
||||||
lyricWindow.once('ready-to-show', () => {
|
win.setMinimumSize(600, 200);
|
||||||
|
win.setSkipTaskbar(true);
|
||||||
|
|
||||||
|
win.once('ready-to-show', () => {
|
||||||
console.log('Lyric window ready to show');
|
console.log('Lyric window ready to show');
|
||||||
lyricWindow.show();
|
win.show();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('send-lyric', (e, data) => {
|
ipcMain.on('send-lyric', (_, data) => {
|
||||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
try {
|
try {
|
||||||
lyricWindow.webContents.send('receive-lyric', data);
|
lyricWindow.webContents.send('receive-lyric', data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing lyric data:', error);
|
console.error('Error processing lyric data:', error);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log('Cannot send lyric: window not available or destroyed');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('top-lyric', (e, data) => {
|
ipcMain.on('top-lyric', (_, data) => {
|
||||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
lyricWindow.setAlwaysOnTop(data);
|
lyricWindow.setAlwaysOnTop(data);
|
||||||
}
|
}
|
||||||
@@ -96,22 +118,27 @@ const loadLyricWindow = (ipcMain, mainWin) => {
|
|||||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
lyricWindow.webContents.send('lyric-window-close');
|
lyricWindow.webContents.send('lyric-window-close');
|
||||||
mainWin.webContents.send('lyric-control-back', 'close');
|
mainWin.webContents.send('lyric-control-back', 'close');
|
||||||
lyricWindow.close();
|
lyricWindow.destroy();
|
||||||
lyricWindow = null;
|
lyricWindow = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 处理鼠标事件
|
||||||
ipcMain.on('mouseenter-lyric', () => {
|
ipcMain.on('mouseenter-lyric', () => {
|
||||||
lyricWindow.setIgnoreMouseEvents(true);
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
|
lyricWindow.setIgnoreMouseEvents(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('mouseleave-lyric', () => {
|
ipcMain.on('mouseleave-lyric', () => {
|
||||||
lyricWindow.setIgnoreMouseEvents(false);
|
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||||
|
lyricWindow.setIgnoreMouseEvents(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理拖动移动
|
// 处理拖动移动
|
||||||
ipcMain.on('lyric-drag-move', (e, { deltaX, deltaY }) => {
|
ipcMain.on('lyric-drag-move', (_, { deltaX, deltaY }) => {
|
||||||
if (!lyricWindow) return;
|
if (!lyricWindow || lyricWindow.isDestroyed()) return;
|
||||||
|
|
||||||
const [currentX, currentY] = lyricWindow.getPosition();
|
const [currentX, currentY] = lyricWindow.getPosition();
|
||||||
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
||||||
@@ -127,30 +154,23 @@ const loadLyricWindow = (ipcMain, mainWin) => {
|
|||||||
store.set('lyricWindowBounds', {
|
store.set('lyricWindowBounds', {
|
||||||
...lyricWindow.getBounds(),
|
...lyricWindow.getBounds(),
|
||||||
x: newX,
|
x: newX,
|
||||||
y: newY,
|
y: newY
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加鼠标穿透事件处理
|
// 添加鼠标穿透事件处理
|
||||||
ipcMain.on('set-ignore-mouse', (e, shouldIgnore) => {
|
ipcMain.on('set-ignore-mouse', (_, shouldIgnore) => {
|
||||||
if (!lyricWindow) return;
|
if (!lyricWindow || lyricWindow.isDestroyed()) return;
|
||||||
|
|
||||||
if (shouldIgnore) {
|
lyricWindow.setIgnoreMouseEvents(shouldIgnore, { forward: true });
|
||||||
// 设置鼠标穿透,但保留拖动区域可交互
|
|
||||||
lyricWindow.setIgnoreMouseEvents(true, { forward: true });
|
|
||||||
} else {
|
|
||||||
// 取消鼠标穿透
|
|
||||||
lyricWindow.setIgnoreMouseEvents(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加播放控制处理
|
// 添加播放控制处理
|
||||||
ipcMain.on('control-back', (e, command) => {
|
ipcMain.on('control-back', (_, command) => {
|
||||||
console.log('Received control-back request:', command);
|
console.log('command', command);
|
||||||
mainWin.webContents.send('lyric-control-back', command);
|
if (mainWin && !mainWin.isDestroyed()) {
|
||||||
|
console.log('Sending control-back command:', command);
|
||||||
|
mainWin.webContents.send('lyric-control-back', command);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
loadLyricWindow,
|
|
||||||
};
|
|
||||||
32
src/main/server.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { unblockMusic } from './unblockMusic';
|
||||||
|
|
||||||
|
const store = new Store();
|
||||||
|
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
|
||||||
|
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理解锁音乐请求
|
||||||
|
ipcMain.handle('unblock-music', async (_, id) => {
|
||||||
|
return unblockMusic(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
import server from 'netease-cloud-music-api-alger/server';
|
||||||
|
|
||||||
|
|
||||||
|
async function startMusicApi(): Promise<void> {
|
||||||
|
console.log('MUSIC API STARTED');
|
||||||
|
|
||||||
|
const port = (store.get('set') as any).musicApiPort || 30488;
|
||||||
|
|
||||||
|
await server.serveNcmApi({
|
||||||
|
port
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { startMusicApi };
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
"noAnimate": false,
|
"noAnimate": false,
|
||||||
"animationSpeed": 1,
|
"animationSpeed": 1,
|
||||||
"author": "Alger",
|
"author": "Alger",
|
||||||
"authorUrl": "https://github.com/algerkong"
|
"authorUrl": "https://github.com/algerkong",
|
||||||
|
"musicApiPort": 30488
|
||||||
}
|
}
|
||||||
23
src/main/unblockMusic.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import match from '@unblockneteasemusic/server';
|
||||||
|
|
||||||
|
const unblockMusic = async (id: any): Promise<any> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
match(parseInt(id, 10), ['qq', 'migu', 'kugou', 'joox'])
|
||||||
|
.then((data) => {
|
||||||
|
resolve({
|
||||||
|
data: {
|
||||||
|
data,
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
type: 'song'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { unblockMusic };
|
||||||
18
src/preload/index.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { ElectronAPI } from '@electron-toolkit/preload';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron: ElectronAPI;
|
||||||
|
api: {
|
||||||
|
sendLyric: (data: string) => void;
|
||||||
|
openLyric: () => void;
|
||||||
|
minimize: () => void;
|
||||||
|
maximize: () => void;
|
||||||
|
close: () => void;
|
||||||
|
dragStart: (data: string) => void;
|
||||||
|
miniTray: () => void;
|
||||||
|
restart: () => void;
|
||||||
|
unblockMusic: (id: number) => Promise<any>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/preload/index.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { electronAPI } from '@electron-toolkit/preload';
|
||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
// Custom APIs for renderer
|
||||||
|
const api = {
|
||||||
|
minimize: () => ipcRenderer.send('minimize-window'),
|
||||||
|
maximize: () => ipcRenderer.send('maximize-window'),
|
||||||
|
close: () => ipcRenderer.send('close-window'),
|
||||||
|
dragStart: (data) => ipcRenderer.send('drag-start', data),
|
||||||
|
miniTray: () => ipcRenderer.send('mini-tray'),
|
||||||
|
restart: () => ipcRenderer.send('restart'),
|
||||||
|
openLyric: () => ipcRenderer.send('open-lyric'),
|
||||||
|
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
|
||||||
|
unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
// renderer only if context isolation is enabled, otherwise
|
||||||
|
// just add to the DOM global.
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||||
|
contextBridge.exposeInMainWorld('api', api);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.electron = electronAPI;
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.api = api;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container" :class="{ mobile: isMobile, noElectron: !isElectron }">
|
<div class="app-container" :class="{ mobile: isMobile, noElectron: !isElectron }">
|
||||||
<n-config-provider :theme="darkTheme">
|
<n-config-provider :theme="theme === 'dark' ? darkTheme : lightTheme">
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
@@ -11,21 +11,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { darkTheme } from 'naive-ui';
|
import { darkTheme, lightTheme } from 'naive-ui';
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
|
import { isElectron } from '@/utils';
|
||||||
|
|
||||||
import { isElectron } from '@/hooks/MusicHook';
|
|
||||||
import homeRouter from '@/router/home';
|
import homeRouter from '@/router/home';
|
||||||
import store from '@/store';
|
import store from '@/store';
|
||||||
|
|
||||||
import { isMobile } from './utils';
|
import { isMobile } from './utils';
|
||||||
|
|
||||||
|
const theme = computed(() => {
|
||||||
|
return store.state.theme;
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.dispatch('initializeSettings');
|
store.dispatch('initializeSettings');
|
||||||
|
store.dispatch('initializeTheme');
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
store.commit(
|
store.commit(
|
||||||
'setMenus',
|
'setMenus',
|
||||||
homeRouter.filter((item) => item.meta.isMobile),
|
homeRouter.filter((item) => item.meta.isMobile)
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'qqq ',
|
||||||
|
homeRouter.filter((item) => item.meta.isMobile)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ export function getListByTag(params: IListByTagParams) {
|
|||||||
// 根据cat 获取歌单列表
|
// 根据cat 获取歌单列表
|
||||||
export function getListByCat(params: IListByCatParams) {
|
export function getListByCat(params: IListByCatParams) {
|
||||||
return request.get('/top/playlist', {
|
return request.get('/top/playlist', {
|
||||||
params,
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +41,6 @@ export function logout() {
|
|||||||
export function loginByCellphone(phone: string, password: string) {
|
export function loginByCellphone(phone: string, password: string) {
|
||||||
return request.post('/login/cellphone', {
|
return request.post('/login/cellphone', {
|
||||||
phone,
|
phone,
|
||||||
password,
|
password
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ILyric } from '@/type/lyric';
|
import { ILyric } from '@/type/lyric';
|
||||||
import { IPlayMusicUrl } from '@/type/music';
|
import { IPlayMusicUrl } from '@/type/music';
|
||||||
|
import { isElectron } from '@/utils';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import requestMusic from '@/utils/request_music';
|
import requestMusic from '@/utils/request_music';
|
||||||
// 根据音乐Id获取音乐播放URl
|
// 根据音乐Id获取音乐播放URl
|
||||||
@@ -18,5 +19,8 @@ export const getMusicLrc = (id: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getParsingMusicUrl = (id: number) => {
|
export const getParsingMusicUrl = (id: number) => {
|
||||||
|
if (isElectron) {
|
||||||
|
return window.api.unblockMusic(id);
|
||||||
|
}
|
||||||
return requestMusic.get<any>('/music', { params: { id } });
|
return requestMusic.get<any>('/music', { params: { id } });
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IData } from '@/type';
|
import { IData } from '@/type';
|
||||||
import { IMvItem, IMvUrlData } from '@/type/mv';
|
import { IMvUrlData } from '@/type/mv';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
|
|
||||||
interface MvParams {
|
interface MvParams {
|
||||||
@@ -13,7 +13,7 @@ export const getTopMv = (params: MvParams) => {
|
|||||||
return request({
|
return request({
|
||||||
url: '/mv/all',
|
url: '/mv/all',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params,
|
params
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export const getAllMv = (params: MvParams) => {
|
|||||||
return request({
|
return request({
|
||||||
url: '/mv/all',
|
url: '/mv/all',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params,
|
params
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ export const getAllMv = (params: MvParams) => {
|
|||||||
export const getMvDetail = (mvid: string) => {
|
export const getMvDetail = (mvid: string) => {
|
||||||
return request.get('/mv/detail', {
|
return request.get('/mv/detail', {
|
||||||
params: {
|
params: {
|
||||||
mvid,
|
mvid
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export const getMvDetail = (mvid: string) => {
|
|||||||
export const getMvUrl = (id: Number) => {
|
export const getMvUrl = (id: Number) => {
|
||||||
return request.get<IData<IMvUrlData>>('/mv/url', {
|
return request.get<IData<IMvUrlData>>('/mv/url', {
|
||||||
params: {
|
params: {
|
||||||
id,
|
id
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -7,6 +7,6 @@ interface IParams {
|
|||||||
// 搜索内容
|
// 搜索内容
|
||||||
export const getSearch = (params: IParams) => {
|
export const getSearch = (params: IParams) => {
|
||||||
return request.get<any>('/cloudsearch', {
|
return request.get<any>('/cloudsearch', {
|
||||||
params,
|
params
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
3687
src/renderer/assets/css/animate.css
vendored
Normal file
7
src/renderer/assets/css/base.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
body {
|
||||||
|
/* background-color: #000; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-popover:has(.music-play) {
|
||||||
|
border-radius: 1.5rem !important;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@@ -1,12 +1,13 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 2685283 */
|
font-family: 'iconfont'; /* Project id 2685283 */
|
||||||
src: url('iconfont.woff2?t=1703643214551') format('woff2'),
|
src:
|
||||||
url('iconfont.woff?t=1703643214551') format('woff'),
|
url('iconfont.woff2?t=1703643214551') format('woff2'),
|
||||||
url('iconfont.ttf?t=1703643214551') format('truetype');
|
url('iconfont.woff?t=1703643214551') format('woff'),
|
||||||
|
url('iconfont.ttf?t=1703643214551') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
font-family: "iconfont" !important;
|
font-family: 'iconfont' !important;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -14,270 +15,269 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-list:before {
|
.icon-list:before {
|
||||||
content: "\e603";
|
content: '\e603';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-maxsize:before {
|
.icon-maxsize:before {
|
||||||
content: "\e692";
|
content: '\e692';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-close:before {
|
.icon-close:before {
|
||||||
content: "\e616";
|
content: '\e616';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-minisize:before {
|
.icon-minisize:before {
|
||||||
content: "\e602";
|
content: '\e602';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-shuaxin:before {
|
.icon-shuaxin:before {
|
||||||
content: "\e627";
|
content: '\e627';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-icon_error:before {
|
.icon-icon_error:before {
|
||||||
content: "\e615";
|
content: '\e615';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-3User:before {
|
.icon-a-3User:before {
|
||||||
content: "\e601";
|
content: '\e601';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Chat:before {
|
.icon-Chat:before {
|
||||||
content: "\e605";
|
content: '\e605';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Category:before {
|
.icon-Category:before {
|
||||||
content: "\e606";
|
content: '\e606';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Document:before {
|
.icon-Document:before {
|
||||||
content: "\e607";
|
content: '\e607';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Heart:before {
|
.icon-Heart:before {
|
||||||
content: "\e608";
|
content: '\e608';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Hide:before {
|
.icon-Hide:before {
|
||||||
content: "\e609";
|
content: '\e609';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Home:before {
|
.icon-Home:before {
|
||||||
content: "\e60a";
|
content: '\e60a';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-Image2:before {
|
.icon-a-Image2:before {
|
||||||
content: "\e60b";
|
content: '\e60b';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Profile:before {
|
.icon-Profile:before {
|
||||||
content: "\e60c";
|
content: '\e60c';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Search:before {
|
.icon-Search:before {
|
||||||
content: "\e60d";
|
content: '\e60d';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Paper:before {
|
.icon-Paper:before {
|
||||||
content: "\e60e";
|
content: '\e60e';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Play:before {
|
.icon-Play:before {
|
||||||
content: "\e60f";
|
content: '\e60f';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-Setting:before {
|
.icon-Setting:before {
|
||||||
content: "\e610";
|
content: '\e610';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-TicketStar:before {
|
.icon-a-TicketStar:before {
|
||||||
content: "\e611";
|
content: '\e611';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-VolumeOff:before {
|
.icon-a-VolumeOff:before {
|
||||||
content: "\e612";
|
content: '\e612';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-VolumeUp:before {
|
.icon-a-VolumeUp:before {
|
||||||
content: "\e613";
|
content: '\e613';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-VolumeDown:before {
|
.icon-a-VolumeDown:before {
|
||||||
content: "\e614";
|
content: '\e614';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-stop:before {
|
.icon-stop:before {
|
||||||
content: "\e600";
|
content: '\e600';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-next:before {
|
.icon-next:before {
|
||||||
content: "\e6a9";
|
content: '\e6a9';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-prev:before {
|
.icon-prev:before {
|
||||||
content: "\e6ac";
|
content: '\e6ac';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-play:before {
|
.icon-play:before {
|
||||||
content: "\e6aa";
|
content: '\e6aa';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-xiasanjiaoxing:before {
|
.icon-xiasanjiaoxing:before {
|
||||||
content: "\e642";
|
content: '\e642';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-videofill:before {
|
.icon-videofill:before {
|
||||||
content: "\e7c7";
|
content: '\e7c7';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-favorfill:before {
|
.icon-favorfill:before {
|
||||||
content: "\e64b";
|
content: '\e64b';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-favor:before {
|
.icon-favor:before {
|
||||||
content: "\e64c";
|
content: '\e64c';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-loading:before {
|
.icon-loading:before {
|
||||||
content: "\e64f";
|
content: '\e64f';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-search:before {
|
.icon-search:before {
|
||||||
content: "\e65c";
|
content: '\e65c';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-likefill:before {
|
.icon-likefill:before {
|
||||||
content: "\e668";
|
content: '\e668';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-like:before {
|
.icon-like:before {
|
||||||
content: "\e669";
|
content: '\e669';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-notificationfill:before {
|
.icon-notificationfill:before {
|
||||||
content: "\e66a";
|
content: '\e66a';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-notification:before {
|
.icon-notification:before {
|
||||||
content: "\e66b";
|
content: '\e66b';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-evaluate:before {
|
.icon-evaluate:before {
|
||||||
content: "\e672";
|
content: '\e672';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-homefill:before {
|
.icon-homefill:before {
|
||||||
content: "\e6bb";
|
content: '\e6bb';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-link:before {
|
.icon-link:before {
|
||||||
content: "\e6bf";
|
content: '\e6bf';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-roundaddfill:before {
|
.icon-roundaddfill:before {
|
||||||
content: "\e6d8";
|
content: '\e6d8';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-roundadd:before {
|
.icon-roundadd:before {
|
||||||
content: "\e6d9";
|
content: '\e6d9';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-add:before {
|
.icon-add:before {
|
||||||
content: "\e6da";
|
content: '\e6da';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-appreciatefill:before {
|
.icon-appreciatefill:before {
|
||||||
content: "\e6e3";
|
content: '\e6e3';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-forwardfill:before {
|
.icon-forwardfill:before {
|
||||||
content: "\e6ea";
|
content: '\e6ea';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-voicefill:before {
|
.icon-voicefill:before {
|
||||||
content: "\e6f0";
|
content: '\e6f0';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-wefill:before {
|
.icon-wefill:before {
|
||||||
content: "\e6f4";
|
content: '\e6f4';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-keyboard:before {
|
.icon-keyboard:before {
|
||||||
content: "\e71b";
|
content: '\e71b';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-picfill:before {
|
.icon-picfill:before {
|
||||||
content: "\e72c";
|
content: '\e72c';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-markfill:before {
|
.icon-markfill:before {
|
||||||
content: "\e730";
|
content: '\e730';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-presentfill:before {
|
.icon-presentfill:before {
|
||||||
content: "\e732";
|
content: '\e732';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-peoplefill:before {
|
.icon-peoplefill:before {
|
||||||
content: "\e735";
|
content: '\e735';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-read:before {
|
.icon-read:before {
|
||||||
content: "\e742";
|
content: '\e742';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-backwardfill:before {
|
.icon-backwardfill:before {
|
||||||
content: "\e74d";
|
content: '\e74d';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-playfill:before {
|
.icon-playfill:before {
|
||||||
content: "\e74f";
|
content: '\e74f';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-all:before {
|
.icon-all:before {
|
||||||
content: "\e755";
|
content: '\e755';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-hotfill:before {
|
.icon-hotfill:before {
|
||||||
content: "\e757";
|
content: '\e757';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-recordfill:before {
|
.icon-recordfill:before {
|
||||||
content: "\e7a4";
|
content: '\e7a4';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-full:before {
|
.icon-full:before {
|
||||||
content: "\e7bc";
|
content: '\e7bc';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-favor_fill_light:before {
|
.icon-favor_fill_light:before {
|
||||||
content: "\e7ec";
|
content: '\e7ec';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-round_favor_fill:before {
|
.icon-round_favor_fill:before {
|
||||||
content: "\e80a";
|
content: '\e80a';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-round_location_fill:before {
|
.icon-round_location_fill:before {
|
||||||
content: "\e80b";
|
content: '\e80b';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-round_like_fill:before {
|
.icon-round_like_fill:before {
|
||||||
content: "\e80c";
|
content: '\e80c';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-round_people_fill:before {
|
.icon-round_people_fill:before {
|
||||||
content: "\e80d";
|
content: '\e80d';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-round_skin_fill:before {
|
.icon-round_skin_fill:before {
|
||||||
content: "\e80e";
|
content: '\e80e';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-broadcast_fill:before {
|
.icon-broadcast_fill:before {
|
||||||
content: "\e81d";
|
content: '\e81d';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-card_fill:before {
|
.icon-card_fill:before {
|
||||||
content: "\e81f";
|
content: '\e81f';
|
||||||
}
|
}
|
||||||
|
|
||||||
66
src/renderer/assets/icon/iconfont.js
Normal file
BIN
src/renderer/assets/logo.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
75
src/renderer/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useDialog: typeof import('naive-ui')['useDialog']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
|
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
||||||
|
const useMessage: typeof import('naive-ui')['useMessage']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
|
const useNotification: typeof import('naive-ui')['useNotification']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
37
src/renderer/components.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||||
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||||
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
|
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||||
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
|
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||||
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
|
NImage: typeof import('naive-ui')['NImage']
|
||||||
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
|
NSpin: typeof import('naive-ui')['NSpin']
|
||||||
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
|
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||||
|
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,20 +12,33 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="p-6 bg-black rounded-lg shadow-lg">
|
<div class="p-6 rounded-lg shadow-lg bg-light dark:bg-gray-800">
|
||||||
<div class="flex gap-10">
|
<div class="flex gap-10">
|
||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<n-image :src="alipayQR" alt="支付宝收款码" class="w-32 h-32 rounded-lg cursor-none" preview-disabled />
|
<n-image
|
||||||
<span class="text-sm text-gray-100">支付宝</span>
|
:src="alipayQR"
|
||||||
|
alt="支付宝收款码"
|
||||||
|
class="w-32 h-32 rounded-lg cursor-none"
|
||||||
|
preview-disabled
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-200">支付宝</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<n-image :src="wechatQR" alt="微信收款码" class="w-32 h-32 rounded-lg cursor-none" preview-disabled />
|
<n-image
|
||||||
<span class="text-sm text-gray-100">微信支付</span>
|
:src="wechatQR"
|
||||||
|
alt="微信收款码"
|
||||||
|
class="w-32 h-32 rounded-lg cursor-none"
|
||||||
|
preview-disabled
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-200">微信支付</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<p class="text-sm text-gray-100 text-center cursor-pointer hover:text-green-500" @click="copyQQ">
|
<p
|
||||||
|
class="text-sm text-gray-700 dark:text-gray-200 text-center cursor-pointer hover:text-green-500"
|
||||||
|
@click="copyQQ"
|
||||||
|
>
|
||||||
QQ群:789288579
|
QQ群:789288579
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,11 +59,11 @@ const copyQQ = () => {
|
|||||||
defineProps({
|
defineProps({
|
||||||
alipayQR: {
|
alipayQR: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
wechatQR: {
|
wechatQR: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -25,24 +25,24 @@
|
|||||||
<div class="music-info">
|
<div class="music-info">
|
||||||
<div class="music-cover">
|
<div class="music-cover">
|
||||||
<n-image
|
<n-image
|
||||||
:src="getImgUrl(cover ? listInfo?.coverImgUrl : displayedSongs[0]?.picUrl, '300y300')"
|
:src="getImgUrl(cover ? listInfo?.coverImgUrl : displayedSongs[0]?.picUrl, '500y500')"
|
||||||
class="cover-img"
|
class="cover-img"
|
||||||
preview-disabled
|
preview-disabled
|
||||||
:class="setAnimationClass('animate__fadeIn')"
|
:class="setAnimationClass('animate__fadeIn')"
|
||||||
object-fit="cover"
|
object-fit="cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-detail">
|
<div v-if="listInfo?.creator" class="creator-info">
|
||||||
<div v-if="listInfo?.creator" class="creator-info">
|
<n-avatar round :size="24" :src="getImgUrl(listInfo.creator.avatarUrl, '50y50')" />
|
||||||
<n-avatar round :size="24" :src="getImgUrl(listInfo.creator.avatarUrl, '50y50')" />
|
<span class="creator-name">{{ listInfo.creator.nickname }}</span>
|
||||||
<span class="creator-name">{{ listInfo.creator.nickname }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="listInfo?.description" class="music-desc">
|
|
||||||
<n-ellipsis :line-clamp="isMobile ? 3 : 10">
|
|
||||||
{{ listInfo.description }}
|
|
||||||
</n-ellipsis>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<n-scrollbar style="max-height: 200">
|
||||||
|
<div v-if="listInfo?.description" class="music-desc">
|
||||||
|
{{ listInfo.description }}
|
||||||
|
</div>
|
||||||
|
<play-bottom />
|
||||||
|
</n-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧歌曲列表 -->
|
<!-- 右侧歌曲列表 -->
|
||||||
@@ -98,8 +98,8 @@ const props = withDefaults(
|
|||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
loading: false,
|
loading: false,
|
||||||
cover: true,
|
cover: true
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits(['update:show', 'update:loading']);
|
const emit = defineEmits(['update:show', 'update:loading']);
|
||||||
@@ -122,7 +122,7 @@ const formatDetail = computed(() => (detail: any) => {
|
|||||||
const song = {
|
const song = {
|
||||||
artists: detail.ar,
|
artists: detail.ar,
|
||||||
name: detail.al.name,
|
name: detail.al.name,
|
||||||
id: detail.al.id,
|
id: detail.al.id
|
||||||
};
|
};
|
||||||
|
|
||||||
detail.song = song;
|
detail.song = song;
|
||||||
@@ -138,9 +138,9 @@ const handlePlay = () => {
|
|||||||
...item,
|
...item,
|
||||||
picUrl: item.al.picUrl,
|
picUrl: item.al.picUrl,
|
||||||
song: {
|
song: {
|
||||||
artists: item.ar,
|
artists: item.ar
|
||||||
},
|
}
|
||||||
})),
|
}))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ watch(
|
|||||||
if (!props.cover) {
|
if (!props.cover) {
|
||||||
loadingList.value = false;
|
loadingList.value = false;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 监听 songList 变化,重置分页状态
|
// 监听 songList 变化,重置分页状态
|
||||||
@@ -218,23 +218,23 @@ watch(
|
|||||||
}
|
}
|
||||||
loadingList.value = false;
|
loadingList.value = false;
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.music {
|
.music {
|
||||||
&-title {
|
&-title {
|
||||||
@apply text-xl font-bold text-white;
|
@apply text-xl font-bold text-gray-900 dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-page {
|
&-page {
|
||||||
@apply px-8 w-full h-full bg-black bg-opacity-75 rounded-t-2xl;
|
@apply px-8 w-full h-full bg-light dark:bg-black bg-opacity-75 dark:bg-opacity-75 rounded-t-2xl;
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-close {
|
&-close {
|
||||||
@apply cursor-pointer text-white flex gap-2 items-center;
|
@apply cursor-pointer text-gray-900 dark:text-white flex gap-2 items-center;
|
||||||
.icon {
|
.icon {
|
||||||
@apply text-3xl;
|
@apply text-3xl;
|
||||||
}
|
}
|
||||||
@@ -248,34 +248,29 @@ watch(
|
|||||||
@apply w-[25%] flex-shrink-0 pr-8 flex flex-col;
|
@apply w-[25%] flex-shrink-0 pr-8 flex flex-col;
|
||||||
|
|
||||||
.music-cover {
|
.music-cover {
|
||||||
@apply w-full aspect-square rounded-lg overflow-hidden mb-4;
|
@apply w-full aspect-square rounded-2xl overflow-hidden mb-4 min-h-[250px];
|
||||||
.cover-img {
|
.cover-img {
|
||||||
@apply w-full h-full object-cover;
|
@apply w-full h-full object-cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.music-detail {
|
.creator-info {
|
||||||
@apply flex flex-col flex-grow;
|
@apply flex items-center mb-4;
|
||||||
|
.creator-name {
|
||||||
.creator-info {
|
@apply ml-2 text-gray-700 dark:text-gray-300;
|
||||||
@apply flex items-center mb-4;
|
|
||||||
.creator-name {
|
|
||||||
@apply ml-2 text-sm text-gray-300;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-desc {
|
|
||||||
@apply text-sm text-gray-400;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&-list-container {
|
.music-desc {
|
||||||
@apply flex-grow min-h-0 flex flex-col relative;
|
@apply text-sm text-gray-600 dark:text-gray-400 leading-relaxed pr-4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-list {
|
&-list {
|
||||||
@apply flex-grow min-h-0;
|
@apply flex-grow min-h-0;
|
||||||
|
&-container {
|
||||||
|
@apply flex-grow min-h-0 flex flex-col relative;
|
||||||
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
@apply min-h-[calc(80vh-60px)];
|
@apply min-h-[calc(80vh-60px)];
|
||||||
@@ -312,16 +307,10 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loading-more {
|
.loading-more {
|
||||||
@apply text-center text-white py-10;
|
@apply text-center py-4 text-gray-500 dark:text-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.double-list {
|
.double-item {
|
||||||
.double-item {
|
@apply mb-2 bg-light-100 bg-opacity-20 dark:bg-dark-100 dark:bg-opacity-20 rounded-3xl;
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-item {
|
|
||||||
background-color: #191919;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-drawer :show="show" height="100%" placement="bottom" :z-index="999999999" :to="`#layout-main`">
|
<n-drawer :show="show" height="100%" placement="bottom" :z-index="999999999" :to="`#layout-main`">
|
||||||
<div class="mv-detail">
|
<div class="mv-detail">
|
||||||
<div ref="videoContainerRef" class="video-container" :class="{ 'cursor-hidden': !showCursor }">
|
<div
|
||||||
|
ref="videoContainerRef"
|
||||||
|
class="video-container"
|
||||||
|
:class="{ 'cursor-hidden': !showCursor }"
|
||||||
|
>
|
||||||
<video
|
<video
|
||||||
ref="videoRef"
|
ref="videoRef"
|
||||||
:src="mvUrl"
|
:src="mvUrl"
|
||||||
@@ -86,7 +90,9 @@
|
|||||||
下一个
|
下一个
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
|
|
||||||
<div class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</div>
|
<div class="time-display">
|
||||||
|
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-controls">
|
<div class="right-controls">
|
||||||
@@ -96,14 +102,22 @@
|
|||||||
<n-button quaternary circle @click="toggleMute">
|
<n-button quaternary circle @click="toggleMute">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon size="24">
|
<n-icon size="24">
|
||||||
<i :class="volume === 0 ? 'ri-volume-mute-line' : 'ri-volume-up-line'"></i>
|
<i
|
||||||
|
:class="volume === 0 ? 'ri-volume-mute-line' : 'ri-volume-up-line'"
|
||||||
|
></i>
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
{{ volume === 0 ? '取消静音' : '静音' }}
|
{{ volume === 0 ? '取消静音' : '静音' }}
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
<n-slider v-model:value="volume" :min="0" :max="100" :tooltip="false" class="volume-slider" />
|
<n-slider
|
||||||
|
v-model:value="volume"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:tooltip="false"
|
||||||
|
class="volume-slider"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-tooltip v-if="!props.noList" placement="top">
|
<n-tooltip v-if="!props.noList" placement="top">
|
||||||
@@ -111,7 +125,11 @@
|
|||||||
<n-button quaternary circle class="play-mode-btn" @click="togglePlayMode">
|
<n-button quaternary circle class="play-mode-btn" @click="togglePlayMode">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon size="24">
|
<n-icon size="24">
|
||||||
<i :class="playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'"></i>
|
<i
|
||||||
|
:class="
|
||||||
|
playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'
|
||||||
|
"
|
||||||
|
></i>
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button>
|
||||||
@@ -124,7 +142,9 @@
|
|||||||
<n-button quaternary circle @click="toggleFullscreen">
|
<n-button quaternary circle @click="toggleFullscreen">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon size="24">
|
<n-icon size="24">
|
||||||
<i :class="isFullscreen ? 'ri-fullscreen-exit-line' : 'ri-fullscreen-line'"></i>
|
<i
|
||||||
|
:class="isFullscreen ? 'ri-fullscreen-exit-line' : 'ri-fullscreen-line'"
|
||||||
|
></i>
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button>
|
||||||
@@ -181,7 +201,7 @@ import { IMvItem } from '@/type/mv';
|
|||||||
type PlayMode = 'single' | 'auto';
|
type PlayMode = 'single' | 'auto';
|
||||||
const PLAY_MODE = {
|
const PLAY_MODE = {
|
||||||
Single: 'single' as PlayMode,
|
Single: 'single' as PlayMode,
|
||||||
Auto: 'auto' as PlayMode,
|
Auto: 'auto' as PlayMode
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -193,8 +213,8 @@ const props = withDefaults(
|
|||||||
{
|
{
|
||||||
show: false,
|
show: false,
|
||||||
currentMv: undefined,
|
currentMv: undefined,
|
||||||
noList: false,
|
noList: false
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -310,7 +330,7 @@ watch(
|
|||||||
if (newMv) {
|
if (newMv) {
|
||||||
await loadMvUrl(newMv);
|
await loadMvUrl(newMv);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const autoPlayBlocked = ref(false);
|
const autoPlayBlocked = ref(false);
|
||||||
@@ -383,11 +403,21 @@ const checkFullscreenAPI = () => {
|
|||||||
(videoContainerRef.value as any)?.webkitRequestFullscreen ||
|
(videoContainerRef.value as any)?.webkitRequestFullscreen ||
|
||||||
(videoContainerRef.value as any)?.mozRequestFullScreen ||
|
(videoContainerRef.value as any)?.mozRequestFullScreen ||
|
||||||
(videoContainerRef.value as any)?.msRequestFullscreen,
|
(videoContainerRef.value as any)?.msRequestFullscreen,
|
||||||
exitFullscreen: doc.exitFullscreen || doc.webkitExitFullscreen || doc.mozCancelFullScreen || doc.msExitFullscreen,
|
exitFullscreen:
|
||||||
|
doc.exitFullscreen ||
|
||||||
|
doc.webkitExitFullscreen ||
|
||||||
|
doc.mozCancelFullScreen ||
|
||||||
|
doc.msExitFullscreen,
|
||||||
fullscreenElement:
|
fullscreenElement:
|
||||||
doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement,
|
doc.fullscreenElement ||
|
||||||
|
doc.webkitFullscreenElement ||
|
||||||
|
doc.mozFullScreenElement ||
|
||||||
|
doc.msFullscreenElement,
|
||||||
fullscreenEnabled:
|
fullscreenEnabled:
|
||||||
doc.fullscreenEnabled || doc.webkitFullscreenEnabled || doc.mozFullScreenEnabled || doc.msFullscreenEnabled,
|
doc.fullscreenEnabled ||
|
||||||
|
doc.webkitFullscreenEnabled ||
|
||||||
|
doc.mozFullScreenEnabled ||
|
||||||
|
doc.msFullscreenEnabled
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -549,226 +579,57 @@ const isMobile = computed(() => store.state.isMobile);
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.mv-detail {
|
.mv-detail {
|
||||||
@apply w-full h-full bg-black relative;
|
@apply h-full bg-light dark:bg-black;
|
||||||
|
|
||||||
// 添加横屏模式支持
|
&-title {
|
||||||
@media screen and (orientation: landscape) {
|
@apply fixed top-0 left-0 right-0 p-4 z-10 transition-opacity duration-300;
|
||||||
height: 100% !important;
|
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), transparent);
|
||||||
width: 100% !important;
|
|
||||||
|
.title {
|
||||||
|
@apply text-white text-lg font-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
@apply h-full w-full relative;
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
@apply h-full w-full object-contain bg-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container {
|
.play-hint {
|
||||||
@apply w-full h-full relative;
|
@apply absolute inset-0 flex items-center justify-center bg-black bg-opacity-50;
|
||||||
transition: cursor 0.3s ease;
|
.n-button {
|
||||||
|
@apply text-white hover:text-green-500 transition-colors;
|
||||||
// 移动端适配
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.custom-controls {
|
|
||||||
.controls-main {
|
|
||||||
@apply flex-wrap gap-2 justify-center;
|
|
||||||
|
|
||||||
.left-controls,
|
|
||||||
.right-controls {
|
|
||||||
@apply w-full justify-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-display {
|
|
||||||
@apply order-first w-full text-center mb-2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调整标题样式
|
|
||||||
.mv-detail-title {
|
|
||||||
.title {
|
|
||||||
@apply text-base max-w-full;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调整进度条
|
|
||||||
.progress-bar {
|
|
||||||
@apply mb-2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.cursor-hidden {
|
.custom-controls {
|
||||||
* {
|
@apply absolute bottom-0 left-0 right-0 p-4 transition-opacity duration-300;
|
||||||
cursor: none !important;
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
|
||||||
}
|
|
||||||
|
|
||||||
// 控制栏区域保持鼠标可见
|
.controls-main {
|
||||||
.custom-controls {
|
@apply flex justify-between items-center;
|
||||||
* {
|
|
||||||
cursor: default !important;
|
.left-controls,
|
||||||
}
|
.right-controls {
|
||||||
|
@apply flex items-center gap-2;
|
||||||
|
|
||||||
.n-button {
|
.n-button {
|
||||||
cursor: pointer !important;
|
@apply text-white hover:text-green-500 transition-colors;
|
||||||
}
|
|
||||||
|
|
||||||
.n-slider {
|
|
||||||
cursor: pointer !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:fullscreen,
|
|
||||||
&:-webkit-full-screen,
|
|
||||||
&:-moz-full-screen,
|
|
||||||
&:-ms-fullscreen {
|
|
||||||
background: black;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
// 确保全屏时标题栏正确显示
|
|
||||||
.mv-detail-title {
|
|
||||||
@apply px-8 py-6;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
@apply text-xl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保全屏时控制栏正确显示
|
|
||||||
.custom-controls {
|
|
||||||
padding: 20px 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
@apply absolute inset-0 bg-black opacity-0 transition-opacity duration-200;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active::after {
|
|
||||||
@apply opacity-10;
|
|
||||||
}
|
|
||||||
|
|
||||||
video {
|
|
||||||
@apply w-full h-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-controls {
|
|
||||||
@apply absolute bottom-0 left-0 w-full transition-opacity duration-300 ease-in-out;
|
|
||||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);
|
|
||||||
padding: 16px 20px;
|
|
||||||
|
|
||||||
&.controls-hidden {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
@apply mb-4;
|
|
||||||
|
|
||||||
.progress-rail {
|
|
||||||
@apply relative w-full h-full;
|
|
||||||
|
|
||||||
.progress-buffer {
|
|
||||||
@apply absolute h-full bg-gray-600 rounded-full;
|
|
||||||
transition: width 0.2s ease;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-main {
|
|
||||||
@apply flex justify-between items-center;
|
|
||||||
|
|
||||||
.left-controls,
|
|
||||||
.right-controls {
|
|
||||||
@apply flex items-center gap-4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-display {
|
.time-display {
|
||||||
@apply text-sm text-white ml-2;
|
@apply text-white text-sm ml-4;
|
||||||
}
|
|
||||||
|
|
||||||
.volume-control {
|
|
||||||
@apply flex items-center gap-2;
|
|
||||||
|
|
||||||
.volume-slider {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-button {
|
|
||||||
@apply text-white;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@apply text-green-400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-hint {
|
|
||||||
@apply absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 cursor-pointer;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
.n-button {
|
|
||||||
@apply text-white opacity-80 transform transition-all duration-300;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@apply opacity-100 scale-110;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mv-detail-title {
|
|
||||||
@apply absolute w-full left-0 top-0 px-6 py-4 transition-opacity duration-300 z-50;
|
|
||||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);
|
|
||||||
|
|
||||||
&.title-hidden {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
@apply text-white text-lg font-medium;
|
|
||||||
max-width: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-slider {
|
|
||||||
:deep(.n-slider) {
|
|
||||||
--n-rail-height: 4px;
|
|
||||||
--n-rail-color: rgba(255, 255, 255, 0.2);
|
|
||||||
--n-fill-color: var(--primary-color);
|
|
||||||
--n-handle-size: 12px;
|
|
||||||
--n-handle-color: var(--primary-color);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
--n-rail-height: 6px;
|
|
||||||
--n-handle-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-slider-rail {
|
|
||||||
@apply overflow-hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-slider-handle {
|
|
||||||
@apply transition-opacity duration-200;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .n-slider-handle {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--primary-color: #18a058;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加模式提示样式
|
|
||||||
.mode-hint {
|
.mode-hint {
|
||||||
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2;
|
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center;
|
||||||
@apply flex flex-col items-center justify-center;
|
|
||||||
@apply bg-black bg-opacity-70 rounded-lg p-4;
|
|
||||||
z-index: 20;
|
|
||||||
|
|
||||||
.mode-icon {
|
.mode-icon {
|
||||||
@apply text-white mb-2;
|
@apply text-white mb-2;
|
||||||
@@ -779,7 +640,49 @@ const isMobile = computed(() => store.state.isMobile);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加过渡动画
|
.custom-slider {
|
||||||
|
:deep(.n-slider) {
|
||||||
|
--n-rail-height: 4px;
|
||||||
|
--n-rail-color: rgba(255, 255, 255, 0.2);
|
||||||
|
--n-fill-color: #10b981;
|
||||||
|
--n-handle-size: 12px;
|
||||||
|
--n-handle-color: #10b981;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
@apply mb-4;
|
||||||
|
|
||||||
|
.progress-rail {
|
||||||
|
@apply relative w-full h-1 bg-gray-600;
|
||||||
|
|
||||||
|
.progress-buffer {
|
||||||
|
@apply absolute top-0 left-0 h-full bg-gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-control {
|
||||||
|
@apply flex items-center gap-2;
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-hidden {
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
@@ -789,79 +692,4 @@ const isMobile = computed(() => store.state.isMobile);
|
|||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加 tooltip 样式
|
|
||||||
:deep(.n-tooltip) {
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调左侧控制按钮的样式
|
|
||||||
.left-controls {
|
|
||||||
@apply flex items-center gap-2;
|
|
||||||
|
|
||||||
.time-display {
|
|
||||||
@apply text-sm text-white ml-4; // 增加时间显示的左边距
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 可以添加按钮禁用状态的样式
|
|
||||||
:deep(.n-button--disabled) {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加加载动画样式
|
|
||||||
:deep(.n-spin) {
|
|
||||||
.n-spin-body {
|
|
||||||
@apply text-white;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加视频播放器样式
|
|
||||||
.video-player {
|
|
||||||
@apply w-full h-full cursor-pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加点击反馈效果
|
|
||||||
.video-container {
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
@apply absolute inset-0 bg-black opacity-0 transition-opacity duration-200;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active::after {
|
|
||||||
@apply opacity-10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加鼠标隐藏样式
|
|
||||||
.video-container {
|
|
||||||
@apply w-full h-full relative;
|
|
||||||
transition: cursor 0.3s ease;
|
|
||||||
|
|
||||||
&.cursor-hidden {
|
|
||||||
* {
|
|
||||||
cursor: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 控制栏区域保持鼠标可见
|
|
||||||
.custom-controls {
|
|
||||||
* {
|
|
||||||
cursor: default !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-button {
|
|
||||||
cursor: pointer !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-slider {
|
|
||||||
cursor: pointer !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
? 'animate__bounceIn'
|
? 'animate__bounceIn'
|
||||||
: !isShowAllPlaylistCategory
|
: !isShowAllPlaylistCategory
|
||||||
? 'animate__backOutLeft'
|
? 'animate__backOutLeft'
|
||||||
: 'animate__bounceIn',
|
: 'animate__bounceIn'
|
||||||
) +
|
) +
|
||||||
' ' +
|
' ' +
|
||||||
'type-item-' +
|
'type-item-' +
|
||||||
@@ -27,7 +27,11 @@
|
|||||||
<div
|
<div
|
||||||
class="play-list-type-showall"
|
class="play-list-type-showall"
|
||||||
:class="setAnimationClass('animate__bounceIn')"
|
:class="setAnimationClass('animate__bounceIn')"
|
||||||
:style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
|
:style="
|
||||||
|
setAnimationDelay(
|
||||||
|
!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30
|
||||||
|
)
|
||||||
|
"
|
||||||
@click="handleToggleShowAllPlaylistCategory"
|
@click="handleToggleShowAllPlaylistCategory"
|
||||||
>
|
>
|
||||||
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
|
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
|
||||||
@@ -63,8 +67,8 @@ const getAnimationDelay = computed(() => {
|
|||||||
|
|
||||||
watch(isShowAllPlaylistCategory, (newVal) => {
|
watch(isShowAllPlaylistCategory, (newVal) => {
|
||||||
if (!newVal) {
|
if (!newVal) {
|
||||||
const elements = playlistCategory.value?.sub.map((item, index) =>
|
const elements = playlistCategory.value?.sub.map((_, index) =>
|
||||||
document.querySelector(`.type-item-${index}`),
|
document.querySelector(`.type-item-${index}`)
|
||||||
) as HTMLElement[];
|
) as HTMLElement[];
|
||||||
elements
|
elements
|
||||||
.slice(20)
|
.slice(20)
|
||||||
@@ -75,7 +79,7 @@ watch(isShowAllPlaylistCategory, (newVal) => {
|
|||||||
() => {
|
() => {
|
||||||
(element as HTMLElement).style.position = 'absolute';
|
(element as HTMLElement).style.position = 'absolute';
|
||||||
},
|
},
|
||||||
index * DELAY_TIME + 400,
|
index * DELAY_TIME + 400
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -90,7 +94,7 @@ watch(isShowAllPlaylistCategory, (newVal) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(playlistCategory.value?.sub.length || 0 - 19) * DELAY_TIME,
|
(playlistCategory.value?.sub.length || 0 - 19) * DELAY_TIME
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
document.querySelectorAll('.play-list-type-item').forEach((element) => {
|
document.querySelectorAll('.play-list-type-item').forEach((element) => {
|
||||||
@@ -112,8 +116,8 @@ const handleClickPlaylistType = (type: string) => {
|
|||||||
router.push({
|
router.push({
|
||||||
path: '/list',
|
path: '/list',
|
||||||
query: {
|
query: {
|
||||||
type,
|
type
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,15 +136,15 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.title {
|
.title {
|
||||||
@apply text-lg font-bold mb-4;
|
@apply text-lg font-bold mb-4 text-gray-900 dark:text-white;
|
||||||
}
|
}
|
||||||
.play-list-type {
|
.play-list-type {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
@apply mx-6;
|
@apply mr-4;
|
||||||
&-item,
|
&-item,
|
||||||
&-showall {
|
&-showall {
|
||||||
@apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 transition;
|
@apply bg-light dark:bg-black text-gray-900 dark:text-white;
|
||||||
background-color: #1a1a1a;
|
@apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-200 dark:border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 hover:text-white transition;
|
||||||
}
|
}
|
||||||
&-showall {
|
&-showall {
|
||||||
@apply block text-center;
|
@apply block text-center;
|
||||||
@@ -38,6 +38,7 @@ import { getNewAlbum } from '@/api/home';
|
|||||||
import { getAlbum } from '@/api/list';
|
import { getAlbum } from '@/api/list';
|
||||||
import type { IAlbumNew } from '@/type/album';
|
import type { IAlbumNew } from '@/type/album';
|
||||||
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
|
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
|
import MusicList from '@/components/MusicList.vue';
|
||||||
|
|
||||||
const albumData = ref<IAlbumNew>();
|
const albumData = ref<IAlbumNew>();
|
||||||
const loadAlbumList = async () => {
|
const loadAlbumList = async () => {
|
||||||
@@ -65,9 +66,9 @@ const handleClick = async (item: any) => {
|
|||||||
...res.data.album,
|
...res.data.album,
|
||||||
creator: {
|
creator: {
|
||||||
avatarUrl: res.data.album.artist.img1v1Url,
|
avatarUrl: res.data.album.artist.img1v1Url,
|
||||||
nickname: `${res.data.album.artist.name} - ${res.data.album.company}`,
|
nickname: `${res.data.album.artist.name} - ${res.data.album.company}`
|
||||||
},
|
},
|
||||||
description: res.data.album.description,
|
description: res.data.album.description
|
||||||
};
|
};
|
||||||
loadingList.value = false;
|
loadingList.value = false;
|
||||||
};
|
};
|
||||||
@@ -81,7 +82,7 @@ onMounted(() => {
|
|||||||
.recommend-album {
|
.recommend-album {
|
||||||
@apply flex-1 mx-5;
|
@apply flex-1 mx-5;
|
||||||
.title {
|
.title {
|
||||||
@apply text-lg font-bold mb-4;
|
@apply text-lg font-bold mb-4 text-gray-900 dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recommend-album-list {
|
.recommend-album-list {
|
||||||
@@ -95,7 +96,7 @@ onMounted(() => {
|
|||||||
filter: brightness(50%);
|
filter: brightness(50%);
|
||||||
}
|
}
|
||||||
&-content {
|
&-content {
|
||||||
@apply w-full h-full opacity-0 transition absolute z-10 top-0 left-0 p-4 text-xl bg-opacity-60 bg-black;
|
@apply w-full h-full opacity-0 transition absolute z-10 top-0 left-0 p-4 text-xl text-white bg-opacity-60 bg-black dark:bg-opacity-60 dark:bg-black;
|
||||||
}
|
}
|
||||||
&-content:hover {
|
&-content:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -10,7 +10,9 @@
|
|||||||
:style="setAnimationDelay(0, 100)"
|
:style="setAnimationDelay(0, 100)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:style="setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '300y300'))"
|
:style="
|
||||||
|
setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '500y500'))
|
||||||
|
"
|
||||||
class="recommend-singer-item-bg"
|
class="recommend-singer-item-bg"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
@@ -20,7 +22,11 @@
|
|||||||
<div class="font-bold text-xl">每日推荐</div>
|
<div class="font-bold text-xl">每日推荐</div>
|
||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<p v-for="item in dayRecommendData?.dailySongs.slice(0, 5)" :key="item.id" class="text-el">
|
<p
|
||||||
|
v-for="item in dayRecommendData?.dailySongs.slice(0, 5)"
|
||||||
|
:key="item.id"
|
||||||
|
class="text-el"
|
||||||
|
>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
<br />
|
<br />
|
||||||
</p>
|
</p>
|
||||||
@@ -34,8 +40,13 @@
|
|||||||
:class="setAnimationClass('animate__backInRight')"
|
:class="setAnimationClass('animate__backInRight')"
|
||||||
:style="setAnimationDelay(index + 1, 100)"
|
:style="setAnimationDelay(index + 1, 100)"
|
||||||
>
|
>
|
||||||
<div :style="setBackgroundImg(getImgUrl(item.picUrl, '300y300'))" class="recommend-singer-item-bg"></div>
|
<div
|
||||||
<div class="recommend-singer-item-count p-2 text-base text-gray-200 z-10">{{ item.musicSize }}首</div>
|
:style="setBackgroundImg(getImgUrl(item.picUrl, '500y500'))"
|
||||||
|
class="recommend-singer-item-bg"
|
||||||
|
></div>
|
||||||
|
<div class="recommend-singer-item-count p-2 text-base text-gray-200 z-10">
|
||||||
|
{{ item.musicSize }}首
|
||||||
|
</div>
|
||||||
<div class="recommend-singer-item-info z-10">
|
<div class="recommend-singer-item-info z-10">
|
||||||
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
|
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
|
||||||
<i class="iconfont icon-playfill text-xl"></i>
|
<i class="iconfont icon-playfill text-xl"></i>
|
||||||
@@ -68,6 +79,7 @@ import router from '@/router';
|
|||||||
import { IDayRecommend } from '@/type/day_recommend';
|
import { IDayRecommend } from '@/type/day_recommend';
|
||||||
import type { IHotSinger } from '@/type/singer';
|
import type { IHotSinger } from '@/type/singer';
|
||||||
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
|
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
|
||||||
|
import MusicList from '@/components/MusicList.vue';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
@@ -88,13 +100,13 @@ const loadData = async () => {
|
|||||||
// 第二个请求:获取每日推荐
|
// 第二个请求:获取每日推荐
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { data: dayRecommend },
|
data: { data: dayRecommend }
|
||||||
} = await getDayRecommend();
|
} = await getDayRecommend();
|
||||||
// 处理数据
|
// 处理数据
|
||||||
if (dayRecommend) {
|
if (dayRecommend) {
|
||||||
singerData.artists = singerData.artists.slice(0, 4);
|
singerData.artists = singerData.artists.slice(0, 4);
|
||||||
}
|
}
|
||||||
dayRecommendData.value = dayRecommend;
|
dayRecommendData.value = dayRecommend as unknown as IDayRecommend;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error);
|
console.error('error', error);
|
||||||
}
|
}
|
||||||
@@ -109,8 +121,8 @@ const toSearchSinger = (keyword: string) => {
|
|||||||
router.push({
|
router.push({
|
||||||
path: '/search',
|
path: '/search',
|
||||||
query: {
|
query: {
|
||||||
keyword,
|
keyword
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,14 +143,20 @@ watchEffect(() => {
|
|||||||
&-item {
|
&-item {
|
||||||
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between overflow-hidden;
|
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between overflow-hidden;
|
||||||
&-bg {
|
&-bg {
|
||||||
@apply bg-gray-900 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
|
@apply bg-gray-900 dark:bg-gray-800 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
|
||||||
filter: brightness(60%);
|
filter: brightness(60%);
|
||||||
}
|
}
|
||||||
&-info {
|
&-info {
|
||||||
@apply flex items-center p-2;
|
@apply flex items-center p-2;
|
||||||
&-play {
|
&-play {
|
||||||
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer;
|
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer text-white;
|
||||||
}
|
}
|
||||||
|
&-name {
|
||||||
|
@apply text-gray-100 dark:text-gray-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-count {
|
||||||
|
@apply text-gray-100 dark:text-gray-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,10 @@
|
|||||||
>
|
>
|
||||||
<!-- 推荐音乐列表 -->
|
<!-- 推荐音乐列表 -->
|
||||||
<template v-for="(item, index) in recommendMusic?.result" :key="item.id">
|
<template v-for="(item, index) in recommendMusic?.result" :key="item.id">
|
||||||
<div :class="setAnimationClass('animate__bounceInUp')" :style="setAnimationDelay(index, 100)">
|
<div
|
||||||
|
:class="setAnimationClass('animate__bounceInUp')"
|
||||||
|
:style="setAnimationDelay(index, 100)"
|
||||||
|
>
|
||||||
<song-item :item="item" @play="handlePlay" />
|
<song-item :item="item" @play="handlePlay" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -51,17 +54,15 @@ const handlePlay = () => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.title {
|
.title {
|
||||||
@apply text-lg font-bold mb-4;
|
@apply text-lg font-bold mb-4 text-gray-900 dark:text-white;
|
||||||
}
|
}
|
||||||
.recommend-music {
|
.recommend-music {
|
||||||
@apply flex-auto;
|
@apply flex-auto;
|
||||||
// width: 530px;
|
|
||||||
.text-ellipsis {
|
.text-ellipsis {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
&-list {
|
&-list {
|
||||||
@apply rounded-3xl p-2 w-full border border-gray-700;
|
@apply rounded-3xl p-2 w-full border border-gray-200 dark:border-gray-700 bg-light dark:bg-black;
|
||||||
background-color: #0d0d0d;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-modal v-model:show="showModal" preset="dialog" :show-icon="false" :mask-closable="true" class="install-app-modal">
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="dialog"
|
||||||
|
:show-icon="false"
|
||||||
|
:mask-closable="true"
|
||||||
|
class="install-app-modal"
|
||||||
|
>
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="app-icon">
|
<div class="app-icon">
|
||||||
@@ -18,7 +24,10 @@
|
|||||||
<div class="modal-desc mt-4 text-center">
|
<div class="modal-desc mt-4 text-center">
|
||||||
<p class="text-xs text-gray-400">
|
<p class="text-xs text-gray-400">
|
||||||
下载遇到问题?去
|
下载遇到问题?去
|
||||||
<a class="text-green-500" target="_blank" href="https://github.com/algerkong/AlgerMusicPlayer/releases"
|
<a
|
||||||
|
class="text-green-500"
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/algerkong/AlgerMusicPlayer/releases"
|
||||||
>GitHub</a
|
>GitHub</a
|
||||||
>
|
>
|
||||||
下载最新版本
|
下载最新版本
|
||||||
@@ -31,11 +40,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import config from '@/../package.json';
|
import { isElectron, isMobile } from '@/utils';
|
||||||
import { isMobile } from '@/utils';
|
|
||||||
|
import config from '../../../../package.json';
|
||||||
|
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const isElectron = ref((window as any).electron !== undefined);
|
|
||||||
const noPrompt = ref(false);
|
const noPrompt = ref(false);
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
@@ -47,7 +56,7 @@ const closeModal = () => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 如果是 electron 环境,不显示安装提示
|
// 如果是 electron 环境,不显示安装提示
|
||||||
if (isElectron.value || isMobile.value) {
|
if (isElectron || isMobile.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +73,13 @@ const handleInstall = async (): Promise<void> => {
|
|||||||
console.log('userAgent', userAgent);
|
console.log('userAgent', userAgent);
|
||||||
const isMac: boolean = userAgent.includes('Mac');
|
const isMac: boolean = userAgent.includes('Mac');
|
||||||
const isWindows: boolean = userAgent.includes('Win');
|
const isWindows: boolean = userAgent.includes('Win');
|
||||||
const isARM: boolean = userAgent.includes('ARM') || userAgent.includes('arm') || userAgent.includes('OS X');
|
const isARM: boolean =
|
||||||
const isX64: boolean = userAgent.includes('x86_64') || userAgent.includes('Win64') || userAgent.includes('WOW64');
|
userAgent.includes('ARM') || userAgent.includes('arm') || userAgent.includes('OS X');
|
||||||
|
const isX64: boolean =
|
||||||
|
userAgent.includes('x86_64') || userAgent.includes('Win64') || userAgent.includes('WOW64');
|
||||||
const isX86: boolean =
|
const isX86: boolean =
|
||||||
!isX64 && (userAgent.includes('i686') || userAgent.includes('i386') || userAgent.includes('Win32'));
|
!isX64 &&
|
||||||
|
(userAgent.includes('i686') || userAgent.includes('i386') || userAgent.includes('Win32'));
|
||||||
|
|
||||||
const getDownloadUrl = (os: string, arch: string): string => {
|
const getDownloadUrl = (os: string, arch: string): string => {
|
||||||
const version = config.version as string;
|
const version = config.version as string;
|
||||||
@@ -4,12 +4,12 @@ import { setAnimationClass } from '@/utils';
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
showPop: {
|
showPop: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false
|
||||||
},
|
},
|
||||||
showClose: {
|
showClose: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const musicFullClass = computed(() => {
|
const musicFullClass = computed(() => {
|
||||||
@@ -10,8 +10,8 @@ const isPlay = computed(() => store.state.isPlay as boolean);
|
|||||||
defineProps({
|
defineProps({
|
||||||
height: {
|
height: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search-item" :class="item.type" @click="handleClick">
|
<div class="search-item" :class="item.type" @click="handleClick">
|
||||||
<div class="search-item-img">
|
<div class="search-item-img">
|
||||||
<n-image :src="getImgUrl(item.picUrl, '320y180')" lazy preview-disabled />
|
<n-image
|
||||||
|
:src="getImgUrl(item.picUrl, item.type === 'mv' ? '320y180' : '100y100')"
|
||||||
|
lazy
|
||||||
|
preview-disabled
|
||||||
|
/>
|
||||||
<div v-if="item.type === 'mv'" class="play">
|
<div v-if="item.type === 'mv'" class="play">
|
||||||
<i class="iconfont icon icon-play"></i>
|
<i class="iconfont icon icon-play"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,8 +21,14 @@
|
|||||||
:name="item.name"
|
:name="item.name"
|
||||||
:song-list="songList"
|
:song-list="songList"
|
||||||
:list-info="listInfo"
|
:list-info="listInfo"
|
||||||
|
:cover="false"
|
||||||
|
/>
|
||||||
|
<mv-player
|
||||||
|
v-if="item.type === 'mv'"
|
||||||
|
v-model:show="showPop"
|
||||||
|
:current-mv="getCurrentMv()"
|
||||||
|
no-list
|
||||||
/>
|
/>
|
||||||
<mv-player v-if="item.type === 'mv'" v-model:show="showPop" :current-mv="getCurrentMv()" no-list />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -30,6 +40,7 @@ import MvPlayer from '@/components/MvPlayer.vue';
|
|||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
import { IMvItem } from '@/type/mv';
|
import { IMvItem } from '@/type/mv';
|
||||||
import { getImgUrl } from '@/utils';
|
import { getImgUrl } from '@/utils';
|
||||||
|
import MusicList from '../MusicList.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
item: {
|
item: {
|
||||||
@@ -49,7 +60,7 @@ const listInfo = ref<any>(null);
|
|||||||
const getCurrentMv = () => {
|
const getCurrentMv = () => {
|
||||||
return {
|
return {
|
||||||
id: props.item.id,
|
id: props.item.id,
|
||||||
name: props.item.name,
|
name: props.item.name
|
||||||
} as unknown as IMvItem;
|
} as unknown as IMvItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,6 +75,14 @@ const handleClick = async () => {
|
|||||||
song.al.picUrl = song.al.picUrl || props.item.picUrl;
|
song.al.picUrl = song.al.picUrl || props.item.picUrl;
|
||||||
return song;
|
return song;
|
||||||
});
|
});
|
||||||
|
listInfo.value = {
|
||||||
|
...res.data.album,
|
||||||
|
creator: {
|
||||||
|
avatarUrl: res.data.album.artist.img1v1Url,
|
||||||
|
nickname: `${res.data.album.artist.name} - ${res.data.album.company}`
|
||||||
|
},
|
||||||
|
description: res.data.album.description
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.item.type === 'playlist') {
|
if (props.item.type === 'playlist') {
|
||||||
@@ -84,7 +103,7 @@ const handleClick = async () => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.search-item {
|
.search-item {
|
||||||
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition cursor-pointer;
|
@apply rounded-3xl p-3 flex items-center hover:bg-light-200 dark:hover:bg-gray-800 transition cursor-pointer;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
.search-item-img {
|
.search-item-img {
|
||||||
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
|
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
|
||||||
@@ -3,21 +3,24 @@
|
|||||||
<n-image
|
<n-image
|
||||||
v-if="item.picUrl"
|
v-if="item.picUrl"
|
||||||
ref="songImg"
|
ref="songImg"
|
||||||
:src="getImgUrl(item.picUrl, '40y40')"
|
:src="getImgUrl(item.picUrl, '100y100')"
|
||||||
class="song-item-img"
|
class="song-item-img"
|
||||||
preview-disabled
|
preview-disabled
|
||||||
:img-props="{
|
:img-props="{
|
||||||
crossorigin: 'anonymous',
|
crossorigin: 'anonymous'
|
||||||
}"
|
}"
|
||||||
@load="imageLoad"
|
@load="imageLoad"
|
||||||
/>
|
/>
|
||||||
<div class="song-item-content">
|
<div class="song-item-content">
|
||||||
<div v-if="list" class="song-item-content-wrapper">
|
<div v-if="list" class="song-item-content-wrapper">
|
||||||
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
|
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1">{{
|
||||||
|
item.name
|
||||||
|
}}</n-ellipsis>
|
||||||
<div class="song-item-content-divider">-</div>
|
<div class="song-item-content-divider">-</div>
|
||||||
<n-ellipsis class="song-item-content-name text-ellipsis" line-clamp="1">
|
<n-ellipsis class="song-item-content-name text-ellipsis" line-clamp="1">
|
||||||
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
||||||
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
>{{ artists.name
|
||||||
|
}}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
||||||
>
|
>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,8 +30,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="song-item-content-name">
|
<div class="song-item-content-name">
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||||
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
<span
|
||||||
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
v-for="(artists, artistsindex) in item.ar || item.song.artists"
|
||||||
|
:key="artistsindex"
|
||||||
|
>{{ artists.name
|
||||||
|
}}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
||||||
>
|
>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,10 +42,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="song-item-operating" :class="{ 'song-item-operating-list': list }">
|
<div class="song-item-operating" :class="{ 'song-item-operating-list': list }">
|
||||||
<div v-if="favorite" class="song-item-operating-like">
|
<div v-if="favorite" class="song-item-operating-like">
|
||||||
<i class="iconfont icon-likefill" :class="{ 'like-active': isFavorite }" @click.stop="toggleFavorite"></i>
|
<i
|
||||||
|
class="iconfont icon-likefill"
|
||||||
|
:class="{ 'like-active': isFavorite }"
|
||||||
|
@click.stop="toggleFavorite"
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="song-item-operating-play bg-black animate__animated"
|
class="song-item-operating-play bg-gray-300 dark:bg-gray-800 animate__animated"
|
||||||
:class="{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }"
|
:class="{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }"
|
||||||
@click="playMusicEvent(item)"
|
@click="playMusicEvent(item)"
|
||||||
>
|
>
|
||||||
@@ -69,8 +79,8 @@ const props = withDefaults(
|
|||||||
{
|
{
|
||||||
mini: false,
|
mini: false,
|
||||||
list: false,
|
list: false,
|
||||||
favorite: true,
|
favorite: true
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -79,7 +89,9 @@ const play = computed(() => store.state.play as boolean);
|
|||||||
|
|
||||||
const playMusic = computed(() => store.state.playMusic);
|
const playMusic = computed(() => store.state.playMusic);
|
||||||
|
|
||||||
const playLoading = computed(() => playMusic.value.id === props.item.id && playMusic.value.playLoading);
|
const playLoading = computed(
|
||||||
|
() => playMusic.value.id === props.item.id && playMusic.value.playLoading
|
||||||
|
);
|
||||||
|
|
||||||
// 判断是否为正在播放的音乐
|
// 判断是否为正在播放的音乐
|
||||||
const isPlaying = computed(() => {
|
const isPlaying = computed(() => {
|
||||||
@@ -95,7 +107,7 @@ const imageLoad = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { backgroundColor } = await getImageBackground(
|
const { backgroundColor } = await getImageBackground(
|
||||||
(songImageRef.value as any).imageRef as unknown as HTMLImageElement,
|
(songImageRef.value as any).imageRef as unknown as HTMLImageElement
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line vue/no-mutating-props
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
props.item.backgroundColor = backgroundColor;
|
props.item.backgroundColor = backgroundColor;
|
||||||
@@ -139,72 +151,98 @@ const toggleFavorite = async (e: Event) => {
|
|||||||
.text-ellipsis {
|
.text-ellipsis {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-item {
|
.song-item {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition;
|
@apply rounded-3xl p-3 flex items-center transition bg-transparent dark:text-white text-gray-900;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-gray-100 dark:bg-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
&-img {
|
&-img {
|
||||||
@apply w-12 h-12 rounded-2xl mr-4;
|
@apply w-12 h-12 rounded-2xl mr-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
@apply flex-1;
|
@apply flex-1;
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
@apply text-base text-white;
|
@apply text-base text-gray-900 dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-name {
|
&-name {
|
||||||
@apply text-xs;
|
@apply text-xs text-gray-500 dark:text-gray-400;
|
||||||
@apply text-gray-400;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-operating {
|
&-operating {
|
||||||
@apply flex items-center rounded-full border border-gray-700 ml-4;
|
@apply flex items-center rounded-full ml-4 border dark:border-gray-700 border-gray-200 bg-light dark:bg-black;
|
||||||
background-color: #0d0d0d;
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@apply text-xl;
|
@apply text-xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-likefill {
|
.icon-likefill {
|
||||||
color: #868686;
|
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-red-500;
|
||||||
@apply text-xl hover:text-red-600 transition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-like {
|
&-like {
|
||||||
@apply mr-2 cursor-pointer ml-4;
|
@apply mr-2 cursor-pointer ml-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.like-active {
|
.like-active {
|
||||||
@apply text-red-600;
|
@apply text-red-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-play {
|
&-play {
|
||||||
@apply cursor-pointer border border-gray-500 rounded-full w-10 h-10 flex justify-center items-center hover:bg-green-600 transition;
|
@apply cursor-pointer rounded-full w-10 h-10 flex justify-center items-center transition
|
||||||
animation-iteration-count: infinite;
|
border dark:border-gray-700 border-gray-200 text-gray-900 dark:text-white;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.bg-green-600 {
|
||||||
|
@apply bg-green-500 border-green-500 text-white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-mini {
|
.song-mini {
|
||||||
@apply p-2 rounded-2xl;
|
@apply p-2 rounded-2xl;
|
||||||
|
|
||||||
.song-item {
|
.song-item {
|
||||||
@apply p-0;
|
@apply p-0;
|
||||||
|
|
||||||
&-img {
|
&-img {
|
||||||
@apply w-10 h-10 mr-2;
|
@apply w-10 h-10 mr-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
@apply flex-1;
|
@apply flex-1;
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
@apply text-sm;
|
@apply text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-name {
|
&-name {
|
||||||
@apply text-xs;
|
@apply text-xs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-operating {
|
&-operating {
|
||||||
@apply pl-2;
|
@apply pl-2;
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@apply text-base;
|
@apply text-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-like {
|
&-like {
|
||||||
@apply mr-1 ml-1;
|
@apply mr-1 ml-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-play {
|
&-play {
|
||||||
@apply w-8 h-8;
|
@apply w-8 h-8;
|
||||||
}
|
}
|
||||||
@@ -213,35 +251,50 @@ const toggleFavorite = async (e: Event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.song-list {
|
.song-list {
|
||||||
@apply p-2 rounded-lg hover:bg-gray-800/50 border border-gray-800/50 mb-2;
|
@apply p-2 rounded-lg mb-2 border dark:border-gray-800 border-gray-200;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-gray-50 dark:bg-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
.song-item-img {
|
.song-item-img {
|
||||||
@apply w-10 h-10 rounded-lg mr-3;
|
@apply w-10 h-10 rounded-lg mr-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-item-content {
|
.song-item-content {
|
||||||
@apply flex items-center flex-1;
|
@apply flex items-center flex-1;
|
||||||
|
|
||||||
&-wrapper {
|
&-wrapper {
|
||||||
@apply flex items-center flex-1 text-sm;
|
@apply flex items-center flex-1 text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
@apply text-white flex-shrink-0 max-w-[45%];
|
@apply flex-shrink-0 max-w-[45%] text-gray-900 dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-divider {
|
&-divider {
|
||||||
@apply mx-2 text-gray-400;
|
@apply mx-2 text-gray-500 dark:text-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-name {
|
&-name {
|
||||||
@apply text-gray-400 flex-1 min-w-0;
|
@apply flex-1 min-w-0 text-gray-500 dark:text-gray-400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-item-operating {
|
.song-item-operating {
|
||||||
@apply flex items-center gap-2;
|
@apply flex items-center gap-2;
|
||||||
|
|
||||||
&-like {
|
&-like {
|
||||||
@apply cursor-pointer hover:scale-110 transition-transform;
|
@apply cursor-pointer hover:scale-110 transition-transform;
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@apply text-base text-gray-400 hover:text-red-500;
|
@apply text-base text-gray-500 dark:text-gray-400 hover:text-red-500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-play {
|
&-play {
|
||||||
@apply w-7 h-7 cursor-pointer hover:scale-110 transition-transform;
|
@apply w-7 h-7 cursor-pointer hover:scale-110 transition-transform;
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@apply text-base;
|
@apply text-base;
|
||||||
}
|
}
|
||||||
248
src/renderer/components/common/UpdateModal.vue
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="dialog"
|
||||||
|
:show-icon="false"
|
||||||
|
:mask-closable="true"
|
||||||
|
class="update-app-modal"
|
||||||
|
style="width: 800px; max-width: 90vw"
|
||||||
|
>
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="app-icon">
|
||||||
|
<img src="@/assets/logo.png" alt="App Icon" />
|
||||||
|
</div>
|
||||||
|
<div class="app-info">
|
||||||
|
<h2 class="app-name">发现新版本 {{ updateInfo.latestVersion }}</h2>
|
||||||
|
<p class="app-desc mb-2">当前版本 {{ updateInfo.currentVersion }}</p>
|
||||||
|
<n-checkbox v-model:checked="noPrompt">不再提示</n-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="update-info">
|
||||||
|
<n-scrollbar style="max-height: 300px">
|
||||||
|
<div class="update-body" v-html="parsedReleaseNotes"></div>
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<n-button class="cancel-btn" @click="closeModal">暂不更新</n-button>
|
||||||
|
<n-button type="primary" class="update-btn" @click="handleUpdate">立即更新</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-desc mt-4 text-center">
|
||||||
|
<p class="text-xs text-gray-400">
|
||||||
|
下载遇到问题?去
|
||||||
|
<a
|
||||||
|
class="text-green-500"
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/algerkong/AlgerMusicPlayer/releases"
|
||||||
|
>GitHub</a
|
||||||
|
>
|
||||||
|
下载最新版本
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, computed } from 'vue';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import { checkUpdate, UpdateResult } from '@/utils/update';
|
||||||
|
import config from '../../../../package.json';
|
||||||
|
|
||||||
|
// 配置 marked
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true, // 支持 GitHub 风格的换行
|
||||||
|
gfm: true // 启用 GitHub 风格的 Markdown
|
||||||
|
});
|
||||||
|
|
||||||
|
const showModal = ref(false);
|
||||||
|
const noPrompt = ref(false);
|
||||||
|
const updateInfo = ref<UpdateResult>({
|
||||||
|
hasUpdate: false,
|
||||||
|
latestVersion: '',
|
||||||
|
currentVersion: config.version,
|
||||||
|
releaseInfo: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 解析 Markdown
|
||||||
|
const parsedReleaseNotes = computed(() => {
|
||||||
|
if (!updateInfo.value.releaseInfo?.body) return '';
|
||||||
|
try {
|
||||||
|
return marked.parse(updateInfo.value.releaseInfo.body);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing markdown:', error);
|
||||||
|
return updateInfo.value.releaseInfo.body;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
showModal.value = false;
|
||||||
|
if (noPrompt.value) {
|
||||||
|
localStorage.setItem('updatePromptDismissed', 'true');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkForUpdates = async () => {
|
||||||
|
try {
|
||||||
|
const result = await checkUpdate(config.version);
|
||||||
|
if (result) {
|
||||||
|
updateInfo.value = result;
|
||||||
|
if (localStorage.getItem('updatePromptDismissed') !== 'true') {
|
||||||
|
showModal.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查更新失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async () => {
|
||||||
|
const assets = updateInfo.value.releaseInfo?.assets || [];
|
||||||
|
const platform = window.electron.process.platform;
|
||||||
|
const arch = window.electron.ipcRenderer.sendSync('get-arch');
|
||||||
|
console.log(arch);
|
||||||
|
console.log(platform);
|
||||||
|
|
||||||
|
let downloadUrl = '';
|
||||||
|
|
||||||
|
// 根据平台和架构选择对应的安装包
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
// macOS
|
||||||
|
const macAsset = assets.find(asset =>
|
||||||
|
asset.name.includes('mac')
|
||||||
|
);
|
||||||
|
downloadUrl = macAsset?.browser_download_url || '';
|
||||||
|
} else if (platform === 'win32') {
|
||||||
|
// Windows
|
||||||
|
const winAsset = assets.find(asset =>
|
||||||
|
asset.name.includes('win') &&
|
||||||
|
(arch === 'x64' ? asset.name.includes('x64') : asset.name.includes('ia32'))
|
||||||
|
);
|
||||||
|
downloadUrl = winAsset?.browser_download_url || '';
|
||||||
|
} else if (platform === 'linux') {
|
||||||
|
// Linux
|
||||||
|
const linuxAsset = assets.find(asset =>
|
||||||
|
(asset.name.endsWith('.AppImage') || asset.name.endsWith('.deb')) &&
|
||||||
|
asset.name.includes('x64')
|
||||||
|
);
|
||||||
|
downloadUrl = linuxAsset?.browser_download_url || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadUrl) {
|
||||||
|
window.open(downloadUrl, '_blank');
|
||||||
|
} else {
|
||||||
|
// 如果没有找到对应的安装包,跳转到 release 页面
|
||||||
|
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkForUpdates();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.update-app-modal {
|
||||||
|
:deep(.n-modal) {
|
||||||
|
@apply max-w-4xl;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
@apply p-6 pb-4;
|
||||||
|
.modal-header {
|
||||||
|
@apply flex items-center mb-6;
|
||||||
|
.app-icon {
|
||||||
|
@apply w-24 h-24 mr-6 rounded-2xl overflow-hidden;
|
||||||
|
img {
|
||||||
|
@apply w-full h-full object-cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-info {
|
||||||
|
@apply flex-1;
|
||||||
|
.app-name {
|
||||||
|
@apply text-2xl font-bold mb-2;
|
||||||
|
}
|
||||||
|
.app-desc {
|
||||||
|
@apply text-base text-gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.update-info {
|
||||||
|
@apply mb-6 rounded-lg bg-gray-50 dark:bg-gray-800;
|
||||||
|
.update-title {
|
||||||
|
@apply text-base font-medium p-4 pb-2;
|
||||||
|
}
|
||||||
|
.update-body {
|
||||||
|
@apply p-4 pt-2 text-gray-600 dark:text-gray-300 rounded-lg overflow-hidden;
|
||||||
|
|
||||||
|
:deep(h1) {
|
||||||
|
@apply text-xl font-bold mb-3;
|
||||||
|
}
|
||||||
|
:deep(h2) {
|
||||||
|
@apply text-lg font-bold mb-3;
|
||||||
|
}
|
||||||
|
:deep(h3) {
|
||||||
|
@apply text-base font-bold mb-2;
|
||||||
|
}
|
||||||
|
:deep(p) {
|
||||||
|
@apply mb-3 leading-relaxed;
|
||||||
|
}
|
||||||
|
:deep(ul) {
|
||||||
|
@apply list-disc list-inside mb-3;
|
||||||
|
}
|
||||||
|
:deep(ol) {
|
||||||
|
@apply list-decimal list-inside mb-3;
|
||||||
|
}
|
||||||
|
:deep(li) {
|
||||||
|
@apply mb-2 leading-relaxed;
|
||||||
|
}
|
||||||
|
:deep(code) {
|
||||||
|
@apply px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200;
|
||||||
|
}
|
||||||
|
:deep(pre) {
|
||||||
|
@apply p-3 rounded bg-gray-100 dark:bg-gray-700 overflow-x-auto mb-3;
|
||||||
|
code {
|
||||||
|
@apply bg-transparent p-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(blockquote) {
|
||||||
|
@apply pl-4 border-l-4 border-gray-200 dark:border-gray-600 mb-3;
|
||||||
|
}
|
||||||
|
:deep(a) {
|
||||||
|
@apply text-green-500 hover:text-green-600 dark:hover:text-green-400;
|
||||||
|
}
|
||||||
|
:deep(hr) {
|
||||||
|
@apply my-4 border-gray-200 dark:border-gray-600;
|
||||||
|
}
|
||||||
|
:deep(table) {
|
||||||
|
@apply w-full mb-3;
|
||||||
|
th, td {
|
||||||
|
@apply px-3 py-2 border border-gray-200 dark:border-gray-600;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
@apply bg-gray-100 dark:bg-gray-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modal-actions {
|
||||||
|
@apply flex gap-4 mt-6;
|
||||||
|
.n-button {
|
||||||
|
@apply flex-1 text-base py-2;
|
||||||
|
}
|
||||||
|
.cancel-btn {
|
||||||
|
@apply bg-gray-800 text-gray-300 border-none;
|
||||||
|
&:hover {
|
||||||
|
@apply bg-gray-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.update-btn {
|
||||||
|
@apply bg-green-600 border-none;
|
||||||
|
&:hover {
|
||||||
|
@apply bg-green-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,22 +13,22 @@ export const USER_SET_OPTIONS = [
|
|||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
label: '退出登录',
|
label: '退出登录',
|
||||||
key: 'logout',
|
key: 'logout'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '设置',
|
label: '设置',
|
||||||
key: 'set',
|
key: 'set'
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SEARCH_TYPES = [
|
export const SEARCH_TYPES = [
|
||||||
{
|
{
|
||||||
label: '单曲',
|
label: '单曲',
|
||||||
key: 1,
|
key: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '专辑',
|
label: '专辑',
|
||||||
key: 10,
|
key: 10
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// label: '歌手',
|
// label: '歌手',
|
||||||
@@ -36,7 +36,7 @@ export const SEARCH_TYPES = [
|
|||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
label: '歌单',
|
label: '歌单',
|
||||||
key: 1000,
|
key: 1000
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// label: '用户',
|
// label: '用户',
|
||||||
@@ -44,8 +44,8 @@ export const SEARCH_TYPES = [
|
|||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
label: 'MV',
|
label: 'MV',
|
||||||
key: 1004,
|
key: 1004
|
||||||
},
|
}
|
||||||
// {
|
// {
|
||||||
// label: '歌词',
|
// label: '歌词',
|
||||||
// key: 1006,
|
// key: 1006,
|
||||||