Compare commits

...

18 Commits

Author SHA1 Message Date
alger
8dab799939 feat: 修改更新检查功能 2025-01-01 15:05:49 +08:00
alger
1ddbe6f24e feat: 修改 github action 2025-01-01 14:48:31 +08:00
alger
4d5bcba6c7 fix: update macOS build config 2025-01-01 14:42:19 +08:00
alger
f833306b60 feat: 修改 github action 2025-01-01 14:31:27 +08:00
alger
4d92ed9963 🐞 fix: 修复mac 安装包损坏问题 2025-01-01 14:19:44 +08:00
alger
a22285156a feat: 修改 github action 添加更新日志 2025-01-01 14:01:55 +08:00
alger
d1029f16d6 feat: 修改 github action 2025-01-01 13:42:08 +08:00
alger
4908555635 feat: 修改 github action 2025-01-01 13:34:36 +08:00
alger
750cf7a484 🐞 fix: 去除无用导入 2025-01-01 13:26:06 +08:00
alger
a334743f6f feat: 添加 github action 自动 打包 发布 2025-01-01 13:14:56 +08:00
alger
14747cac10 feat: 优化打包和版本更新功能 2025-01-01 13:12:46 +08:00
alger
cc239aeaba 📃 docs: 修改文档 2025-01-01 02:44:39 +08:00
alger
eeda296589 📃 docs: 修改文档 2025-01-01 02:43:00 +08:00
alger
edb7ea201c 🌈 style: 去除无用提交 2025-01-01 02:30:37 +08:00
alger
17d20fa299 🦄 refactor: 重构整个项目 优化打包 修改后台服务为本地运行 添加更新版本检测功能 2025-01-01 02:25:18 +08:00
alger
f8d421c9b1 🐞 fix: 修复web移动端 页面空白问题 (#24)
closed #24
2024-12-30 11:20:23 +08:00
alger
dfdf02a17f feat: 优化主题效果 添加展开 menu功能 优化图片清晰度 添加随机播放功能(#20) 2024-12-29 00:43:39 +08:00
alger
abdb2bcd50 feat: 新增主题色切换功能 默认为日间主题可切换夜间 (#19、#21)
fixes #19  #21
2024-12-28 16:43:52 +08:00
162 changed files with 7127 additions and 2701 deletions

View File

@@ -1,13 +0,0 @@
snapshot*
dist
lib
es
esm
node_modules
static
cypress
script/test/cypress
_site
temp*
static/
!.prettierrc.js

140
.eslintrc
View File

@@ -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
View 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
View File

@@ -17,4 +17,6 @@ dist.zip
bun.lockb
.env.*.local
.env.*.local
out

2
.npmrc
View File

@@ -1,2 +0,0 @@
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/

View File

@@ -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
View File

@@ -0,0 +1,21 @@
# 更新日志
## [v3.0.0] - 2024-03-21
### ✨ 新功能
- 新增自动更新检测功能
- 新增 GitHub Actions 自动构建和发布
- 新增主题色切换功能,支持日间/夜间模式 (#19, #21)
- 新增随机播放功能 (#20)
- 优化主题效果和图片清晰度
### 🏗️ 架构重构
- 重构整个项目架构
- 优化打包配置
- 修改后台服务为本地运行
- 优化项目结构
### 🐞 问题修复
- 修复 web 移动端页面空白问题 (#24)
- 修复无用导入问题
- 优化错误处理

201
LICENSE
View File

@@ -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.

View File

@@ -2,12 +2,13 @@
主要功能如下
- 音乐推荐
- 音乐播放
- 网易云登录
- 播放历史
- 桌面歌词
- 歌单 mv 搜索 专辑等功能
- 识别无法播放歌曲 并代理播放
- 识别无法播放歌曲 并解析播放
- 主题切换 更新检测
- 本地服务 不依赖线上服务
- 可听周杰伦(搜索专辑)
## 项目简介
@@ -19,9 +20,10 @@
QQ群:789288579
## 软件截图
![首页](./docs/img/image-7.png)
![歌词](./docs/img/image-6.png)
![搜索](./docs/img/image-8.png)
![首页](./docs/image.png)
![首页黑](./docs/image3.png)
![歌词](./docs/image1.png)
![桌面歌词](./docs/image2.png)
## 技术栈
@@ -30,16 +32,8 @@ QQ群:789288579
- TypeScript - JavaScript 的超集,添加了类型系统
- Electron - 跨平台桌面应用开发框架
- Vite - 下一代前端构建工具
### UI 框架
- Naive UI - 基于 Vue 3 的组件库
### 项目特点
- 完整的类型支持TypeScript
- 模块化设计
- 自动化组件和 API 导入
- 多平台支持Web、Desktop、Mobile Web
- 构建优化(代码分割、压缩)
## 咖啡☕️
| 微信 | 支付宝 |

148
app.js
View File

@@ -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
View File

@@ -6,70 +6,85 @@
// 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']
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')
export type {
Component,
ComponentPublicInstance,
ComputedRef,
DirectiveBinding,
ExtractDefaultPropTypes,
ExtractPropTypes,
ExtractPublicPropTypes,
InjectionKey,
PropType,
Ref,
MaybeRef,
MaybeRefOrGetter,
VNode,
WritableComputedRef
} from 'vue';
import('vue');
}

View 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

Binary file not shown.

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

13
build/installer.nsh Normal file
View 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

View File

@@ -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
}
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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
View File

@@ -2,19 +2,13 @@
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
export {};
/* prettier-ignore */
declare module 'vue' {
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']
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']
@@ -24,27 +18,16 @@ declare module 'vue' {
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']
NPagination: typeof import('naive-ui')['NPagination']
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']
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']
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
View File

@@ -0,0 +1,3 @@
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: electron-lan-file-updater

BIN
docs/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
docs/image1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
docs/image2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
docs/image3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

45
electron-builder.yml Normal file
View 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
View 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}`), '')
}
}
}
}
});

View File

@@ -1,11 +0,0 @@
module.exports = {
// 开发环境配置
development: {
mainPort: 4488,
lyricPort: 4488,
},
// 生产环境配置
production: {
distPath: '../dist',
},
};

View File

@@ -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);
},
},
});

View File

@@ -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;

View File

@@ -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>

View File

@@ -1,32 +1,48 @@
{
"name": "alger-music",
"version": "2.4.0",
"description": "这是一个用于音乐播放的应用程序。",
"name": "AlgerMusicPlayer",
"version": "3.0.0",
"description": "Alger Music Player",
"author": "Alger <algerkc@qq.com>",
"main": "app.js",
"main": "./out/main/index.js",
"homepage": "https://github.com/algerkong/AlgerMusicPlayer",
"scripts": {
"dev": "vite",
"build": "cross-env NODE_ENV=production vite build",
"serve": "vite preview",
"start": "cross-env NODE_ENV=development electron .",
"lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0",
"b:win:x64": "cross-env NODE_ENV=production electron-builder --config ./build/win64.json",
"b:win:x86": "cross-env NODE_ENV=production electron-builder --config ./build/win32.json",
"b:win:arm": "cross-env NODE_ENV=production electron-builder --config ./build/winarm64.json",
"b:mac": "cross-env NODE_ENV=production npm run build && electron-builder --config ./build/mac.json",
"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"
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"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": {
"@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",
"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",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^5.1.3",
"@types/howler": "^2.2.12",
"@types/node": "^20.14.8",
"@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/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/runtime-core": "^3.5.0",
"@vueuse/core": "^11.0.3",
@@ -34,33 +50,104 @@
"autoprefixer": "^10.4.20",
"axios": "^1.7.7",
"cross-env": "^7.0.3",
"electron": "^32.2.7",
"electron-builder": "^25.0.5",
"eslint": "^8.56.0",
"electron": "^31.0.2",
"electron-builder": "^24.13.3",
"electron-vite": "^2.3.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"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",
"howler": "^2.2.4",
"lodash": "^4.17.21",
"naive-ui": "^2.39.0",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"prettier": "^3.3.2",
"remixicon": "^4.2.0",
"sass": "^1.82.0",
"tailwindcss": "^3.4.15",
"typescript": "^5.5.4",
"typescript": "^5.5.2",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.4",
"vfonts": "^0.1.0",
"vite": "^5.4.3",
"vite": "^5.3.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "7.4.0",
"vue": "^3.5.0",
"vue": "^3.4.30",
"vue-router": "^4.4.3",
"vue-tsc": "^2.1.4",
"vue-tsc": "^2.0.22",
"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"
}
}
}

View File

@@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
autoprefixer: {}
}
};

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
body{
background-color: #000;
}
.n-popover:has(.music-play){
border-radius: 1.5rem !important;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

View File

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 626 B

View File

@@ -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
View File

@@ -1,10 +0,0 @@
declare global {
interface Window {
electronAPI: {
minimize: () => void;
maximize: () => void;
close: () => void;
dragStart: () => void;
};
}
}

View File

@@ -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;
}

View File

@@ -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
View 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.

View File

@@ -1,23 +1,29 @@
const { BrowserWindow, screen } = require('electron');
const path = require('path');
const Store = require('electron-store');
const config = require('./config');
import { BrowserWindow, IpcMain, screen } from 'electron';
import Store from 'electron-store';
import path, { join } from 'path';
const store = new Store();
let lyricWindow = null;
let lyricWindow: BrowserWindow | null = null;
const createWin = () => {
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 { 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({
width: width || 800,
@@ -30,63 +36,79 @@ const createWin = () => {
hasShadow: false,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: `${__dirname}/preload.js`,
webSecurity: false,
},
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
contextIsolation: true
}
});
// 监听窗口关闭事件
lyricWindow.on('closed', () => {
console.log('Lyric window closed');
lyricWindow = null;
if (lyricWindow) {
lyricWindow.destroy();
lyricWindow = null;
}
});
return lyricWindow;
};
const loadLyricWindow = (ipcMain, mainWin) => {
ipcMain.on('open-lyric', () => {
console.log('Received open-lyric request');
if (lyricWindow) {
console.log('Lyric window exists, focusing');
if (lyricWindow.isMinimized()) lyricWindow.restore();
export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void => {
const showLyricWindow = () => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
if (lyricWindow.isMinimized()) {
lyricWindow.restore();
}
lyricWindow.focus();
lyricWindow.show();
return true;
}
return false;
};
ipcMain.on('open-lyric', () => {
console.log('Received open-lyric request');
if (showLyricWindow()) {
return;
}
console.log('Creating new lyric window');
createWin();
if (process.env.NODE_ENV === 'development') {
lyricWindow.webContents.openDevTools({ mode: 'detach' });
lyricWindow.loadURL(`http://localhost:${config.development.lyricPort}/#/lyric`);
} else {
const distPath = path.resolve(__dirname, config.production.distPath);
lyricWindow.loadURL(`file://${distPath}/index.html#/lyric`);
const win = createWin();
if (!win) {
console.error('Failed to create lyric window');
return;
}
lyricWindow.setMinimumSize(600, 200);
lyricWindow.setSkipTaskbar(true);
if (process.env.NODE_ENV === 'development') {
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');
lyricWindow.show();
win.show();
});
});
ipcMain.on('send-lyric', (e, data) => {
ipcMain.on('send-lyric', (_, data) => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
try {
lyricWindow.webContents.send('receive-lyric', data);
} catch (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()) {
lyricWindow.setAlwaysOnTop(data);
}
@@ -96,22 +118,27 @@ const loadLyricWindow = (ipcMain, mainWin) => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.webContents.send('lyric-window-close');
mainWin.webContents.send('lyric-control-back', 'close');
lyricWindow.close();
lyricWindow.destroy();
lyricWindow = null;
}
});
// 处理鼠标事件
ipcMain.on('mouseenter-lyric', () => {
lyricWindow.setIgnoreMouseEvents(true);
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setIgnoreMouseEvents(true);
}
});
ipcMain.on('mouseleave-lyric', () => {
lyricWindow.setIgnoreMouseEvents(false);
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setIgnoreMouseEvents(false);
}
});
// 处理拖动移动
ipcMain.on('lyric-drag-move', (e, { deltaX, deltaY }) => {
if (!lyricWindow) return;
ipcMain.on('lyric-drag-move', (_, { deltaX, deltaY }) => {
if (!lyricWindow || lyricWindow.isDestroyed()) return;
const [currentX, currentY] = lyricWindow.getPosition();
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
@@ -127,30 +154,23 @@ const loadLyricWindow = (ipcMain, mainWin) => {
store.set('lyricWindowBounds', {
...lyricWindow.getBounds(),
x: newX,
y: newY,
y: newY
});
});
// 添加鼠标穿透事件处理
ipcMain.on('set-ignore-mouse', (e, shouldIgnore) => {
if (!lyricWindow) return;
ipcMain.on('set-ignore-mouse', (_, shouldIgnore) => {
if (!lyricWindow || lyricWindow.isDestroyed()) return;
if (shouldIgnore) {
// 设置鼠标穿透,但保留拖动区域可交互
lyricWindow.setIgnoreMouseEvents(true, { forward: true });
} else {
// 取消鼠标穿透
lyricWindow.setIgnoreMouseEvents(false);
}
lyricWindow.setIgnoreMouseEvents(shouldIgnore, { forward: true });
});
// 添加播放控制处理
ipcMain.on('control-back', (e, command) => {
console.log('Received control-back request:', command);
mainWin.webContents.send('lyric-control-back', command);
ipcMain.on('control-back', (_, command) => {
console.log('command', 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
View 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 };

View File

@@ -3,5 +3,6 @@
"noAnimate": false,
"animationSpeed": 1,
"author": "Alger",
"authorUrl": "https://github.com/algerkong"
"authorUrl": "https://github.com/algerkong",
"musicApiPort": 30488
}

23
src/main/unblockMusic.ts Normal file
View 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
View 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
View 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;
}

View File

@@ -1,6 +1,6 @@
<template>
<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-message-provider>
<router-view></router-view>
@@ -11,21 +11,30 @@
</template>
<script setup lang="ts">
import { darkTheme } from 'naive-ui';
import { darkTheme, lightTheme } from 'naive-ui';
import { onMounted } from 'vue';
import { isElectron } from '@/utils';
import { isElectron } from '@/hooks/MusicHook';
import homeRouter from '@/router/home';
import store from '@/store';
import { isMobile } from './utils';
const theme = computed(() => {
return store.state.theme;
});
onMounted(() => {
store.dispatch('initializeSettings');
store.dispatch('initializeTheme');
if (isMobile.value) {
store.commit(
'setMenus',
homeRouter.filter((item) => item.meta.isMobile),
homeRouter.filter((item) => item.meta.isMobile)
);
console.log(
'qqq ',
homeRouter.filter((item) => item.meta.isMobile)
);
}
});

View File

@@ -22,7 +22,7 @@ export function getListByTag(params: IListByTagParams) {
// 根据cat 获取歌单列表
export function getListByCat(params: IListByCatParams) {
return request.get('/top/playlist', {
params,
params
});
}

View File

@@ -41,6 +41,6 @@ export function logout() {
export function loginByCellphone(phone: string, password: string) {
return request.post('/login/cellphone', {
phone,
password,
password
});
}

View File

@@ -1,5 +1,6 @@
import { ILyric } from '@/type/lyric';
import { IPlayMusicUrl } from '@/type/music';
import { isElectron } from '@/utils';
import request from '@/utils/request';
import requestMusic from '@/utils/request_music';
// 根据音乐Id获取音乐播放URl
@@ -18,5 +19,8 @@ export const getMusicLrc = (id: number) => {
};
export const getParsingMusicUrl = (id: number) => {
if (isElectron) {
return window.api.unblockMusic(id);
}
return requestMusic.get<any>('/music', { params: { id } });
};

View File

@@ -1,5 +1,5 @@
import { IData } from '@/type';
import { IMvItem, IMvUrlData } from '@/type/mv';
import { IMvUrlData } from '@/type/mv';
import request from '@/utils/request';
interface MvParams {
@@ -13,7 +13,7 @@ export const getTopMv = (params: MvParams) => {
return request({
url: '/mv/all',
method: 'get',
params,
params
});
};
@@ -22,7 +22,7 @@ export const getAllMv = (params: MvParams) => {
return request({
url: '/mv/all',
method: 'get',
params,
params
});
};
@@ -30,8 +30,8 @@ export const getAllMv = (params: MvParams) => {
export const getMvDetail = (mvid: string) => {
return request.get('/mv/detail', {
params: {
mvid,
},
mvid
}
});
};
@@ -39,7 +39,7 @@ export const getMvDetail = (mvid: string) => {
export const getMvUrl = (id: Number) => {
return request.get<IData<IMvUrlData>>('/mv/url', {
params: {
id,
},
id
}
});
};

View File

@@ -7,6 +7,6 @@ interface IParams {
// 搜索内容
export const getSearch = (params: IParams) => {
return request.get<any>('/cloudsearch', {
params,
params
});
};

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

3687
src/renderer/assets/css/animate.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
body {
/* background-color: #000; */
}
.n-popover:has(.music-play) {
border-radius: 1.5rem !important;
}

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,12 +1,13 @@
@font-face {
font-family: "iconfont"; /* Project id 2685283 */
src: url('iconfont.woff2?t=1703643214551') format('woff2'),
url('iconfont.woff?t=1703643214551') format('woff'),
url('iconfont.ttf?t=1703643214551') format('truetype');
font-family: 'iconfont'; /* Project id 2685283 */
src:
url('iconfont.woff2?t=1703643214551') format('woff2'),
url('iconfont.woff?t=1703643214551') format('woff'),
url('iconfont.ttf?t=1703643214551') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
@@ -14,270 +15,269 @@
}
.icon-list:before {
content: "\e603";
content: '\e603';
}
.icon-maxsize:before {
content: "\e692";
content: '\e692';
}
.icon-close:before {
content: "\e616";
content: '\e616';
}
.icon-minisize:before {
content: "\e602";
content: '\e602';
}
.icon-shuaxin:before {
content: "\e627";
content: '\e627';
}
.icon-icon_error:before {
content: "\e615";
content: '\e615';
}
.icon-a-3User:before {
content: "\e601";
content: '\e601';
}
.icon-Chat:before {
content: "\e605";
content: '\e605';
}
.icon-Category:before {
content: "\e606";
content: '\e606';
}
.icon-Document:before {
content: "\e607";
content: '\e607';
}
.icon-Heart:before {
content: "\e608";
content: '\e608';
}
.icon-Hide:before {
content: "\e609";
content: '\e609';
}
.icon-Home:before {
content: "\e60a";
content: '\e60a';
}
.icon-a-Image2:before {
content: "\e60b";
content: '\e60b';
}
.icon-Profile:before {
content: "\e60c";
content: '\e60c';
}
.icon-Search:before {
content: "\e60d";
content: '\e60d';
}
.icon-Paper:before {
content: "\e60e";
content: '\e60e';
}
.icon-Play:before {
content: "\e60f";
content: '\e60f';
}
.icon-Setting:before {
content: "\e610";
content: '\e610';
}
.icon-a-TicketStar:before {
content: "\e611";
content: '\e611';
}
.icon-a-VolumeOff:before {
content: "\e612";
content: '\e612';
}
.icon-a-VolumeUp:before {
content: "\e613";
content: '\e613';
}
.icon-a-VolumeDown:before {
content: "\e614";
content: '\e614';
}
.icon-stop:before {
content: "\e600";
content: '\e600';
}
.icon-next:before {
content: "\e6a9";
content: '\e6a9';
}
.icon-prev:before {
content: "\e6ac";
content: '\e6ac';
}
.icon-play:before {
content: "\e6aa";
content: '\e6aa';
}
.icon-xiasanjiaoxing:before {
content: "\e642";
content: '\e642';
}
.icon-videofill:before {
content: "\e7c7";
content: '\e7c7';
}
.icon-favorfill:before {
content: "\e64b";
content: '\e64b';
}
.icon-favor:before {
content: "\e64c";
content: '\e64c';
}
.icon-loading:before {
content: "\e64f";
content: '\e64f';
}
.icon-search:before {
content: "\e65c";
content: '\e65c';
}
.icon-likefill:before {
content: "\e668";
content: '\e668';
}
.icon-like:before {
content: "\e669";
content: '\e669';
}
.icon-notificationfill:before {
content: "\e66a";
content: '\e66a';
}
.icon-notification:before {
content: "\e66b";
content: '\e66b';
}
.icon-evaluate:before {
content: "\e672";
content: '\e672';
}
.icon-homefill:before {
content: "\e6bb";
content: '\e6bb';
}
.icon-link:before {
content: "\e6bf";
content: '\e6bf';
}
.icon-roundaddfill:before {
content: "\e6d8";
content: '\e6d8';
}
.icon-roundadd:before {
content: "\e6d9";
content: '\e6d9';
}
.icon-add:before {
content: "\e6da";
content: '\e6da';
}
.icon-appreciatefill:before {
content: "\e6e3";
content: '\e6e3';
}
.icon-forwardfill:before {
content: "\e6ea";
content: '\e6ea';
}
.icon-voicefill:before {
content: "\e6f0";
content: '\e6f0';
}
.icon-wefill:before {
content: "\e6f4";
content: '\e6f4';
}
.icon-keyboard:before {
content: "\e71b";
content: '\e71b';
}
.icon-picfill:before {
content: "\e72c";
content: '\e72c';
}
.icon-markfill:before {
content: "\e730";
content: '\e730';
}
.icon-presentfill:before {
content: "\e732";
content: '\e732';
}
.icon-peoplefill:before {
content: "\e735";
content: '\e735';
}
.icon-read:before {
content: "\e742";
content: '\e742';
}
.icon-backwardfill:before {
content: "\e74d";
content: '\e74d';
}
.icon-playfill:before {
content: "\e74f";
content: '\e74f';
}
.icon-all:before {
content: "\e755";
content: '\e755';
}
.icon-hotfill:before {
content: "\e757";
content: '\e757';
}
.icon-recordfill:before {
content: "\e7a4";
content: '\e7a4';
}
.icon-full:before {
content: "\e7bc";
content: '\e7bc';
}
.icon-favor_fill_light:before {
content: "\e7ec";
content: '\e7ec';
}
.icon-round_favor_fill:before {
content: "\e80a";
content: '\e80a';
}
.icon-round_location_fill:before {
content: "\e80b";
content: '\e80b';
}
.icon-round_like_fill:before {
content: "\e80c";
content: '\e80c';
}
.icon-round_people_fill:before {
content: "\e80d";
content: '\e80d';
}
.icon-round_skin_fill:before {
content: "\e80e";
content: '\e80e';
}
.icon-broadcast_fill:before {
content: "\e81d";
content: '\e81d';
}
.icon-card_fill:before {
content: "\e81f";
content: '\e81f';
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

75
src/renderer/auto-imports.d.ts vendored Normal file
View 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
View 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']
}
}

View File

@@ -12,20 +12,33 @@
</slot>
</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 flex-col items-center gap-2">
<n-image :src="alipayQR" alt="支付宝收款码" class="w-32 h-32 rounded-lg cursor-none" preview-disabled />
<span class="text-sm text-gray-100">支付宝</span>
<n-image
: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 class="flex flex-col items-center gap-2">
<n-image :src="wechatQR" alt="微信收款码" class="w-32 h-32 rounded-lg cursor-none" preview-disabled />
<span class="text-sm text-gray-100">微信支付</span>
<n-image
: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 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
</p>
</div>
@@ -46,11 +59,11 @@ const copyQQ = () => {
defineProps({
alipayQR: {
type: String,
required: true,
required: true
},
wechatQR: {
type: String,
required: true,
},
required: true
}
});
</script>

View File

@@ -25,24 +25,24 @@
<div class="music-info">
<div class="music-cover">
<n-image
:src="getImgUrl(cover ? listInfo?.coverImgUrl : displayedSongs[0]?.picUrl, '300y300')"
:src="getImgUrl(cover ? listInfo?.coverImgUrl : displayedSongs[0]?.picUrl, '500y500')"
class="cover-img"
preview-disabled
:class="setAnimationClass('animate__fadeIn')"
object-fit="cover"
/>
</div>
<div class="music-detail">
<div v-if="listInfo?.creator" class="creator-info">
<n-avatar round :size="24" :src="getImgUrl(listInfo.creator.avatarUrl, '50y50')" />
<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 v-if="listInfo?.creator" class="creator-info">
<n-avatar round :size="24" :src="getImgUrl(listInfo.creator.avatarUrl, '50y50')" />
<span class="creator-name">{{ listInfo.creator.nickname }}</span>
</div>
<n-scrollbar style="max-height: 200">
<div v-if="listInfo?.description" class="music-desc">
{{ listInfo.description }}
</div>
<play-bottom />
</n-scrollbar>
</div>
<!-- 右侧歌曲列表 -->
@@ -98,8 +98,8 @@ const props = withDefaults(
}>(),
{
loading: false,
cover: true,
},
cover: true
}
);
const emit = defineEmits(['update:show', 'update:loading']);
@@ -122,7 +122,7 @@ const formatDetail = computed(() => (detail: any) => {
const song = {
artists: detail.ar,
name: detail.al.name,
id: detail.al.id,
id: detail.al.id
};
detail.song = song;
@@ -138,9 +138,9 @@ const handlePlay = () => {
...item,
picUrl: item.al.picUrl,
song: {
artists: item.ar,
},
})),
artists: item.ar
}
}))
);
};
@@ -204,7 +204,7 @@ watch(
if (!props.cover) {
loadingList.value = false;
}
},
}
);
// songList
@@ -218,23 +218,23 @@ watch(
}
loadingList.value = false;
},
{ immediate: true },
{ immediate: true }
);
</script>
<style scoped lang="scss">
.music {
&-title {
@apply text-xl font-bold text-white;
@apply text-xl font-bold text-gray-900 dark:text-white;
}
&-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);
}
&-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 {
@apply text-3xl;
}
@@ -248,34 +248,29 @@ watch(
@apply w-[25%] flex-shrink-0 pr-8 flex flex-col;
.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 {
@apply w-full h-full object-cover;
}
}
.music-detail {
@apply flex flex-col flex-grow;
.creator-info {
@apply flex items-center mb-4;
.creator-name {
@apply ml-2 text-sm text-gray-300;
}
}
.music-desc {
@apply text-sm text-gray-400;
.creator-info {
@apply flex items-center mb-4;
.creator-name {
@apply ml-2 text-gray-700 dark:text-gray-300;
}
}
}
&-list-container {
@apply flex-grow min-h-0 flex flex-col relative;
.music-desc {
@apply text-sm text-gray-600 dark:text-gray-400 leading-relaxed pr-4;
}
}
&-list {
@apply flex-grow min-h-0;
&-container {
@apply flex-grow min-h-0 flex flex-col relative;
}
&-content {
@apply min-h-[calc(80vh-60px)];
@@ -312,16 +307,10 @@ watch(
}
.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 {
width: 100%;
}
.song-item {
background-color: #191919;
}
.double-item {
@apply mb-2 bg-light-100 bg-opacity-20 dark:bg-dark-100 dark:bg-opacity-20 rounded-3xl;
}
</style>

View File

@@ -1,7 +1,11 @@
<template>
<n-drawer :show="show" height="100%" placement="bottom" :z-index="999999999" :to="`#layout-main`">
<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
ref="videoRef"
:src="mvUrl"
@@ -86,7 +90,9 @@
下一个
</n-tooltip>
<div class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</div>
<div class="time-display">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</div>
</div>
<div class="right-controls">
@@ -96,14 +102,22 @@
<n-button quaternary circle @click="toggleMute">
<template #icon>
<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>
</template>
</n-button>
</template>
{{ volume === 0 ? '取消静音' : '静音' }}
</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>
<n-tooltip v-if="!props.noList" placement="top">
@@ -111,7 +125,11 @@
<n-button quaternary circle class="play-mode-btn" @click="togglePlayMode">
<template #icon>
<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>
</template>
</n-button>
@@ -124,7 +142,9 @@
<n-button quaternary circle @click="toggleFullscreen">
<template #icon>
<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>
</template>
</n-button>
@@ -181,7 +201,7 @@ import { IMvItem } from '@/type/mv';
type PlayMode = 'single' | 'auto';
const PLAY_MODE = {
Single: 'single' as PlayMode,
Auto: 'auto' as PlayMode,
Auto: 'auto' as PlayMode
} as const;
const props = withDefaults(
@@ -193,8 +213,8 @@ const props = withDefaults(
{
show: false,
currentMv: undefined,
noList: false,
},
noList: false
}
);
const emit = defineEmits<{
@@ -310,7 +330,7 @@ watch(
if (newMv) {
await loadMvUrl(newMv);
}
},
}
);
const autoPlayBlocked = ref(false);
@@ -383,11 +403,21 @@ const checkFullscreenAPI = () => {
(videoContainerRef.value as any)?.webkitRequestFullscreen ||
(videoContainerRef.value as any)?.mozRequestFullScreen ||
(videoContainerRef.value as any)?.msRequestFullscreen,
exitFullscreen: doc.exitFullscreen || doc.webkitExitFullscreen || doc.mozCancelFullScreen || doc.msExitFullscreen,
exitFullscreen:
doc.exitFullscreen ||
doc.webkitExitFullscreen ||
doc.mozCancelFullScreen ||
doc.msExitFullscreen,
fullscreenElement:
doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement,
doc.fullscreenElement ||
doc.webkitFullscreenElement ||
doc.mozFullScreenElement ||
doc.msFullscreenElement,
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">
.mv-detail {
@apply w-full h-full bg-black relative;
@apply h-full bg-light dark:bg-black;
//
@media screen and (orientation: landscape) {
height: 100% !important;
width: 100% !important;
&-title {
@apply fixed top-0 left-0 right-0 p-4 z-10 transition-opacity duration-300;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), transparent);
.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 {
@apply w-full h-full relative;
transition: cursor 0.3s ease;
//
@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;
}
.play-hint {
@apply absolute inset-0 flex items-center justify-center bg-black bg-opacity-50;
.n-button {
@apply text-white hover:text-green-500 transition-colors;
}
}
&.cursor-hidden {
* {
cursor: none !important;
}
.custom-controls {
@apply absolute bottom-0 left-0 right-0 p-4 transition-opacity duration-300;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
//
.custom-controls {
* {
cursor: default !important;
}
.controls-main {
@apply flex justify-between items-center;
.left-controls,
.right-controls {
@apply flex items-center gap-2;
.n-button {
cursor: pointer !important;
}
.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;
@apply text-white hover:text-green-500 transition-colors;
}
.time-display {
@apply text-sm text-white ml-2;
}
.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;
@apply text-white text-sm ml-4;
}
}
}
}
.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 {
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2;
@apply flex flex-col items-center justify-center;
@apply bg-black bg-opacity-70 rounded-lg p-4;
z-index: 20;
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center;
.mode-icon {
@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-leave-active {
transition: opacity 0.3s ease;
@@ -789,79 +692,4 @@ const isMobile = computed(() => store.state.isMobile);
.fade-leave-to {
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>

View File

@@ -13,7 +13,7 @@
? 'animate__bounceIn'
: !isShowAllPlaylistCategory
? 'animate__backOutLeft'
: 'animate__bounceIn',
: 'animate__bounceIn'
) +
' ' +
'type-item-' +
@@ -27,7 +27,11 @@
<div
class="play-list-type-showall"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
:style="
setAnimationDelay(
!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30
)
"
@click="handleToggleShowAllPlaylistCategory"
>
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
@@ -63,8 +67,8 @@ const getAnimationDelay = computed(() => {
watch(isShowAllPlaylistCategory, (newVal) => {
if (!newVal) {
const elements = playlistCategory.value?.sub.map((item, index) =>
document.querySelector(`.type-item-${index}`),
const elements = playlistCategory.value?.sub.map((_, index) =>
document.querySelector(`.type-item-${index}`)
) as HTMLElement[];
elements
.slice(20)
@@ -75,7 +79,7 @@ watch(isShowAllPlaylistCategory, (newVal) => {
() => {
(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 {
document.querySelectorAll('.play-list-type-item').forEach((element) => {
@@ -112,8 +116,8 @@ const handleClickPlaylistType = (type: string) => {
router.push({
path: '/list',
query: {
type,
},
type
}
});
};
@@ -132,15 +136,15 @@ onMounted(() => {
<style lang="scss" scoped>
.title {
@apply text-lg font-bold mb-4;
@apply text-lg font-bold mb-4 text-gray-900 dark:text-white;
}
.play-list-type {
width: 250px;
@apply mx-6;
@apply mr-4;
&-item,
&-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;
background-color: #1a1a1a;
@apply bg-light dark:bg-black text-gray-900 dark:text-white;
@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 {
@apply block text-center;

View File

@@ -38,6 +38,7 @@ import { getNewAlbum } from '@/api/home';
import { getAlbum } from '@/api/list';
import type { IAlbumNew } from '@/type/album';
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
import MusicList from '@/components/MusicList.vue';
const albumData = ref<IAlbumNew>();
const loadAlbumList = async () => {
@@ -65,9 +66,9 @@ const handleClick = async (item: any) => {
...res.data.album,
creator: {
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;
};
@@ -81,7 +82,7 @@ onMounted(() => {
.recommend-album {
@apply flex-1 mx-5;
.title {
@apply text-lg font-bold mb-4;
@apply text-lg font-bold mb-4 text-gray-900 dark:text-white;
}
.recommend-album-list {
@@ -95,7 +96,7 @@ onMounted(() => {
filter: brightness(50%);
}
&-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 {
opacity: 1;

View File

@@ -10,7 +10,9 @@
:style="setAnimationDelay(0, 100)"
>
<div
:style="setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '300y300'))"
:style="
setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '500y500'))
"
class="recommend-singer-item-bg"
></div>
<div
@@ -20,7 +22,11 @@
<div class="font-bold text-xl">每日推荐</div>
<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 }}
<br />
</p>
@@ -34,8 +40,13 @@
:class="setAnimationClass('animate__backInRight')"
:style="setAnimationDelay(index + 1, 100)"
>
<div :style="setBackgroundImg(getImgUrl(item.picUrl, '300y300'))" 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
: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-play" @click="toSearchSinger(item.name)">
<i class="iconfont icon-playfill text-xl"></i>
@@ -68,6 +79,7 @@ import router from '@/router';
import { IDayRecommend } from '@/type/day_recommend';
import type { IHotSinger } from '@/type/singer';
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
import MusicList from '@/components/MusicList.vue';
const store = useStore();
@@ -88,13 +100,13 @@ const loadData = async () => {
//
try {
const {
data: { data: dayRecommend },
data: { data: dayRecommend }
} = await getDayRecommend();
//
if (dayRecommend) {
singerData.artists = singerData.artists.slice(0, 4);
}
dayRecommendData.value = dayRecommend;
dayRecommendData.value = dayRecommend as unknown as IDayRecommend;
} catch (error) {
console.error('error', error);
}
@@ -109,8 +121,8 @@ const toSearchSinger = (keyword: string) => {
router.push({
path: '/search',
query: {
keyword,
},
keyword
}
});
};
@@ -131,14 +143,20 @@ watchEffect(() => {
&-item {
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between overflow-hidden;
&-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%);
}
&-info {
@apply flex items-center p-2;
&-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;
}
}
}

View File

@@ -9,7 +9,10 @@
>
<!-- 推荐音乐列表 -->
<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" />
</div>
</template>
@@ -51,17 +54,15 @@ const handlePlay = () => {
<style lang="scss" scoped>
.title {
@apply text-lg font-bold mb-4;
@apply text-lg font-bold mb-4 text-gray-900 dark:text-white;
}
.recommend-music {
@apply flex-auto;
// width: 530px;
.text-ellipsis {
width: 100%;
}
&-list {
@apply rounded-3xl p-2 w-full border border-gray-700;
background-color: #0d0d0d;
@apply rounded-3xl p-2 w-full border border-gray-200 dark:border-gray-700 bg-light dark:bg-black;
}
}
</style>

View File

@@ -1,5 +1,11 @@
<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-header">
<div class="app-icon">
@@ -18,7 +24,10 @@
<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"
<a
class="text-green-500"
target="_blank"
href="https://github.com/algerkong/AlgerMusicPlayer/releases"
>GitHub</a
>
下载最新版本
@@ -31,11 +40,11 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import config from '@/../package.json';
import { isMobile } from '@/utils';
import { isElectron, isMobile } from '@/utils';
import config from '../../../../package.json';
const showModal = ref(false);
const isElectron = ref((window as any).electron !== undefined);
const noPrompt = ref(false);
const closeModal = () => {
@@ -47,7 +56,7 @@ const closeModal = () => {
onMounted(() => {
// electron
if (isElectron.value || isMobile.value) {
if (isElectron || isMobile.value) {
return;
}
@@ -64,10 +73,13 @@ const handleInstall = async (): Promise<void> => {
console.log('userAgent', userAgent);
const isMac: boolean = userAgent.includes('Mac');
const isWindows: boolean = userAgent.includes('Win');
const isARM: boolean = userAgent.includes('ARM') || userAgent.includes('arm') || userAgent.includes('OS X');
const isX64: boolean = userAgent.includes('x86_64') || userAgent.includes('Win64') || userAgent.includes('WOW64');
const isARM: boolean =
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 =
!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 version = config.version as string;

View File

@@ -4,12 +4,12 @@ import { setAnimationClass } from '@/utils';
const props = defineProps({
showPop: {
type: Boolean,
default: false,
default: false
},
showClose: {
type: Boolean,
default: true,
},
default: true
}
});
const musicFullClass = computed(() => {

View File

@@ -10,8 +10,8 @@ const isPlay = computed(() => store.state.isPlay as boolean);
defineProps({
height: {
type: String,
default: undefined,
},
default: undefined
}
});
</script>

View File

@@ -1,7 +1,11 @@
<template>
<div class="search-item" :class="item.type" @click="handleClick">
<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">
<i class="iconfont icon icon-play"></i>
</div>
@@ -17,8 +21,14 @@
:name="item.name"
:song-list="songList"
: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>
</template>
@@ -30,6 +40,7 @@ import MvPlayer from '@/components/MvPlayer.vue';
import { audioService } from '@/services/audioService';
import { IMvItem } from '@/type/mv';
import { getImgUrl } from '@/utils';
import MusicList from '../MusicList.vue';
const props = defineProps<{
item: {
@@ -49,7 +60,7 @@ const listInfo = ref<any>(null);
const getCurrentMv = () => {
return {
id: props.item.id,
name: props.item.name,
name: props.item.name
} as unknown as IMvItem;
};
@@ -64,6 +75,14 @@ const handleClick = async () => {
song.al.picUrl = song.al.picUrl || props.item.picUrl;
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') {
@@ -84,7 +103,7 @@ const handleClick = async () => {
<style scoped lang="scss">
.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;
.search-item-img {
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;

View File

@@ -3,21 +3,24 @@
<n-image
v-if="item.picUrl"
ref="songImg"
:src="getImgUrl(item.picUrl, '40y40')"
:src="getImgUrl(item.picUrl, '100y100')"
class="song-item-img"
preview-disabled
:img-props="{
crossorigin: 'anonymous',
crossorigin: 'anonymous'
}"
@load="imageLoad"
/>
<div class="song-item-content">
<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>
<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"
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
>{{ artists.name
}}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
>
</n-ellipsis>
</div>
@@ -27,8 +30,11 @@
</div>
<div class="song-item-content-name">
<n-ellipsis class="text-ellipsis" line-clamp="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
<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>
</div>
@@ -36,10 +42,14 @@
</div>
<div class="song-item-operating" :class="{ 'song-item-operating-list': list }">
<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
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 }"
@click="playMusicEvent(item)"
>
@@ -69,8 +79,8 @@ const props = withDefaults(
{
mini: false,
list: false,
favorite: true,
},
favorite: true
}
);
const store = useStore();
@@ -79,7 +89,9 @@ const play = computed(() => store.state.play as boolean);
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(() => {
@@ -95,7 +107,7 @@ const imageLoad = async () => {
return;
}
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
props.item.backgroundColor = backgroundColor;
@@ -139,72 +151,98 @@ const toggleFavorite = async (e: Event) => {
.text-ellipsis {
width: 100%;
}
.song-item {
-webkit-user-select: none;
-moz-user-select: none;
-ms-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 {
@apply w-12 h-12 rounded-2xl mr-4;
}
&-content {
@apply flex-1;
&-title {
@apply text-base text-white;
@apply text-base text-gray-900 dark:text-white;
}
&-name {
@apply text-xs;
@apply text-gray-400;
@apply text-xs text-gray-500 dark:text-gray-400;
}
}
&-operating {
@apply flex items-center rounded-full border border-gray-700 ml-4;
background-color: #0d0d0d;
@apply flex items-center rounded-full ml-4 border dark:border-gray-700 border-gray-200 bg-light dark:bg-black;
.iconfont {
@apply text-xl;
}
.icon-likefill {
color: #868686;
@apply text-xl hover:text-red-600 transition;
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-red-500;
}
&-like {
@apply mr-2 cursor-pointer ml-4;
}
.like-active {
@apply text-red-600;
@apply text-red-500;
}
&-play {
@apply cursor-pointer border border-gray-500 rounded-full w-10 h-10 flex justify-center items-center hover:bg-green-600 transition;
animation-iteration-count: infinite;
@apply cursor-pointer rounded-full w-10 h-10 flex justify-center items-center transition
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 {
@apply p-2 rounded-2xl;
.song-item {
@apply p-0;
&-img {
@apply w-10 h-10 mr-2;
}
&-content {
@apply flex-1;
&-title {
@apply text-sm;
}
&-name {
@apply text-xs;
}
}
&-operating {
@apply pl-2;
.iconfont {
@apply text-base;
}
&-like {
@apply mr-1 ml-1;
}
&-play {
@apply w-8 h-8;
}
@@ -213,35 +251,50 @@ const toggleFavorite = async (e: Event) => {
}
.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 {
@apply w-10 h-10 rounded-lg mr-3;
}
.song-item-content {
@apply flex items-center flex-1;
&-wrapper {
@apply flex items-center flex-1 text-sm;
}
&-title {
@apply text-white flex-shrink-0 max-w-[45%];
@apply flex-shrink-0 max-w-[45%] text-gray-900 dark:text-white;
}
&-divider {
@apply mx-2 text-gray-400;
@apply mx-2 text-gray-500 dark:text-gray-400;
}
&-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 {
@apply flex items-center gap-2;
&-like {
@apply cursor-pointer hover:scale-110 transition-transform;
.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 {
@apply w-7 h-7 cursor-pointer hover:scale-110 transition-transform;
.iconfont {
@apply text-base;
}

View 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>

View File

@@ -13,22 +13,22 @@ export const USER_SET_OPTIONS = [
// },
{
label: '退出登录',
key: 'logout',
key: 'logout'
},
{
label: '设置',
key: 'set',
},
key: 'set'
}
];
export const SEARCH_TYPES = [
{
label: '单曲',
key: 1,
key: 1
},
{
label: '专辑',
key: 10,
key: 10
},
// {
// label: '歌手',
@@ -36,7 +36,7 @@ export const SEARCH_TYPES = [
// },
{
label: '歌单',
key: 1000,
key: 1000
},
// {
// label: '用户',
@@ -44,8 +44,8 @@ export const SEARCH_TYPES = [
// },
{
label: 'MV',
key: 1004,
},
key: 1004
}
// {
// label: '歌词',
// key: 1006,

Some files were not shown because too many files have changed in this diff Show More