Merge branch 'dev' of github.com:lyswhut/lx-music-desktop into dev

pull/733/head
lyswhut 2021-11-09 19:34:55 +08:00
commit 6f72da87b1
121 changed files with 5005 additions and 4683 deletions

View File

@ -22,5 +22,6 @@
},
"settings": {
"html/html-extensions": [".html", ".vue"]
}
},
"ignorePatterns": ["vendors", "*.min.js"]
}

View File

@ -1,25 +0,0 @@
---
name: 功能请求(请先查看常见问题及搜索issue列表中有无你要提的问题)
about: 为这个项目提出一个想法
title: 例如添加xxx功能、优化xxx功能
labels: ''
assignees: ''
---
**解决方案检查**
<!-- 请确保你已从以下渠道寻找过解决方案,然后将 [ ] 替换成 [x] -->
- [ ] 我已阅读常见问题(<https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md>)
- [ ] 我已搜索issue列表(<https://github.com/lyswhut/lx-music-desktop/issues?utf8=✓&q=>)
**描述您想要的解决方案**
<!-- 简洁明了地描述您要发生的事情。 -->
**描述您考虑过的替代方案**
<!-- 对您考虑过的所有替代解决方案或功能的简洁明了的描述。 -->
**其他内容**
<!-- 在此处添加有关功能请求的任何其他上下文或屏幕截图(直接把图片拖到编辑框即可添加图片)。 -->

35
.github/ISSUE_TEMPLATE/----.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: ✨功能请求
description: 为这个项目提出一个想法请先查看常见问题及搜索issue列表中有无你要提的问题
title: "[Feature]: <title>"
body:
- type: checkboxes
attributes:
label: 解决方案检查
description: 请确保你已完成以下所有操作
options:
- label: 我已阅读常见问题(<https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md>),但没有找到解决方案
required: true
- label: 我已搜索issue列表(<https://github.com/lyswhut/lx-music-desktop/issues?utf8=✓&q=>),但没有发现类似的问题
required: true
- type: textarea
attributes:
label: 问题描述
description: 请添加清晰简洁的描述,说明你希望通过此功能请求解决的问题
validations:
required: true
- type: textarea
attributes:
label: 描述您想要的解决方案
description: 简洁明了地描述你要发生的事情
validations:
required: true
- type: textarea
attributes:
label: 描述您考虑过的替代方案
description: 对你考虑过的所有替代解决方案或功能的简洁明了的描述
validations:
required: false
- type: textarea
attributes:
label: 附加信息
description: 如果你的问题需要进一步解释,或者想要表达其他内容,请在此处添加更多信息。(直接把图片、视频拖到编辑框即可添加图片或视频)

View File

@ -1,42 +0,0 @@
---
name: 报告Bug(请先查看常见问题及搜索issue列表中有无你要提的问题)
about: 创建报告以帮助我们改进
title: 例如:音乐无法播放
labels: ''
assignees: ''
---
**解决方案检查**
<!-- 请确保你已从以下渠道寻找过解决方案,然后将 [ ] 替换成 [x] -->
- [ ] 我已阅读常见问题(<https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md>)
- [ ] 我已搜索issue列表(<https://github.com/lyswhut/lx-music-desktop/issues?utf8=✓&q=>)
**描述错误**
<!-- 清楚简洁地说明错误是什么。 -->
**重现**
重现行为的步骤:
1.转到“ ...”
2.点击“ ....”
3.向下滚动到“ ....”
4.看到错误
**预期行为**
<!-- 对您期望发生的事情的简洁明了的描述。 -->
**截图**
<!-- 如果适用,请添加屏幕截图以帮助解释您的问题(直接把图片拖到编辑框即可添加图片)。 -->
**环境:**
  -操作系统及版本:[例如Windows 10 64位 18362.156]
  -软件安装包及版本:[例如1.0.0 安装版]
**其他内容**
<!-- 在此处添加有关该问题的任何其他上下文。 -->

48
.github/ISSUE_TEMPLATE/--bug.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: 🐞报告Bug
description: 报告bug请先查看常见问题及搜索issue列表中有无你要提的问题
title: "[Bug]: <title>"
body:
- type: checkboxes
attributes:
label: 解决方案检查
description: 请确保你已完成以下所有操作
options:
- label: 我已阅读常见问题(<https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md>),并没有找到解决方案
required: true
- label: 我已搜索issue列表(<https://github.com/lyswhut/lx-music-desktop/issues?utf8=✓&q=>),并没有发现类似的问题
required: true
- type: textarea
attributes:
label: 预期行为
description: 对期望发生的事情的清晰简明描述
validations:
required: true
- type: textarea
attributes:
label: 实际行为
description: 对实际发生的事情的清晰简明描述
validations:
required: true
- type: input
attributes:
label: Lx Music 版本
description: 你使用什么版本的LX Music
placeholder: 1.15.0
validations:
required: true
- type: input
attributes:
label: 最后正常的版本
description: 如果有,请在此处填写最后正常的版本是多少?
placeholder: 1.15.0
- type: input
attributes:
label: 操作系统版本
description: 您使用的是什么操作系统版本?在 Windows 上,单击开始按钮 > 设置 > 系统 > 关于;在 macOS 上,单击 Apple 菜单 > 关于本机;在 Linux 上,使用 lsb_release 或 uname -a
placeholder: "例如 Windows 10 版本 1909、macOS Catalina 10.15.7 或 Ubuntu 20.04"
validations:
required: true
- type: textarea
attributes:
label: 附加信息
description: 如果你的问题需要进一步解释,或者你所遇到的问题不容易重现,请在此处添加更多信息。(直接把图片、视频拖到编辑框即可添加图片或视频)

View File

@ -3,6 +3,7 @@ module.exports = {
reject: [
'vue-loader',
'webpack-dev-server',
'eslint',
// 'eslint-config-standard'
]
}

View File

@ -6,6 +6,87 @@ Project versioning adheres to [Semantic Versioning](http://semver.org/).
Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
## [1.15.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.15.3...v1.15.2) - 2021-11-09
### 其他
- 降级electron到v13.4.0这修复了windows 7下播放歌曲时软件会崩溃的问题
## [1.15.3](https://github.com/lyswhut/lx-music-desktop/compare/v1.15.1...v1.15.3) - 2021-11-09
### 其他
- 降级electron到v13.4.0这修复了windows 7下播放歌曲时软件会崩溃的问题
## [1.15.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.15.0...v1.15.1) - 2021-11-09
### 优化
- 优化我的列表、下载列表等列表的滚动流畅度
- 优化下载功能的批量添加、删除、暂停任务时的流畅度,现在进行这些操作应该不会再觉得卡顿了
- 支持启动软件时恢复播放下载列表里的歌曲
- 添加媒体播放进度条的信息设置
### 修复
- 修复某些情况下获取URL失败时会意外切歌的问题
- 修复了某些情况下会列表同步失败,导致连接断开无限重连或一直卡在 `syncing...` 的问题
- 修复列表数据过大导致同步失败的问题
### 其他
- 更新electron到v15.3.1(这修复了媒体控制失效的问题)
## [1.15.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.14.1...v1.15.0) - 2021-10-29
### 新增
- 添加黑色托盘图标
- 自定义源新增`version`字段,新增`utils.buffer.bufToString`方法
### 优化
- 大幅优化我的列表、下载、歌单、排行榜列表性能,现在即使同一列表内的歌曲很多时也不会卡顿了
- 优化列表同步代码逻辑
- 优化开关评论时的动画性能
- 优化进入、离开播放详情页的性能
- 兼容桌面歌词以触摸的方式移动、调整大小
- 调整图标尺寸
### 修复
- 修复kg源的歌单链接无法打开的问题
- 修复同一首歌的URL、歌词等同时需要换源时的处理问题
### 其他
- 更新 Electron 到 v15.3.0
## [1.14.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.14.0...v1.14.1) - 2021-10-04
### 修复
- 修复我的列表搜索无法搜索小括号、中括号等字符的问题
- 修复v1.14.0出现的备份与恢复功能备份的数据无法恢复的问题同时兼容使用v1.14.0导出的存在问题的数据
## [1.14.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.13.0...v1.14.0) - 2021-10-02
### 新增
- 新增歌词简体中文转繁体中文,当软件语言被设置为繁体中文后,播放歌曲的歌词也将自动转成繁体中文显示
- 新增单个列表导入/导出功能,可以方便分享歌曲列表,可在右击“我的列表”里的列表名后弹出的菜单中使用
- 新增删除列表前的确认弹窗,防止误删列表
- 新增歌词文本选择复制功能,可在详情页进度条上方的歌词文本选择按钮进入歌词文本选择模式,选择完成后可鼠标右击或者使用系统快捷键复制
- 新增重复歌曲列表,可以方便移除我的列表中的重复歌曲,此列表会列出目标列表里歌曲名相同的歌曲,可在右击“我的列表”里的列表名后弹出的菜单中使用
### 修复
- 修复mg排行榜无法加载的问题
- 修复点击播放详情页的进度条跳进度时会出现偏移的问题
- 修复在有提示信息的地方长按鼠标按键时提示信息会闪烁的问题
- 修复下载歌曲时的歌词下载不尝试获取缓存歌词的问题
- 修复GNOME等桌面下每次打开应用时需重新设置歌词窗口置顶的问题
## [1.13.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.12.2...v1.13.0) - 2021-09-05
如果你喜欢并经常使用洛雪音乐并想要第一时间尝鲜洛雪的新功能可以加入测试企鹅群768786588

29
FAQ.md
View File

@ -56,9 +56,11 @@
播放在线列表内的歌曲需要将它们都添加到我的列表才能播放,你可以全选列表内的歌曲然后添加到现有列表或者新创建的列表,然后去播放该列表内的歌曲。
从v1.10.0起,你可以右击排行榜名字的弹出菜单中直接播放或收藏整个排行榜的歌曲。
## 无法打开外部歌单
不支持源打开歌单,请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应(不一样的话请通过右上角切换歌单源);<br>
不支持源打开歌单,请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应(不一样的话请通过右上角切换歌单源);<br>
对于分享出来的歌单若打开失败可尝试先在浏览器中打开后再从浏览器地址栏复制URL地址到软件打开<br>
或者如果你知道歌单 id 也可以直接输入歌单 id 打开。<br>
@ -128,12 +130,12 @@
由于软件默认使用了透明窗口根据Electron官方文档的[说明](https://electronjs.org/docs/api/frameless-window#%E5%B1%80%E9%99%90%E6%80%A7)
> 在 windows 操作系统上, 当 DWM 被禁用时, 透明窗口将无法工作。
因此,当 win7 没有使用**AERO**主题时界面将会显示异常开启AERO的方法请自行百度`win7开启aero效果`(开启后可看到任务栏变透明)。<br>
因此,当 win7 没有使用**Aero**主题时界面将会显示异常开启AERO的方法请自行百度`win7开启Aero效果`(开启后可看到任务栏变透明)。<br>
从`0.14.0`版本起不再强制要求开启透明效果,若你实在不想开启(若非电脑配置太低,墙裂建议开启!),可通过添加运行参数`-dt`来运行程序即可,例如:`.\lx-music-desktop.exe -dt`,添加方法可自行百度“给快捷方式加参数”,该参数的作用是用来控制程序是否使用非透明窗口运行。
注:启用**AERO**主题后,若软件出现黑边框,则重启软件即可恢复正常。
注:启用**Aero**主题后,若软件出现黑边框,则重启软件即可恢复正常。
对于一些完全无法正常显示界面、开启了AERO后问题仍未解决的情况请阅读下面的 **软件启动后,界面无法显示** 解决。
对于一些完全无法正常显示界面、开启了AERO后问题仍未解决的情况请阅读下面的 **Window 7 下软件启动后,界面无法显示** 解决。
### Linux 下界面异常
@ -142,19 +144,17 @@
v1.6.0及之后的版本才支持`-dha`参数
## 软件启动后,界面无法显示
## Windows 7 下软件启动后,界面无法显示
对于软件启动后,可以在任务栏看到软件,但软件界面在桌面上无任何显示,或者整个界面偶尔闪烁的情况。<br>
原始问题看:<https://github.com/electron/electron/issues/19569#issuecomment-522231083><br>
解决办法:下载`.NET Framework 4.7.1`或**更高**版本安装即可(建议安装最新版,若安装过程中遇到问题可尝试自行百度解决)。<br>
微软官方下载地址:<https://dotnet.microsoft.com/download/dotnet-framework><br>
下载`Runtime(运行时)`版即可,安装完成后可能需要重启才生效。
下载`Runtime(运行时)`版即可,安装完成后可能需要重启才生效**若出现闪烁的情况**,可阅读下面的**Windows 7 下整个界面闪烁**解决
若还是不行可尝试以下操作:
## Windows 7 下整个界面闪烁(消失又出现)
- 更新显卡驱动
- 添加启动参数`-dha`运行(添加的方法请自行百度“给快捷方式加参数”)
- 尝试将绿色版的软件放在**桌面**或**我的文档**运行
可尝试在关掉软件后,在桌面空白处鼠标右击,在弹出的菜单中选择**个性化**,在弹出的窗口中**切换到系统内置的Aero主题**,然后再启动软件看是否解决。
## Windows 7 下桌面歌词字体列表为空
@ -344,8 +344,8 @@ send(EVENT_NAMES.inited, {
```
- `@name `:源的名字,建议不要过长,10个字符以内
- `@description `:源的描述,建议不要过长,20个字符以内,可不填,不填时必须保留 @description
- `@name `:源的名字,建议不要过长,24个字符以内
- `@description `:源的描述,建议不要过长,36个字符以内,可不填,不填时必须保留 @description
- `@version`:源的版本号,可不填,不填时可以删除 @version
- `@author `:脚本作者名字,可不填,不填时可以删除 @author
- `@homepage `:脚本主页,可不填,不填时可以删除 @homepage
@ -354,6 +354,10 @@ send(EVENT_NAMES.inited, {
应用为脚本暴露的API对象。
#### `window.lx.version`
自定义源API版本API变更时此版本号将会更改新增于v1.14.0之后)
#### `window.lx.EVENT_NAMES`
常量事件名称对象,发送、注册事件时传入事件名时使用,可用值:
@ -409,6 +413,7 @@ const cancelHttp = window.lx.request(url, options, callback)
应用提供给脚本的工具方法:
- `window.lx.utils.buffer.from`对应Node.js的 `Buffer.from`
- `window.lx.utils.buffer.bufToString`Buffer转字符串 `bufToString(buffer, format)``format`对应Node.js `Buffer.toString`的参数v1.14.0之后新增)
- `window.lx.utils.crypto.aesEncrypt`AES加密 `aesEncrypt(buffer, mode, key, iv)`
- `window.lx.utils.crypto.md5`MD5加密 `md5(str)`
- `window.lx.utils.crypto.randomBytes`:生成随机字符串 `randomBytes(size)`

View File

@ -36,7 +36,7 @@
所用技术栈:
- Electron 13
- Electron 15
- Vue 2
已支持的平台:

View File

@ -1,4 +1,5 @@
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
module.exports = {
target: 'electron-main',
@ -18,17 +19,6 @@ module.exports = {
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-formatter-friendly'),
},
},
exclude: /node_modules/,
enforce: 'pre',
},
{
test: /\.node$/,
use: 'node-loader',
@ -38,4 +28,7 @@ module.exports = {
performance: {
maxEntrypointSize: 300000,
},
plugins: [
new ESLintPlugin(),
],
}

View File

@ -3,6 +3,7 @@ const VueLoaderPlugin = require('vue-loader/lib/plugin')
const HTMLPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CleanCSSPlugin = require('less-plugin-clean-css')
const ESLintPlugin = require('eslint-webpack-plugin')
const vueLoaderConfig = require('../vue-loader.config')
const { mergeCSSLoader } = require('../utils')
@ -32,18 +33,6 @@ module.exports = {
},
module: {
rules: [
{
test: /\.(vue|js)$/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-formatter-friendly'),
emitWarning: isDev,
},
},
exclude: /node_modules/,
enforce: 'pre',
},
{
test: /\.node$/,
use: 'node-loader',
@ -147,5 +136,8 @@ module.exports = {
filename: isDev ? '[name].css' : '[name].[contenthash:8].css',
chunkFilename: isDev ? '[id].css' : '[id].[contenthash:8].css',
}),
new ESLintPlugin({
extensions: ['js', 'vue'],
}),
],
}

View File

@ -3,6 +3,7 @@ const VueLoaderPlugin = require('vue-loader/lib/plugin')
const HTMLPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CleanCSSPlugin = require('less-plugin-clean-css')
const ESLintPlugin = require('eslint-webpack-plugin')
const vueLoaderConfig = require('../vue-loader.config')
const { mergeCSSLoader } = require('../utils')
@ -32,18 +33,6 @@ module.exports = {
},
module: {
rules: [
{
test: /\.(vue|js)$/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-formatter-friendly'),
emitWarning: isDev,
},
},
exclude: /node_modules/,
enforce: 'pre',
},
{
test: /\.node$/,
use: 'node-loader',
@ -147,5 +136,8 @@ module.exports = {
filename: isDev ? '[name].css' : '[name].[contenthash:8].css',
chunkFilename: isDev ? '[id].css' : '[id].[contenthash:8].css',
}),
new ESLintPlugin({
extensions: ['js', 'vue'],
}),
],
}

5722
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "lx-music-desktop",
"version": "1.13.0",
"version": "1.15.2",
"description": "一个免费的音乐查找助手",
"main": "./dist/electron/main.js",
"productName": "lx-music-desktop",
@ -70,7 +70,7 @@
"up": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://localhost:1081 npm i"
},
"browserslist": [
"Electron 13.3.0"
"Electron 15.2.0"
],
"engines": {
"node": ">= 14"
@ -78,6 +78,7 @@
"build": {
"appId": "cn.toside.music.desktop",
"directories": {
"buildResources": "./resources",
"output": "./build"
},
"files": [
@ -90,12 +91,12 @@
"./licenses"
],
"win": {
"icon": "./resources/icons/256x256.ico",
"icon": "./resources/icons/icon.ico",
"legalTrademarks": "lyswhut",
"artifactName": "${productName} v${version} ${env.ARCH} ${env.TARGET}.${ext}"
},
"mac": {
"icon": "./resources/icons/512x512.icns",
"icon": "./resources/icons/icon.icns",
"category": "public.app-category.music"
},
"linux": {
@ -166,51 +167,51 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/core": "^7.16.0",
"@babel/plugin-proposal-class-properties": "^7.16.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-umd": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/plugin-transform-modules-umd": "^7.16.0",
"@babel/plugin-transform-runtime": "^7.16.0",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.15.4",
"@babel/preset-env": "^7.16.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"babel-loader": "^8.2.3",
"babel-preset-minify": "^0.5.1",
"browserslist": "^4.16.8",
"cfonts": "^2.9.3",
"browserslist": "^4.17.6",
"cfonts": "^2.10.0",
"chalk": "^4.1.2",
"changelog-parser": "^2.8.0",
"copy-webpack-plugin": "^9.0.1",
"core-js": "^3.17.2",
"core-js": "^3.19.1",
"cross-env": "^7.0.3",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"css-loader": "^6.5.1",
"css-minimizer-webpack-plugin": "^3.1.2",
"del": "^6.0.0",
"electron": "^13.3.0",
"electron": "^13.4.0",
"electron-builder": "^22.11.7",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"electron-to-chromium": "^1.3.830",
"electron-to-chromium": "^1.3.891",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-formatter-friendly": "^7.0.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-html": "^6.1.2",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-standard": "^4.1.0",
"eslint-webpack-plugin": "^3.1.0",
"file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^5.3.2",
"less": "^4.1.1",
"less-loader": "^10.0.1",
"html-webpack-plugin": "^5.5.0",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"less-plugin-clean-css": "^1.5.1",
"markdown-it": "^12.2.0",
"mini-css-extract-plugin": "^2.2.2",
"mini-css-extract-plugin": "^2.4.4",
"node-loader": "^2.0.0",
"postcss": "^8.3.6",
"postcss-loader": "^6.1.1",
"postcss": "^8.3.11",
"postcss-loader": "^6.2.0",
"postcss-pxtorem": "^6.0.0",
"pug": "^3.0.2",
"pug-loader": "^2.4.0",
@ -218,38 +219,36 @@
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"spinnies": "^0.5.1",
"terser-webpack-plugin": "^5.2.3",
"terser-webpack-plugin": "^5.2.5",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.52.0",
"webpack-cli": "^4.8.0",
"webpack": "^5.62.1",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^3.11.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-hot-middleware": "^2.25.1",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"bufferutil": "^4.0.3",
"bufferutil": "^4.0.5",
"crypto-js": "^4.1.1",
"electron-log": "^4.4.1",
"electron-store": "^8.0.0",
"electron-store": "^8.0.1",
"electron-updater": "^4.3.9",
"font-list": "git+https://github.com/lyswhut/node-font-list.git#c6caf4060e471afe143a4aca30d554644522966d",
"http-terminator": "^3.0.0",
"font-list": "git+https://github.com/lyswhut/node-font-list.git#2ed3b4ee42e8a43373e8a30d87760c840725843e",
"http-terminator": "^3.0.3",
"iconv-lite": "^0.6.3",
"image-size": "^1.0.0",
"koa": "^2.13.1",
"long": "^4.0.0",
"lrc-file-parser": "^1.1.2",
"koa": "^2.13.4",
"long": "^5.1.0",
"needle": "^3.0.0",
"node-id3": "^0.2.3",
"request": "^2.88.2",
"socket.io": "^4.2.0",
"utf-8-validate": "^5.0.5",
"socket.io": "^4.3.2",
"utf-8-validate": "^5.0.7",
"vue": "^2.6.14",
"vue-i18n": "^8.25.0",
"vue-router": "^3.5.2",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0"
"vue-i18n": "^8.26.7",
"vue-router": "^3.5.3",
"vuex": "^3.6.2"
}
}

View File

@ -15,7 +15,7 @@ module.exports = {
'*-height', '*-width',
'flex', '::-webkit-scrollbar',
'top', 'left', 'bottom', 'right',
'border-radius',
'border-radius', 'gap',
],
selectorBlackList: ['html', 'ignore-to-rem'],
replace: true,

View File

@ -1,18 +1,3 @@
如果你喜欢并经常使用洛雪音乐并想要第一时间尝鲜洛雪的新功能可以加入测试企鹅群768786588
注意:测试版的功可能会不稳定,打算潜水的勿加。
### 其他
### 新增
- 歌曲搜索框新增清理按钮,点击此按钮可以清理搜索框并返回初始搜索界面
- 新增“下载的歌词文件编码格式”设置,默认下载的歌词编码仍是`UTF-8`,对于某些在设备(如车机)上出现歌词中文乱码的用户可以尝试选择以`GBK`编码格式保存歌词文件
- 新增设置-桌面歌词-歌词字体设置此设置可用于设置桌面歌词的字体已知的问题Windows 7 下可能会出现字体列表为空的情况,这是当前系统的 Powershell 版本小于5.1导致的,请自行**尝试**看常见解决)
### 优化
- 支持网易源“我喜欢”歌单以注入token的方式打开。由于网易源的“我喜欢”歌单需要登录才能打开若你看不懂后半句就去阅读 常见问题-无法打开外部歌单),现若想要打开此类歌单,需要在歌单链接后面拼上 `###` 再加上有效的token拼接格式`[id|url]###token`例子最后面的xxxxxx替换成你的token`https://music.163.com/#/playlist?id=123456&userid=123456###xxxxxx`
- 软件内快捷键的最小化触发时,如果已启用托盘,则隐藏程序,否则最小化程序
### 修复
- 修复某些情况下同步功能会导致切歌混乱的问题
- 修复从电脑浏览器复制的企鹅歌单链接无法打开的问题
- 降级electron到v13.4.0这修复了windows 7下播放歌曲时软件会崩溃的问题

File diff suppressed because one or more lines are too long

BIN
resources/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
resources/icons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

BIN
resources/icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
resources/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

BIN
resources/icons/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

BIN
resources/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
resources/icons/icon.icns Normal file

Binary file not shown.

BIN
resources/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
resources/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -35,7 +35,6 @@ const defaultSetting = {
list: {
isShowAlbumName: true,
isShowSource: true,
prevSelectListId: 'default',
isSaveScrollLocation: true,
addMusicLocationType: 'top',
},

View File

@ -33,6 +33,8 @@ const names = {
restart_window: 'restart_window',
lang_s2t: 'lang_s2t',
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
get_lyric_info: 'get_lyric_info',
set_lyric_info: 'set_lyric_info',
@ -74,6 +76,7 @@ const names = {
sync_generate_code: 'sync_generate_code',
sync_action_list: 'sync_action_list',
sync_list: 'sync_list',
},
winLyric: {
close: 'close',

View File

@ -153,8 +153,6 @@ exports.initSetting = isShowErrorAlert => {
// 迁移列表滚动位置设置 ~0.18.3
if (setting.list.scroll) {
let scroll = setting.list.scroll
electronStore_list.set('defaultList.location', scroll.locations.default || 0)
electronStore_list.set('loveList.location', scroll.locations.love || 0)
electronStore_config.delete('setting.list.scroll')
electronStore_config.set('setting.list.isSaveScrollLocation', scroll.enable)
delete setting.list.scroll

View File

@ -46,7 +46,7 @@ const { isMac, isLinux, initHotKey } = require('../common/utils')
// https://github.com/electron/electron/issues/18397
// 开发模式下为true时 多次引入native模块会导致渲染进程卡死
// https://github.com/electron/electron/issues/22791
app.allowRendererProcessReuse = !isDev
// app.allowRendererProcessReuse = !isDev
app.on('web-contents-created', (event, contents) => {

View File

@ -5,6 +5,7 @@ const modules = require('../modules')
const { authCode, authConnect } = require('./auth')
const { getAddress, getServerId, generateCode, getClientKeyInfo } = require('./utils')
const syncList = require('./syncList')
const { log } = require('@common/utils')
let status = {
@ -71,7 +72,7 @@ const handleStartServer = (port = 9527) => new Promise((resolve, reject) => {
serveClient: false,
connectTimeout: 10000,
pingTimeout: 30000,
maxHttpBufferSize: 3e6,
maxHttpBufferSize: 1e9, // 1G
allowRequest: authConnection,
transports: ['websocket'],
})
@ -88,7 +89,8 @@ const handleStartServer = (port = 9527) => new Promise((resolve, reject) => {
try {
await syncList(io, socket)
} catch (err) {
console.log(err)
// console.log(err)
log.warn(err)
return
}
status.devices.push(keyInfo)

View File

@ -11,6 +11,22 @@ let io
let syncingId = null
const wait = (time = 1000) => new Promise((resolve, reject) => setTimeout(resolve, time))
const patchListData = listData => {
return Object.assign({}, {
defaultList: {
id: 'default',
name: '试听列表',
list: [],
},
loveList: {
id: 'love',
name: '我的收藏',
list: [],
},
userList: [],
}, listData)
}
const getRemoteListData = socket => new Promise((resolve, reject) => {
console.log('getRemoteListData')
const handleError = reason => {
@ -23,7 +39,7 @@ const getRemoteListData = socket => new Promise((resolve, reject) => {
const data = JSON.parse(decryptMsg(socket.data.keyInfo, enData))
if (!data) return reject(new Error('Get remote list data failed'))
if (data.action != 'getData') return
resolve(data.data)
resolve(patchListData(data.data))
}
socket.on('disconnect', handleError)
@ -35,7 +51,7 @@ const getLocalListData = () => new Promise((resolve, reject) => {
const handleSuccess = ({ action, data }) => {
if (action !== 'getData') return
global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
resolve(data)
resolve(patchListData(data))
}
global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
global.lx_event.sync.sync_list({
@ -87,10 +103,10 @@ const updateSnapshot = (path, data) => {
}
const createListDataObj = listData => {
const listDataObj = {}
for (const list of listData.userList) listDataObj[list.id] = list
return listDataObj
const createUserListDataObj = listData => {
const userListDataObj = {}
for (const list of listData.userList) userListDataObj[list.id] = list
return userListDataObj
}
const handleMergeList = (sourceList, targetList, addMusicLocationType) => {
@ -137,11 +153,11 @@ const mergeList = (sourceListData, targetListData) => {
newListData.defaultList = handleMergeList(sourceListData.defaultList, targetListData.defaultList, addMusicLocationType)
newListData.loveList = handleMergeList(sourceListData.loveList, targetListData.loveList, addMusicLocationType)
const listDataObj = createListDataObj(sourceListData)
const userListDataObj = createUserListDataObj(sourceListData)
newListData.userList = [...sourceListData.userList]
for (const list of targetListData.userList) {
const targetList = listDataObj[list.id]
const targetList = userListDataObj[list.id]
if (targetList) {
targetList.list = handleMergeList(targetList, list, addMusicLocationType).list
} else {
@ -156,11 +172,11 @@ const overwriteList = (sourceListData, targetListData) => {
newListData.defaultList = sourceListData.defaultList
newListData.loveList = sourceListData.loveList
const listDataObj = createListDataObj(sourceListData)
const userListDataObj = createUserListDataObj(sourceListData)
newListData.userList = [...sourceListData.userList]
for (const list of targetListData.userList) {
const targetList = listDataObj[list.id]
const targetList = userListDataObj[list.id]
if (targetList) continue
newListData.userList.push(list)
}
@ -259,11 +275,10 @@ const mergeListDataFromSnapshot = (sourceList, targetList, snapshotList, addMusi
const targetListItemIds = new Set()
for (const m of sourceList.list) sourceListItemIds.add(m.songmid)
for (const m of targetList.list) targetListItemIds.add(m.songmid)
for (const m of snapshotList.list) {
if (!sourceListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
}
for (const m of snapshotList.list) {
if (!targetListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
if (snapshotList) {
for (const m of snapshotList.list) {
if (!sourceListItemIds.has(m.songmid) || !targetListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
}
}
let newList
@ -297,13 +312,12 @@ const mergeListDataFromSnapshot = (sourceList, targetList, snapshotList, addMusi
const handleMergeListDataFromSnapshot = async(socket, snapshot) => {
const addMusicLocationType = global.appSetting.list.addMusicLocationType
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleMergeListDataFromSnapshot', 'remoteListData, localListData')
const newListData = {}
newListData.defaultList = mergeListDataFromSnapshot(localListData.defaultList, remoteListData.defaultList, snapshot.defaultList, addMusicLocationType)
newListData.loveList = mergeListDataFromSnapshot(localListData.loveList, remoteListData.loveList, snapshot.loveList, addMusicLocationType)
const localUserListData = createListDataObj(localListData)
const remoteUserListData = createListDataObj(remoteListData)
const snapshotUserListData = createListDataObj(snapshot)
const localUserListData = createUserListDataObj(localListData)
const remoteUserListData = createUserListDataObj(remoteListData)
const snapshotUserListData = createUserListDataObj(snapshot)
const removedListIds = new Set()
const localUserListIds = new Set()
const remoteUserListIds = new Set()
@ -311,10 +325,7 @@ const handleMergeListDataFromSnapshot = async(socket, snapshot) => {
for (const l of remoteListData.userList) remoteUserListIds.add(l.id)
for (const l of snapshot.userList) {
if (!localUserListIds.has(l.id)) removedListIds.add(l.id)
}
for (const l of snapshot.userList) {
if (!remoteUserListIds.has(l.id)) removedListIds.add(l.id)
if (!localUserListIds.has(l.id) || !remoteUserListIds.has(l.id)) removedListIds.add(l.id)
}
let newUserList = []
@ -353,7 +364,7 @@ const registerUpdateSnapshotTask = (socket, snapshot) => {
if (loveList != null) snapshot.loveList = loveList
if (userList != null) snapshot.userList = userList
updateSnapshot(socket.data.snapshotFilePath, JSON.stringify(snapshot))
}, 10000)
}, 2000)
global.lx_event.common.on(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
socket.on('disconnect', () => {
global.lx_event.common.off(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
@ -373,7 +384,7 @@ const syncList = async socket => {
}
console.log('isSyncRequired', isSyncRequired)
if (isSyncRequired) return handleSyncList(socket)
return handleMergeListDataFromSnapshot(socket, fileData)
return handleMergeListDataFromSnapshot(socket, patchListData(fileData))
}
const checkSyncQueue = async() => {

View File

@ -1,5 +1,5 @@
const { app, Tray, Menu } = require('electron')
const { isWin } = require('../../common/utils')
const { app, Tray, Menu, nativeImage } = require('electron')
// const { isWin } = require('../../common/utils')
const { tray: TRAY_EVENT_NAME, common: COMMON_EVENT_NAME, mainWindow: MAIN_WINDOW_NAME } = require('../events/_name')
const path = require('path')
let isEnableTray = null
@ -7,12 +7,17 @@ let themeId = null
const themeList = [
{
id: 0,
fileName: 'tray0Template',
fileName: 'trayTemplate',
isNative: true,
},
{
id: 1,
fileName: 'tray1Template',
fileName: 'tray_origin',
isNative: false,
},
{
id: 2,
fileName: 'tray_black',
isNative: false,
},
]
@ -43,11 +48,11 @@ const createTray = () => {
if ((global.modules.tray && !global.modules.tray.isDestroyed()) || !global.appSetting.tray || !global.appSetting.tray.isShow) return
themeId = global.appSetting.tray.themeId
let themeName = (themeList.find(item => item.id === themeId) || themeList[0]).fileName
const iconPath = path.join(global.__static, 'images/tray', isWin ? themeName + '@2x.ico' : themeName + '.png')
let theme = themeList.find(item => item.id === themeId) || themeList[0]
const iconPath = path.join(global.__static, 'images/tray', theme.fileName + '.png')
// 托盘
global.modules.tray = new Tray(iconPath)
global.modules.tray = new Tray(nativeImage.createFromPath(iconPath))
global.modules.tray.setToolTip('洛雪音乐助手')
createMenu(global.modules.tray)
@ -140,7 +145,7 @@ const createMenu = tray => {
const setTrayImage = themeId => {
if (!global.modules.tray) return
let themeName = (themeList.find(item => item.id === themeId) || themeList[0]).fileName
const iconPath = path.join(global.__static, 'images/tray', isWin ? themeName + '@2x.ico' : themeName + '.png')
global.modules.tray.setImage(iconPath)
let theme = themeList.find(item => item.id === themeId) || themeList[0]
const iconPath = path.join(global.__static, 'images/tray', theme.fileName + '.png')
global.modules.tray.setImage(nativeImage.createFromPath(iconPath))
}

View File

@ -66,6 +66,7 @@ const handleRequest = (context, { requestKey, data }) => {
*
* @param {*} context
* @param {*} info {
* openDevTools: false,
* status: true,
* message: 'xxx',
* sources: {
@ -184,7 +185,7 @@ contextBridge.exposeInMainWorld('lx', {
utils: {
crypto: {
aesEncrypt(buffer, mode, key, iv) {
const cipher = createCipheriv('aes-128-' + mode, key, iv)
const cipher = createCipheriv(mode, key, iv)
return Buffer.concat([cipher.update(buffer), cipher.final()])
},
rsaEncrypt(buffer, key) {
@ -202,8 +203,12 @@ contextBridge.exposeInMainWorld('lx', {
from(...args) {
return Buffer.from(...args)
},
bufToString(buf, format) {
return Buffer.from(buf, 'binary').toString(format)
},
},
},
version: '1.1.0',
// removeEvent(eventName, handler) {
// if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
// let handlers

View File

@ -19,9 +19,9 @@ exports.importApi = script => {
let name = scriptInfo[1] || ''
let description = scriptInfo[2] || ''
name = name.startsWith(' * @name ') ? name.replace(' * @name ', '').trim() : `user_api_${new Date().toLocaleString()}`
if (name.length > 10) name = name.substring(0, 10) + '...'
if (name.length > 24) name = name.substring(0, 24) + '...'
description = description.startsWith(' * @description ') ? description.replace(' * @description ', '').trim() : ''
if (description.length > 20) description = description.substring(0, 20) + '...'
if (description.length > 36) description = description.substring(0, 36) + '...'
const apiInfo = {
id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`,
name,

View File

@ -1,7 +1,7 @@
const path = require('path')
const { BrowserWindow } = require('electron')
const { winLyric: WIN_LYRIC_EVENT_NAME } = require('../../events/_name')
const { debounce } = require('../../../common/utils')
const { debounce, isLinux } = require('../../../common/utils')
const { getLyricWindowBounds } = require('./utils')
require('./event')
@ -66,6 +66,10 @@ const winEvent = lyricWindow => {
if (global.appSetting.desktopLyric.isLock) {
global.modules.lyricWindow.setIgnoreMouseEvents(true, { forward: false })
}
// linux下每次重开时貌似要重新设置置顶
if (isLinux && global.appSetting.desktopLyric.isAlwaysOnTop) {
global.modules.lyricWindow.setAlwaysOnTop(global.appSetting.desktopLyric.isAlwaysOnTop, 'screen-saver')
}
})
}

View File

@ -25,3 +25,5 @@ require('./kw_decodeLyric')
require('./userApi')
require('./sync')
require('./s2t')

View File

@ -0,0 +1,9 @@
const { mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
const { tranditionalize } = require('../utils/simplify-chinese-main')
mainHandle(ipcMainWindowNames.lang_s2t, async(event, textBase64) => {
if (!global.modules.mainWindow) throw new Error('mainWindow is undefined')
const text = tranditionalize(Buffer.from(textBase64, 'base64').toString())
return Buffer.from(text).toString('base64')
})

View File

@ -0,0 +1,18 @@
todo.md
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
tsconfig.tsbuildinfo
report.*.json
.eslintcache
.DS_Store
.idea
.vscode
.yarn
*.suo
*.ntvs*
*.njsproj
*.sln

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2021 Shigma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,10 @@
# simplify-chinese
Convert chinese characters between simplified form and tranditional form / 汉字简繁体转换工具。
```js
const { simplify, tranditionalize } = require('simplify-chinese')
console.log(simplify('窩窩頭')) // 窝窝头
console.log(tranditionalize('窝窝头')) // 窩窩頭
```

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
export function simplify(source: string): string
export function tranditionalize(source: string): string

View File

@ -0,0 +1,30 @@
const { simplified, traditional } = require('./chinese')
const stMap = new Map()
const tsMap = new Map()
simplified.split('').forEach((char, index) => {
stMap.set(char, traditional[index])
tsMap.set(traditional[index], char)
})
function simplify(source) {
let result = []
for (const char of source) {
result.push(tsMap.get(char) || char)
}
return result.join('')
}
function tranditionalize(source) {
let result = []
for (const char of source) {
result.push(stMap.get(char) || char)
}
return result.join('')
}
module.exports = {
simplify,
tranditionalize,
}

View File

@ -0,0 +1,10 @@
{
"name": "simplify-chinese",
"description": "Convert chinese characters between simplified form and tranditional form 汉字简繁体转换工具",
"version": "1.1.0",
"main": "index.js",
"typings": "index.d.ts",
"repository": "https://github.com/koishijs/simplify-chinese.git",
"author": "Shigma <1700011071@pku.edu.cn>",
"license": "MIT"
}

View File

@ -5,14 +5,14 @@
.control-bar(v-show="!lrcConfig.isLock")
core-control-bar(:lrcConfig="lrcConfig" :themes="themeList")
core-lyric(:lrcConfig="lrcConfig" :isPlayLxlrc="isPlayLxlrc" :isShowLyricTranslation="isShowLyricTranslation")
div.resize-left(@mousedown.self="handleMouseDown('left', $event)")
div.resize-top(@mousedown.self="handleMouseDown('top', $event)")
div.resize-right(@mousedown.self="handleMouseDown('right', $event)")
div.resize-bottom(@mousedown.self="handleMouseDown('bottom', $event)")
div.resize-top-left(@mousedown.self="handleMouseDown('top-left', $event)")
div.resize-top-right(@mousedown.self="handleMouseDown('top-right', $event)")
div.resize-bottom-left(@mousedown.self="handleMouseDown('bottom-left', $event)")
div.resize-bottom-right(@mousedown.self="handleMouseDown('bottom-right', $event)")
div.resize-left(@mousedown.self="handleMouseDown('left', $event)" @touchstart.self="handleTouchDown('left', $event)")
div.resize-top(@mousedown.self="handleMouseDown('top', $event)" @touchstart.self="handleTouchDown('top', $event)")
div.resize-right(@mousedown.self="handleMouseDown('right', $event)" @touchstart.self="handleTouchDown('right', $event)")
div.resize-bottom(@mousedown.self="handleMouseDown('bottom', $event)" @touchstart.self="handleTouchDown('bottom', $event)")
div.resize-top-left(@mousedown.self="handleMouseDown('top-left', $event)" @touchstart.self="handleTouchDown('top-left', $event)")
div.resize-top-right(@mousedown.self="handleMouseDown('top-right', $event)" @touchstart.self="handleTouchDown('top-right', $event)")
div.resize-bottom-left(@mousedown.self="handleMouseDown('bottom-left', $event)" @touchstart.self="handleTouchDown('bottom-left', $event)")
div.resize-bottom-right(@mousedown.self="handleMouseDown('bottom-right', $event)" @touchstart.self="handleTouchDown('bottom-right', $event)")
core-icons
</template>
@ -116,16 +116,34 @@ export default {
this.isPlayLxlrc = isPlayLxlrc
if (this.$i18n.locale !== languageId && languageId != null) this.$i18n.locale = languageId
},
handleMouseDown(origin, event) {
handleDown(origin, clientX, clientY) {
this.handleMouseUp()
this.resize.origin = origin
this.resize.msDownX = event.clientX
this.resize.msDownY = event.clientY
this.resize.msDownX = clientX
this.resize.msDownY = clientY
},
handleMouseUp() {
this.resize.origin = null
},
handleMouseDown(origin, event) {
this.handleDown(origin, event.clientX, event.clientY)
},
handleTouchDown(origin, event) {
if (event.changedTouches.length) {
const touch = event.changedTouches[0]
this.handleDown(origin, touch.clientX, touch.clientY)
}
},
handleMouseMove(event) {
this.handleMove(event.clientX, event.clientY)
},
handleTouchMove(event) {
if (event.changedTouches.length) {
const touch = event.changedTouches[0]
this.handleMove(touch.clientX, touch.clientY)
}
},
handleMove(clientX, clientY) {
if (!this.resize.origin) return
// if (!event.target.classList.contains('resize-' + this.resize.origin)) return
// console.log(event.target)
@ -136,49 +154,49 @@ export default {
let temp
switch (this.resize.origin) {
case 'left':
temp = event.clientX - this.resize.msDownX
temp = clientX - this.resize.msDownX
bounds.w = -temp
bounds.x = temp
break
case 'right':
bounds.w = event.clientX - this.resize.msDownX
bounds.w = clientX - this.resize.msDownX
this.resize.msDownX += bounds.w
break
case 'top':
temp = event.clientY - this.resize.msDownY
temp = clientY - this.resize.msDownY
bounds.y = temp
bounds.h = -temp
break
case 'bottom':
bounds.h = event.clientY - this.resize.msDownY
bounds.h = clientY - this.resize.msDownY
this.resize.msDownY += bounds.h
break
case 'top-left':
temp = event.clientX - this.resize.msDownX
temp = clientX - this.resize.msDownX
bounds.w = -temp
bounds.x = temp
temp = event.clientY - this.resize.msDownY
temp = clientY - this.resize.msDownY
bounds.y = temp
bounds.h = -temp
break
case 'top-right':
temp = event.clientY - this.resize.msDownY
temp = clientY - this.resize.msDownY
bounds.y = temp
bounds.h = -temp
bounds.w = event.clientX - this.resize.msDownX
bounds.w = clientX - this.resize.msDownX
this.resize.msDownX += bounds.w
break
case 'bottom-left':
temp = event.clientX - this.resize.msDownX
temp = clientX - this.resize.msDownX
bounds.w = -temp
bounds.x = temp
bounds.h = event.clientY - this.resize.msDownY
bounds.h = clientY - this.resize.msDownY
this.resize.msDownY += bounds.h
break
case 'bottom-right':
bounds.w = event.clientX - this.resize.msDownX
bounds.w = clientX - this.resize.msDownX
this.resize.msDownX += bounds.w
bounds.h = event.clientY - this.resize.msDownY
bounds.h = clientY - this.resize.msDownY
this.resize.msDownY += bounds.h
break
}
@ -187,9 +205,9 @@ export default {
bounds.h = window.innerHeight + bounds.h
rendererSend(NAMES.winLyric.set_win_bounds, bounds)
},
handleMouseOver() {
// this.handleMouseUp()
},
// handleMouseOver() {
// // this.handleMouseUp()
// },
},
}
</script>

View File

@ -1,5 +1,6 @@
<template lang="pug">
div(:class="[$style.lyric, { [$style.draging]: lyricEvent.isMsDown }, { [$style.lrcActiveZoom]: lrcConfig.style.isZoomActiveLrc } ]" :style="lrcStyles" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="[$style.lyric, { [$style.draging]: lyricEvent.isMsDown }, { [$style.lrcActiveZoom]: lrcConfig.style.isZoomActiveLrc } ]"
:style="lrcStyles" @wheel="handleWheel" @mousedown="handleLyricMouseDown" @touchstart="handleLyricTouchStart" ref="dom_lyric")
div(:class="$style.lyricSpace")
div(:class="[$style.lyricText]" ref="dom_lyric_text")
//- div(v-for="(info, index) in lyricLines" :key="index" :class="[$style.lineContent, lyric.line == index ? (lrcConfig.style.isZoomActiveLrc ? $style.lrcActiveZoom : $style.lrcActive) : null]")
@ -167,12 +168,16 @@ export default {
mounted() {
document.addEventListener('mousemove', this.handleMouseMsMove)
document.addEventListener('mouseup', this.handleMouseMsUp)
document.addEventListener('touchmove', this.handleTouchMove)
document.addEventListener('touchend', this.handleMouseMsUp)
rendererSend(NAMES.winLyric.get_lyric_info, 'info')
},
beforeDestroy() {
this.clearLyricScrollTimeout()
document.removeEventListener('mousemove', this.handleMouseMsMove)
document.removeEventListener('mouseup', this.handleMouseMsUp)
document.removeEventListener('touchmove', this.handleTouchMove)
document.removeEventListener('touchend', this.handleMouseMsUp)
},
methods: {
handleSetInfo({ type, data }) {
@ -234,49 +239,59 @@ export default {
let dom_p = this.dom_lines[this.lyric.line]
cancelScrollFn = scrollTo(this.$refs.dom_lyric, dom_p ? (dom_p.offsetTop - this.$refs.dom_lyric.clientHeight * 0.5 + dom_p.clientHeight / 2) : 0)
},
handleLyricMouseDown(e) {
if (e.target.classList.contains('font') ||
e.target.parentNode.classList.contains('font') ||
e.target.classList.contains('translation') ||
e.target.parentNode.classList.contains('translation')) {
handleLyricDown(target, x, y) {
if (target.classList.contains('font') ||
target.parentNode.classList.contains('font') ||
target.classList.contains('translation') ||
target.parentNode.classList.contains('translation')) {
this.lyricEvent.isMsDown = true
this.lyricEvent.msDownY = e.clientY
this.lyricEvent.msDownY = y
this.lyricEvent.msDownScrollY = this.$refs.dom_lyric.scrollTop
} else {
this.winEvent.isMsDown = true
this.winEvent.msDownX = e.clientX
this.winEvent.msDownY = e.clientY
this.winEvent.msDownX = x
this.winEvent.msDownY = y
}
},
handleLyricMouseDown(e) {
this.handleLyricDown(e.target, e.clientX, e.clientY)
},
handleLyricTouchStart(e) {
if (e.changedTouches.length) {
const touch = e.changedTouches[0]
this.handleLyricDown(e.target, touch.clientX, touch.clientY)
}
},
handleMouseMsUp(e) {
this.lyricEvent.isMsDown = false
this.winEvent.isMsDown = false
},
handleMouseMsMove(e) {
handleMove(x, y) {
if (this.lyricEvent.isMsDown) {
if (!this.lyricEvent.isStopScroll) this.lyricEvent.isStopScroll = true
if (cancelScrollFn) {
cancelScrollFn()
cancelScrollFn = null
}
this.$refs.dom_lyric.scrollTop = this.lyricEvent.msDownScrollY + this.lyricEvent.msDownY - e.clientY
this.$refs.dom_lyric.scrollTop = this.lyricEvent.msDownScrollY + this.lyricEvent.msDownY - y
this.startLyricScrollTimeout()
} else if (this.winEvent.isMsDown) {
rendererSend(NAMES.winLyric.set_win_bounds, {
x: e.clientX - this.winEvent.msDownX,
y: e.clientY - this.winEvent.msDownY,
x: x - this.winEvent.msDownX,
y: y - this.winEvent.msDownY,
w: window.innerWidth,
h: window.innerHeight,
})
}
// if (this.volumeEvent.isMsDown) {
// let val = this.volumeEvent.msDownValue + (e.clientX - this.volumeEvent.msDownX) / 70
// this.volume = val < 0 ? 0 : val > 1 ? 1 : val
// if (this.audio) this.audio.volume = this.volume
// }
// console.log(val)
},
handleTouchMove(e) {
if (e.changedTouches.length) {
const touch = e.changedTouches[0]
this.handleMove(touch.clientX, touch.clientY)
}
},
handleMouseMsMove(e) {
this.handleMove(e.clientX, e.clientY)
},
startLyricScrollTimeout() {
this.clearLyricScrollTimeout()

View File

@ -29,6 +29,7 @@ import music from './utils/music'
import { throttle, openUrl, compareVer, getPlayList, parseUrlParams, saveSetting } from './utils'
import { base as eventBaseName, sync as eventSyncName } from './event/names'
import apiSourceInfo from './utils/music/api-source-info'
import { initListPosition, initListPrevSelectId } from '@renderer/utils/data'
window.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS
@ -147,7 +148,7 @@ export default {
},
downloadList: {
handler(n) {
this.saveDownloadList(n)
this.saveDownloadList(window.downloadListFull)
},
deep: true,
},
@ -338,6 +339,8 @@ export default {
return Promise.all([
this.initMyList(), //
this.initSearchHistoryList(), //
initListPosition(), //
initListPrevSelectId(), //
])
// this.initDownloadList() //
},
@ -370,12 +373,17 @@ export default {
},
initDownloadList(downloadList) {
if (downloadList) {
downloadList.forEach(item => {
downloadList = downloadList.filter(item => item && item.key && item.musicInfo)
for (const item of downloadList) {
if (item.name == null) {
item.name = `${item.musicInfo.name} - ${item.musicInfo.singer}`
item.songmid = item.musicInfo.songmid
}
if (item.status == this.downloadStatus.RUN || item.status == this.downloadStatus.WAITING) {
item.status = this.downloadStatus.PAUSE
item.statusText = '暂停下载'
}
})
}
this.updateDownloadList(downloadList)
}
},
@ -396,10 +404,17 @@ export default {
if (!info) return
if (info.index < 0) return
if (info.listId) {
const list = window.allList[info.listId]
// console.log(list)
if (!list || !list.list[info.index]) return
info.list = list.list
if (info.listId == 'download') {
const list = this.downloadList
// console.log(list)
if (!list || !list[info.index]) return
info.list = list
} else {
const list = window.allList[info.listId]
// console.log(list)
if (!list || !list.list[info.index]) return
info.list = list.list
}
}
if (!info.list || !info.list[info.index]) return
window.restorePlayInfo = info

View File

@ -1,5 +1,7 @@
@import './reset.less';
@import './animate.less';
@import './layout.less';
*, *::after, *::before {
-webkit-user-drag: none;
}
@ -72,6 +74,45 @@ table {
}
}
.list {
width: 100%;
overflow: hidden;
color: @color-theme_2-font;
.list-item {
height: 100%;
display: flex;
flex-flow: row nowrap;
align-items: center;
// border-top: 1px solid rgba(0, 0, 0, 0.12);
transition: background-color 0.2s ease;
border-bottom: 1px solid @color-theme_2-line;
box-sizing: border-box;
&:hover {
background-color: @color-theme_2-hover;
}
&.active {
background-color: @color-theme_2-active;
}
&.selected {
background-color: @color-theme_2-hover;
}
.list-item-cell {
flex: none;
padding: 0 6px;
position: relative;
transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 13px;
line-height: 16px;
vertical-align: middle;
box-sizing: border-box;
.mixin-ellipsis-1;
&.auto {
flex: auto;
}
}
}
}
.badge {
display: inline-block;
@ -240,6 +281,22 @@ each(@themes, {
}
}
.list {
color: ~'@{color-@{value}-theme_2-font}';
.list-item {
border-bottom-color: ~'@{color-@{value}-theme_2-line}';
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&.active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
&.selected {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
}
}
input, textarea {
&::placeholder {
color: ~'@{color-@{value}-theme_2-font-label}';

View File

@ -35,7 +35,7 @@ div(:class="$style.aside")
dl
//- dt {{$t('core.aside.my_music')}}
dd
router-link(:active-class="$style.active" :tips="$t('core.aside.my_list')" :to="`list?id=${setting.list.prevSelectListId || defaultList.id}`")
router-link(:active-class="$style.active" to="list" :tips="$t('core.aside.my_list')")
div(:class="$style.icon")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 444.87 391.18' space='preserve')
use(xlink:href='#icon-love')

View File

@ -217,5 +217,10 @@ svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/19
g#icon-comment(fill='currentColor')
// 0 0 24 24
path(d='M16 11H8V9H16V11M22 4V16C22 17.11 21.11 18 20 18H13.9L10.2 21.71C10 21.9 9.75 22 9.5 22H9C8.45 22 8 21.55 8 21V18H4C2.9 18 2 17.11 2 16V4C2 2.89 2.9 2 4 2H20C21.11 2 22 2.9 22 4M20 4H4V16H10V19.08L13.08 16H20V4')
g#icon-text(fill='currentColor')
// 0 0 24 24
path(fill='currentColor', d='M21,6V8H3V6H21M3,18H12V16H3V18M3,13H21V11H3V13Z')
</template>

View File

@ -5,7 +5,7 @@ div(:class="$style.player")
svg(v-else version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='102%' width='100%' viewBox='0 0 60 60' space='preserve')
use(:xlink:href='`#${$style.iconPic}`')
div(:class="$style.middle")
div(:class="$style.middleContainer" v-if="!isShowPlayerDetail")
div(:class="$style.middleContainer")
div(:class="$style.column1")
div(:class="$style.container")
div(:class="$style.title" @click="handleCopy(title)" :tips="title + $t('core.player.copy_title')") {{title}}
@ -64,14 +64,14 @@ div(:class="$style.player")
//- transition(enter-active-class="animated lightSpeedIn"
transition(enter-active-class="animated lightSpeedIn"
leave-active-class="animated slideOutDown")
core-player-detail(v-if="isShowPlayerDetail" :musicInfo="listId == 'download' ? targetSong.musicInfo : targetSong"
core-player-detail(v-if="isShowPlayerDetail" :visible.sync="isShowPlayerDetail" :musicInfo="currentMusicInfo"
:lyric="lyric" :list="list" :listId="listId"
:playInfo="{ nowPlayTimeStr, maxPlayTimeStr, progress, nowPlayTime, status }"
:isPlay="isPlay" @action="handlePlayDetailAction"
:nextTogglePlayName="nextTogglePlayName"
@toggle-next-play-mode="toggleNextPlayMode" @add-music-to="addMusicTo")
material-list-add-modal(:show="isShowAddMusicTo" :musicInfo="listId == 'download' ? targetSong.musicInfo : targetSong" @close="isShowAddMusicTo = false")
material-list-add-modal(:show="isShowAddMusicTo" :musicInfo="currentMusicInfo" @close="isShowAddMusicTo = false")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' style="display: none;")
defs
g(:id="$style.iconPic")
@ -87,7 +87,7 @@ div(:class="$style.player")
<script>
import Lyric from '@renderer/utils/lyric-font-player'
import { rendererSend, rendererOn, NAMES } from '../../../common/ipc'
import { rendererSend, rendererOn, NAMES, rendererInvoke } from '../../../common/ipc'
import { formatPlayTime2, getRandom, checkPath, setTitle, clipboardWriteText, debounce, throttle, assertApiSupport } from '../../utils'
import { mapGetters, mapActions, mapMutations } from 'vuex'
import { requestMsg } from '../../utils/message'
@ -121,6 +121,7 @@ export default {
singer: '',
album: '',
},
currentMusicInfo: {},
pregessWidth: 0,
lyric: {
lines: [],
@ -141,11 +142,12 @@ export default {
playTime: 0,
},
isShowAddMusicTo: false,
isShowPlayerDetail: false,
}
},
computed: {
...mapGetters(['setting']),
...mapGetters('player', ['list', 'changePlay', 'playMusicInfo', 'isShowPlayerDetail', 'playInfo', 'playedList']),
...mapGetters('player', ['list', 'changePlay', 'playMusicInfo', 'playInfo', 'playedList']),
// pic() {
// return this.musicInfo.img ? this.musicInfo.img : ''
// },
@ -340,7 +342,6 @@ export default {
'setPlayMusicInfo',
'setPlayIndex',
'resetChangePlay',
'visiblePlayerDetail',
'clearPlayedList',
'setPlayedList',
]),
@ -395,7 +396,7 @@ export default {
// console.log(this.retryNum)
if (!this.restorePlayTime) this.restorePlayTime = audio.currentTime //
this.retryNum++
this.setUrl(this.targetSong, true)
this.setUrl(this.currentMusicInfo, true)
this.status = this.statusText = this.$t('core.player.refresh_url')
return
}
@ -416,6 +417,8 @@ export default {
if (!this.targetSong.interval && this.listId != 'download') {
this.updateMusicInfo({ listId: this.listId, id: this.targetSong.songmid, musicInfo: this.targetSong, data: { interval: formatPlayTime2(this.maxPlayTime) } })
}
this.updatePositionState()
})
audio.addEventListener('loadstart', () => {
console.log('loadstart')
@ -430,6 +433,7 @@ export default {
audio.currentTime = playTime
}
this.clearBufferTimeout()
this.updatePositionState()
// if (this.musicInfo.lrc) window.lrc.play(audio.currentTime * 1000)
this.status = this.statusText = ''
@ -483,9 +487,8 @@ export default {
},
async play() {
this.clearDelayNextTimeout()
this.updateMediaSessionInfo()
const targetSong = this.targetSong
let targetSong = this.targetSong
if (this.setting.player.togglePlayMethod == 'random' && !this.playMusicInfo.isTempPlay) this.setPlayedList(this.playMusicInfo)
this.retryNum = 0
@ -497,24 +500,26 @@ export default {
if (!await checkPath(filePath) || !targetSong.isComplate || /\.ape$/.test(filePath)) {
return this.list.length == 1 ? null : this.playNext()
}
this.musicInfo.songmid = targetSong.musicInfo.songmid
this.musicInfo.singer = targetSong.musicInfo.singer
this.musicInfo.name = targetSong.musicInfo.name
this.currentMusicInfo = targetSong = window.downloadListFullMap.get(targetSong.key).musicInfo
this.musicInfo.songmid = targetSong.songmid
this.musicInfo.singer = targetSong.singer
this.musicInfo.name = targetSong.name
this.musicInfo.album = targetSong.albumName
audio.src = filePath
// console.log(filePath)
this.setImg(targetSong.musicInfo)
this.setLrc(targetSong.musicInfo)
} else {
// if (!this.assertApiSupport(targetSong.source)) return this.playNext()
this.currentMusicInfo = targetSong
this.musicInfo.songmid = targetSong.songmid
this.musicInfo.singer = targetSong.singer
this.musicInfo.name = targetSong.name
this.musicInfo.album = targetSong.albumName
this.setUrl(targetSong)
this.setImg(targetSong)
this.setLrc(targetSong)
}
this.updateMediaSessionInfo()
this.setImg(targetSong)
this.setLrc(targetSong)
this.handleUpdateWinLyricInfo('music_info', {
songmid: this.musicInfo.songmid,
singer: this.musicInfo.singer,
@ -592,11 +597,25 @@ export default {
window.getComputedStyle(this.$refs.dom_progress, null).width,
)
},
togglePlay() {
async togglePlay() {
if (!audio.src) {
if (this.restorePlayTime != null) {
// if (!this.assertApiSupport(this.targetSong.source)) return this.playNext()
this.setUrl(this.targetSong)
if (this.listId == 'download') {
const filePath = path.join(this.setting.download.savePath, this.targetSong.fileName)
// console.log(filePath)
if (!await checkPath(filePath) || !this.targetSong.isComplate || /\.ape$/.test(filePath)) {
if (this.list.length == 1) {
this.handleRemoveMusic()
} else {
this.playNext()
}
return
}
audio.src = filePath
} else {
// if (!this.assertApiSupport(this.targetSong.source)) return this.playNext()
this.setUrl(this.targetSong)
}
}
return
}
@ -634,6 +653,7 @@ export default {
audio.src = this.musicInfo.url = url
}).catch(err => {
// console.log('err', err.message)
if (targetSong !== this.targetSong || this.isPlay) return
if (err.message == requestMsg.cancelRequest) return
if (!isRetryed) return this.setUrl(targetSong, isRefresh, true)
this.status = this.statusText = err.message
@ -655,12 +675,31 @@ export default {
setLrc(targetSong) {
this.getLrc(targetSong).then(({ lyric, tlyric, lxlyric }) => {
if (targetSong.songmid !== this.musicInfo.songmid) return
this.musicInfo.lrc = lyric
this.musicInfo.tlrc = tlyric
this.musicInfo.lxlrc = lxlyric
}).catch(() => {
return (
global.i18n.locale == 'zh-tw'
? Promise.all([
lyric
? rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(lyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())
: Promise.resolve(''),
tlyric
? rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(tlyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())
: Promise.resolve(''),
lxlyric
? rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(lxlyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())
: Promise.resolve(''),
])
: Promise.resolve([lyric, tlyric, lxlyric])
).then(([lyric, tlyric, lxlyric]) => {
this.musicInfo.lrc = lyric
this.musicInfo.tlrc = tlyric
this.musicInfo.lxlrc = lxlyric
})
}).catch((err) => {
console.log(err)
if (targetSong.songmid !== this.musicInfo.songmid) return
this.status = this.statusText = this.$t('core.player.lyric_error')
}).finally(() => {
if (targetSong.songmid !== this.musicInfo.songmid) return
this.handleUpdateWinLyricInfo('lyric', { lrc: this.musicInfo.lrc, tlrc: this.musicInfo.tlrc, lxlrc: this.musicInfo.lxlrc })
this.setLyric()
})
@ -684,6 +723,7 @@ export default {
this.lyric.text = 0
this.handleUpdateWinLyricInfo('lines', [])
this.handleUpdateWinLyricInfo('line', 0)
this.currentMusicInfo = {}
},
sendProgressEvent(status, mode) {
// console.log(status)
@ -740,7 +780,7 @@ export default {
this.setProgressWidth()
},
handleToMusicLocation() {
if (!this.listId || this.listId == '__temp__' || this.listId == 'download') return
if (!this.listId || this.listId == '__temp__' || this.listId == 'download' || !this.currentMusicInfo.songmid) return
if (this.playIndex == -1) return
this.$router.push({
path: 'list',
@ -751,8 +791,8 @@ export default {
})
},
showPlayerDetail() {
if (!this.targetSong) return
this.visiblePlayerDetail(true)
if (!this.currentMusicInfo.songmid) return
this.isShowPlayerDetail = true
},
handleTransitionEnd(e) {
// console.log(e)
@ -849,7 +889,7 @@ export default {
this.playNext()
break
case 'progress':
this.handleSetProgress(data)
this.setProgress(data * this.maxPlayTime)
break
case 'volume':
break
@ -895,8 +935,18 @@ export default {
if (!this.musicInfo.songmid) return
this.isShowAddMusicTo = true
},
handleRestorePlay(restorePlayInfo) {
let musicInfo = this.list[restorePlayInfo.index]
async handleRestorePlay(restorePlayInfo) {
let musicInfo
if (this.listId == 'download') {
this.currentMusicInfo = musicInfo = window.downloadListFullMap.get(this.list[restorePlayInfo.index].key).musicInfo
// console.log(filePath)
} else {
// if (!this.assertApiSupport(targetSong.source)) return this.playNext()
musicInfo = this.list[restorePlayInfo.index]
this.currentMusicInfo = musicInfo
}
this.musicInfo.songmid = musicInfo.songmid
this.musicInfo.singer = musicInfo.singer
this.musicInfo.name = musicInfo.name
@ -919,13 +969,20 @@ export default {
},
updateMediaSessionInfo() {
const mediaMetadata = {
title: this.targetSong.name,
artist: this.targetSong.singer,
album: this.targetSong.albumName,
title: this.currentMusicInfo.name,
artist: this.currentMusicInfo.singer,
album: this.currentMusicInfo.albumName,
}
if (this.targetSong.img) mediaMetadata.artwork = [{ src: this.targetSong.img }]
if (this.currentMusicInfo.img) mediaMetadata.artwork = [{ src: this.currentMusicInfo.img }]
navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata)
},
updatePositionState() {
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: audio.currentTime,
})
},
registerMediaSessionHandler() {
// navigator.mediaSession.setActionHandler('play', () => {
// if (this.isPlay || !this.playMusicInfo) return

View File

@ -4,7 +4,7 @@
//- div(:class="$style.bg2")
div(:class="$style.header")
div(:class="$style.controBtn")
button(type="button" :class="$style.hide" :tips="$t('core.player.hide_detail')" @click="visiblePlayerDetail(false)")
button(type="button" :class="$style.hide" :tips="$t('core.player.hide_detail')" @click="hide")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='80%' viewBox='0 0 30.727 30.727' space='preserve')
use(xlink:href='#icon-window-hide')
button(type="button" :class="$style.min" :tips="$t('core.toolbar.min')" @click="min")
@ -27,17 +27,28 @@
p(v-if="musicInfo.album") {{$t('core.player.album')}}{{musicInfo.album}}
div(:class="$style.right")
div(:class="[$style.lyric, lyricEvent.isMsDown ? $style.draging : null]" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="[$style.lyric, { [$style.draging]: lyricEvent.isMsDown }]" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="$style.lyricSpace")
div(:class="[$style.lyricText]" ref="dom_lyric_text")
//- p(v-for="(info, index) in lyricLines" :key="index" :class="lyric.line == index ? $style.lrcActive : null") {{info.text}}
div(:class="$style.lyricSpace")
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
div(:class="[$style.lyricSelectContent, 'select', 'scroll']" v-if="isShowLrcSelectContent" @contextmenu="handleCopySelectText")
//- div(:class="$style.lyricSpace")
div(v-for="(info, index) in lyricLines" :key="index" :class="[$style.lyricSelectline, { [$style.lrcActive]: lyric.line == index }]")
span {{info.text}}
br(v-if="info.translation")
span(:class="$style.lyricSelectlineTransition") {{info.translation}}
//- div(:class="$style.lyricSpace")
material-music-comment(:class="$style.comment" :titleFormat="this.setting.download.fileName" :musicInfo="musicInfo" v-model="isShowComment")
div(:class="$style.footer")
div(:class="$style.footerLeft")
div(:class="$style.footerLeftControlBtns")
div(:class="[$style.footerLeftControlBtn, { [$style.active]: isShowLrcSelectContent }]" @click="isShowLrcSelectContent = !isShowLrcSelectContent" :tips="$t('core.player.lyric_select')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-text')
div(:class="[$style.footerLeftControlBtn, isShowComment ? $style.active : null]" @click="isShowComment = !isShowComment" :tips="$t('core.player.comment_show')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-comment')
@ -85,14 +96,18 @@
<script>
import { mapGetters, mapMutations } from 'vuex'
import { mapGetters } from 'vuex'
import { base as eventBaseName } from '../../event/names'
import { scrollTo } from '../../utils'
import { clipboardWriteText, scrollTo } from '../../utils'
let cancelScrollFn = null
export default {
props: {
visible: {
type: Boolean,
required: true,
},
musicInfo: {
type: Object,
default() {
@ -230,32 +245,40 @@ export default {
lyricLines: [],
isSetedLines: false,
isShowComment: false,
isShowLrcSelectContent: false,
}
},
mounted() {
this.$nextTick(() => {
this.setProgressWidth()
})
document.addEventListener('mousemove', this.handleMouseMsMove)
document.addEventListener('mouseup', this.handleMouseMsUp)
window.addEventListener('resize', this.handleResize)
this.listenEvent()
// console.log('object', this.$refs.dom_lyric_text)
this.setLyric(this.lyricLines)
},
beforeDestroy() {
this.unlistenEvent()
this.clearLyricScrollTimeout()
document.removeEventListener('mousemove', this.handleMouseMsMove)
document.removeEventListener('mouseup', this.handleMouseMsUp)
window.removeEventListener('resize', this.handleResize)
},
computed: {
...mapGetters(['setting']),
...mapGetters('player', ['isShowPlayerDetail']),
},
methods: {
...mapMutations('player', [
'visiblePlayerDetail',
]),
hide() {
this.$emit('update:visible', false)
},
listenEvent() {
document.addEventListener('mousemove', this.handleMouseMsMove)
document.addEventListener('mouseup', this.handleMouseMsUp)
window.addEventListener('resize', this.handleResize)
},
unlistenEvent() {
document.removeEventListener('mousemove', this.handleMouseMsMove)
document.removeEventListener('mouseup', this.handleMouseMsUp)
window.removeEventListener('resize', this.handleResize)
},
setLyric(lines) {
const dom_lines = document.createDocumentFragment()
for (const line of lines) {
@ -282,7 +305,7 @@ export default {
setProgress(event) {
this.$emit('action', {
type: 'progress',
data: event,
data: event.offsetX / this.pregessWidth,
})
},
setProgressWidth() {
@ -296,7 +319,7 @@ export default {
return
}
this.clickTime = 0
this.visiblePlayerDetail(false)
this.hide()
},
handleLyricMouseDown(e) {
// console.log(e)
@ -359,6 +382,12 @@ export default {
close() {
window.eventHub.$emit(eventBaseName.close)
},
handleCopySelectText() {
let str = window.getSelection().toString()
str = str.trim()
if (!str.length) return
clipboardWriteText(str)
},
},
}
</script>
@ -485,7 +514,8 @@ export default {
min-height: 0;
overflow: hidden;
display: flex;
padding: 0 30px;
margin: 0 30px;
position: relative;
&.showComment {
.left {
@ -501,13 +531,13 @@ export default {
}
}
.comment {
flex-basis: 50%;
opacity: 1;
transform: scaleX(1);
}
}
}
.left {
flex: 40%;
flex: 0 0 40%;
display: flex;
flex-flow: column nowrap;
align-items: center;
@ -635,19 +665,51 @@ export default {
// transition: @transition-theme !important;
// transition-property: color, font-size;
// }
// .lrc-active {
// color: @color-theme;
// font-size: 1.2em;
// }
}
.lyricSelectContent {
position: absolute;
left: 0;
top: 0;
// text-align: center;
height: 100%;
width: 100%;
font-size: 16px;
background-color: @color-theme_2-background_1;
z-index: 10;
color: @color-player-detail-lyric;
.lyricSelectline {
padding: 8px 0;
overflow-wrap: break-word;
transition: @transition-theme !important;
transition-property: color, font-size;
line-height: 1.3;
}
.lyricSelectlineTransition {
font-size: 14px;
}
.lrc-active {
color: @color-theme;
}
}
.lyric-space {
height: 70%;
}
.lrc-active {
color: @color-theme;
font-size: 1.2em;
}
.comment {
flex: 0 0 0;
opacity: 0;
position: absolute;
right: 0;
top: 0;
width: 50%;
height: 100%;
opacity: 1;
margin-left: 10px;
transform: scaleX(0);
}
.footer {
@ -853,6 +915,13 @@ each(@themes, {
// .lrc-active {
// color: ~'@{color-@{value}-theme}';
// }
.lyricSelectContent {
background-color: ~'@{color-@{value}-theme_2-background_1}';
color: ~'@{color-@{value}-player-detail-lyric}';
.lrc-active {
color: ~'@{color-@{value}-theme}';
}
}
.footerLeftControlBtns {
color: ~'@{color-@{value}-theme_2-font}';
}

View File

@ -0,0 +1,257 @@
<template lang="pug">
Modal(:show="visible" @close="$emit('update:visible', false)" bg-close)
div(:class="$style.header")
h2 {{listInfo.name}}
main.scroll(:class="$style.main")
ul(ref="dom_list" v-if="duplicateList.length" :class="$style.list")
li(v-for="(item, index) in duplicateList" :key="item.songmid" :class="$style.listItem")
div(:class="$style.num") {{item.index + 1}}
div(:class="$style.text")
h3(:class="$style.text") {{item.musicInfo.name}} - {{item.musicInfo.singer}}
h3(v-if="item.musicInfo.albumName" :class="[$style.text, $style.albumName]") {{item.musicInfo.albumName}}
div(:class="$style.label") {{item.musicInfo.source}}
div(:class="$style.label") {{item.musicInfo.interval}}
div(:class="$style.btns")
button(type="button" @click="handlePlay(index)" :class="$style.btn")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='50%' viewBox='0 0 287.386 287.386' space='preserve' v-once)
use(xlink:href='#icon-testPlay')
button(type="button" @click="handleRemove(index)" :class="$style.btn")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='50%' viewBox='0 0 212.982 212.982' space='preserve' v-once)
use(xlink:href='#icon-delete')
div(:class="$style.noItem" v-else)
p(v-text="$t('view.list.no_item')")
</template>
<script>
import { mapMutations } from 'vuex'
import Modal from './Modal'
import Btn from './Btn'
export default {
props: {
visible: {
type: Boolean,
default: false,
},
listInfo: {
type: Object,
required: true,
},
},
model: {
prop: 'visible',
event: 'visible',
},
components: {
Modal,
Btn,
},
watch: {
visible(n) {
if (n) this.handleFilterList()
},
},
data() {
return {
duplicateList: [],
}
},
mounted() {
if (this.listInfo.list) this.handleFilterList()
},
methods: {
...mapMutations('list', [
'listRemove',
]),
...mapMutations('player', {
setPlayList: 'setList',
}),
handleFilterList() {
const listMap = new Map()
const duplicateList = []
this.listInfo.list.forEach((musicInfo, index) => {
const musicInfoName = musicInfo.name.toLowerCase().trim()
if (listMap.has(musicInfoName)) {
const targetMusicInfo = listMap.get(musicInfoName)
duplicateList.push({
index: this.listInfo.list.indexOf(targetMusicInfo),
musicInfo: targetMusicInfo,
}, {
index,
musicInfo,
})
} else {
listMap.set(musicInfoName, musicInfo)
}
})
this.duplicateList = duplicateList
},
handleRemove(index) {
const { musicInfo: targetMusicInfo } = this.duplicateList.splice(index, 1)[0]
// let duplicates = []
// for (let index = 0; index < this.duplicateList.length; index++) {
// const { musicInfo } = this.duplicateList[index]
// if (musicInfo.name == targetMusicInfo.name) {
// duplicates.push(index)
// if (duplicates.length > 1) break
// }
// }
// console.log(duplicates)
// if (duplicates.length < 2) this.duplicateList.splice(duplicates[0], 1)
this.listRemove({ listId: this.listInfo.id, id: targetMusicInfo.songmid })
this.handleFilterList()
},
handlePlay(index) {
const { index: musicInfoIndex } = this.duplicateList[index]
this.setPlayList({ list: this.listInfo, index: musicInfoIndex })
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.header {
flex: none;
padding: 15px;
text-align: center;
}
.main {
min-height: 200px;
width: 460px;
}
.list {
// background-color: @color-search-form-background;
font-size: 13px;
transition-property: height;
position: relative;
.listItem {
position: relative;
padding: 8px 5px;
transition: background-color .2s ease;
line-height: 1.3;
// overflow: hidden;
display: flex;
flex-flow: row nowrap;
align-items: center;
&:hover {
background-color: @color-theme_2-hover;
}
// border-radius: 4px;
// &:last-child {
// border-bottom-left-radius: 4px;
// border-bottom-right-radius: 4px;
// }
}
}
.num {
flex: none;
font-size: 12px;
width: 30px;
text-align: center;
color: @color-theme_2-font-label;
}
.text {
flex: auto;
padding-left: 5px;
.mixin-ellipsis-1;
}
.albumName {
font-size: 12px;
opacity: 0.6;
.mixin-ellipsis-1;
}
.label {
flex: none;
font-size: 12px;
opacity: 0.5;
padding: 0 5px;
display: flex;
align-items: center;
// transform: rotate(45deg);
// background-color:
}
.btns {
flex: none;
font-size: 12px;
padding: 0 5px;
display: flex;
align-items: center;
}
.btn {
background-color: transparent;
border: none;
border-radius: @form-radius;
margin-right: 5px;
cursor: pointer;
padding: 4px 7px;
color: @color-btn;
outline: none;
transition: background-color 0.2s ease;
line-height: 0;
&:last-child {
margin-right: 0;
}
svg {
height: 16px;
}
&:hover {
background-color: @color-theme_2-hover;
}
&:active {
background-color: @color-theme_2-active;
}
}
.no-item {
position: relative;
height: 200px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
p {
font-size: 16px;
color: @color-theme_2-font-label;
}
}
each(@themes, {
:global(#container.@{value}) {
.listItem {
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
}
.num {
color: ~'@{color-@{value}-theme_2-font-label}';
}
.btn {
color: ~'@{color-@{value}-btn}';
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
.no-item {
p {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
}
})
</style>

View File

@ -3,7 +3,9 @@ transition(enter-active-class="animated fadeIn"
leave-active-class="animated fadeOut")
div(:class="$style.modal" v-show="show" @click="bgClose && close()")
transition(:enter-active-class="inClass"
:leave-active-class="outClass")
:leave-active-class="outClass"
@after-leave="$emit('after-leave', $event)"
)
div(:class="$style.content" v-show="show" @click.stop)
header(:class="$style.header")
button(type="button" @click="close" v-if="closeBtn")
@ -141,6 +143,7 @@ export default {
overflow: hidden;
max-height: 80%;
max-width: 76%;
min-width: 220px;
position: relative;
display: flex;
flex-flow: column nowrap;

View File

@ -185,7 +185,8 @@ export default {
display: flex;
flex-flow: column nowrap;
transition: @transition-theme;
transition-property: flex-basis opacity;
transition-property: transform,opacity;
transform-origin: 100%;
overflow: hidden;
}
.commentHeader {

View File

@ -96,6 +96,12 @@ export default {
...mapMutations(['setAgreePact']),
handleClick() {
this.setAgreePact()
setTimeout(() => {
this.$dialog({
message: Buffer.from('e69cace8bdafe4bbb6e5ae8ce585a8e5858de8b4b9e4b894e5bc80e6ba90efbc8ce5a682e69e9ce4bda0e698afe88ab1e992b1e8b4ade4b9b0e79a84efbc8ce8afb7e79bb4e68ea5e7bb99e5b7aee8af84efbc810a0a5468697320736f667477617265206973206672656520616e64206f70656e20736f757263652e', 'hex').toString(),
confirmButtonText: Buffer.from('e5a5bde79a8420284f4b29', 'hex').toString(),
})
}, 2000)
},
handleClose(isExit) {
if (isExit) return rendererSend(NAMES.mainWindow.close, true)

View File

@ -239,7 +239,7 @@ export default {
handleSearch() {
if (!this.text.length) return this.resultList = []
let list = []
let rxp = new RegExp(this.text.split('').join('.*') + '.*', 'i')
let rxp = new RegExp(this.text.split('').map(s => s.replace(/[.*+?^${}()|[\]\\]/, '\\$&')).join('.*') + '.*', 'i')
for (const item of this.list) {
if (rxp.test(`${item.name}${item.singer}${item.albumName ? item.albumName : ''}`)) list.push(item)
}
@ -374,6 +374,7 @@ export default {
.albumName {
font-size: 12px;
opacity: 0.6;
.mixin-ellipsis-1;
}
.source {
flex: none;

View File

@ -5,7 +5,7 @@
div.icon(:class="$style.icon")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 451.847 451.847' space='preserve')
use(xlink:href='#icon-down')
ul.list.scroll(:class="$style.list" :style="listStyles" ref="dom_list")
ul.selection-list.scroll(:class="$style.list" :style="listStyles" ref="dom_list")
li(v-for="item in list" :class="(itemKey ? item[itemKey] : item) == value ? $style.active : null" @click="handleClick(item)" :tips="itemName ? item[itemName] : item") {{itemName ? item[itemName] : item}}
</template>

View File

@ -13,7 +13,31 @@ div(:class="$style.songList")
th.nobreak(:style="{ width: rowWidth.r5 }") {{$t('material.song_list.time')}}
th.nobreak(:style="{ width: rowWidth.r6 }") {{$t('material.song_list.action')}}
div(:class="$style.content")
div.scroll(v-show="list.length" :class="$style.tbody" ref="dom_scrollContent")
div(v-if="list.length" :class="$style.content" ref="dom_listContent")
material-virtualized-list(:list="list" key-name="songmid" ref="list" :item-height="listItemHeight"
containerClass="scroll" contentClass="list" @contextmenu.native.capture="handleContextMenu")
template(#default="{ item, index }")
div.list-item(@click="handleDoubleClick($event, index)" @contextmenu="handleListItemRigthClick($event, index)"
:class="[{ selected: selectedIndex == index }, { active: selectdList.includes(item) }]")
div.list-item-cell.nobreak.center(:style="{ width: rowWidth.r1 }" style="padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}}
div.list-item-cell.auto(:style="{ width: rowWidth.r2 }" :tips="item.name + ((item._types.ape || item._types.flac || item._types.wav) ? ` - ${$t('material.song_list.lossless')}` : item._types['320k'] ? ` - ${$t('material.song_list.high_quality')}` : '')")
span.select {{item.name}}
span.badge.badge-theme-success(:class="[$style.labelQuality, $style.noSelect]" v-if="item._types.ape || item._types.flac || item._types.wav") {{$t('material.song_list.lossless')}}
span.badge.badge-theme-info(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item._types['320k']") {{$t('material.song_list.high_quality')}}
div.list-item-cell(:style="{ width: rowWidth.r3 }" :tips="item.singer")
span.select {{item.singer}}
div.list-item-cell(:style="{ width: rowWidth.r4 }" :tips="item.albumName")
span.select {{item.albumName}}
div.list-item-cell(:style="{ width: rowWidth.r5 }")
span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}}
div.list-item-cell(:style="{ width: rowWidth.r6 }" style="padding-left: 0; padding-right: 0;")
material-list-buttons(:index="index" :class="$style.btns"
:remove-btn="false" @btn-click="handleListBtnClick"
:download-btn="assertApiSupport(item.source)")
template(#footer)
div(:class="$style.pagination")
material-pagination(:count="total" :limit="limit" :page="page" @btn-click="handleTogglePage")
//- div.scroll(v-show="list.length" :class="$style.tbody" ref="dom_scrollContent")
table
tbody(@contextmenu.capture="handleContextMenu" ref="dom_tbody")
tr(v-for='(item, index) in list' :key='item.songmid' @contextmenu="handleListItemRigthClick($event, index)" @click="handleDoubleClick($event, index)")
@ -48,6 +72,7 @@ div(:class="$style.songList")
import { mapGetters } from 'vuex'
import { scrollTo, clipboardWriteText, assertApiSupport } from '../../utils'
import musicSdk from '../../utils/music'
import { windowSizeList } from '@common/config'
export default {
name: 'MaterialSongList',
model: {
@ -138,6 +163,9 @@ export default {
},
]
},
listItemHeight() {
return parseInt(windowSizeList.find(item => item.id == this.setting.windowSizeId).fontSize) / 16 * 37
},
},
watch: {
// selectdList(n) {
@ -175,7 +203,9 @@ export default {
isModDown: false,
},
lastSelectIndex: 0,
selectedIndex: -1,
listMenu: {
rightClickItemIndex: -1,
isShowItemMenu: false,
itemMenuControl: {
play: true,
@ -233,7 +263,7 @@ export default {
this.handleSelectAllData()
},
handleDoubleClick(event, index) {
if (event.target.classList.contains('select')) return
if (this.listMenu.rightClickItemIndex > -1) return
this.handleSelectData(event, index)
@ -264,14 +294,8 @@ export default {
}
this.selectdList = this.list.slice(lastSelectIndex, clickIndex + 1)
if (isNeedReverse) this.selectdList.reverse()
let nodes = this.$refs.dom_tbody.childNodes
do {
nodes[lastSelectIndex].classList.add('active')
lastSelectIndex++
} while (lastSelectIndex <= clickIndex)
}
} else {
event.currentTarget.classList.add('active')
this.selectdList.push(this.list[clickIndex])
this.lastSelectIndex = clickIndex
}
@ -281,10 +305,8 @@ export default {
let index = this.selectdList.indexOf(item)
if (index < 0) {
this.selectdList.push(item)
event.currentTarget.classList.add('active')
} else {
this.selectdList.splice(index, 1)
event.currentTarget.classList.remove('active')
}
} else if (this.selectdList.length) {
this.removeAllSelect()
@ -293,12 +315,6 @@ export default {
},
removeAllSelect() {
this.selectdList = []
let dom_tbody = this.$refs.dom_tbody
if (!dom_tbody) return
let nodes = dom_tbody.querySelectorAll('.active')
for (const node of nodes) {
if (node.parentNode == dom_tbody) node.classList.remove('active')
}
},
handleListBtnClick(info) {
this.emitEvent('listBtnClick', info)
@ -306,10 +322,6 @@ export default {
handleSelectAllData() {
this.removeAllSelect()
this.selectdList = [...this.list]
let nodes = this.$refs.dom_tbody.childNodes
for (const node of nodes) {
node.classList.add('active')
}
this.$emit('input', [...this.selectdList])
},
handleTogglePage(page) {
@ -327,12 +339,12 @@ export default {
handleContextMenu(event) {
if (!event.target.classList.contains('select')) return
event.stopImmediatePropagation()
let classList = this.$refs.dom_scrollContent.classList
let classList = this.$refs.dom_listContent.classList
classList.add(this.$style.copying)
window.requestAnimationFrame(() => {
let str = window.getSelection().toString()
classList.remove(this.$style.copying)
str = str.trim()
str = str.split(/\n\n/).map(s => s.replace(/\n/g, ' ')).join('\n').trim()
if (!str.length) return
clipboardWriteText(str)
})
@ -344,23 +356,27 @@ export default {
this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.list[index].source].getMusicDetailPageUrl
// this.listMenu.itemMenuControl.play =
// this.listMenu.itemMenuControl.playLater =
this.listMenu.itemMenuControl.download =
this.assertApiSupport(this.list[index].source)
let dom_selected = this.$refs.dom_tbody.querySelector('tr.selected')
if (dom_selected) dom_selected.classList.remove('selected')
this.$refs.dom_tbody.querySelectorAll('tr')[index].classList.add('selected')
let dom_td = event.target.closest('td')
this.listMenu.itemMenuControl.download = this.assertApiSupport(this.list[index].source)
let dom_container = event.target.closest('.' + this.$style.songList)
const getOffsetValue = (target, x = 0, y = 0) => {
if (target === dom_container) return { x, y }
if (!target) return { x: 0, y: 0 }
x += target.offsetLeft
y += target.offsetTop
return getOffsetValue(target.offsetParent, x, y)
}
this.listMenu.rightClickItemIndex = index
this.listMenu.menuLocation.x = dom_td.offsetLeft + event.offsetX
this.listMenu.menuLocation.y = dom_td.offsetParent.offsetTop + dom_td.offsetTop + event.offsetY - this.$refs.dom_scrollContent.scrollTop
this.selectedIndex = index
let { x, y } = getOffsetValue(event.target)
this.listMenu.menuLocation.x = x + event.offsetX
this.listMenu.menuLocation.y = y + event.offsetY - this.$refs.list.getScrollTop()
this.hideListsMenu()
this.$nextTick(() => {
this.listMenu.isShowItemMenu = true
})
},
hideListMenu() {
let dom_selected = this.$refs.dom_tbody && this.$refs.dom_tbody.querySelector('tr.selected')
if (dom_selected) dom_selected.classList.remove('selected')
this.selectedIndex = -1
this.listMenu.isShowItemMenu = false
this.listMenu.rightClickItemIndex = -1
},
@ -406,24 +422,7 @@ export default {
flex: auto;
min-height: 0;
position: relative;
}
.tbody {
height: 100%;
overflow-y: auto;
td {
font-size: 12px;
:global(.badge) {
margin-left: 3px;
}
&:first-child {
// padding-left: 10px;
font-size: 11px;
color: @color-theme_2-font-label;
}
}
:global(.badge) {
opacity: .85;
}
&.copying {
.no-select {
@ -431,6 +430,24 @@ export default {
}
}
}
:global(.list) {
height: 100%;
overflow-y: auto;
:global(.list-item-cell) {
font-size: 12px !important;
:global(.badge) {
margin-left: 3px;
}
&:first-child {
// padding-left: 10px;
font-size: 11px !important;
color: @color-theme_2-font-label !important;
}
}
:global(.badge) {
opacity: .85;
}
}
.pagination {
text-align: center;
padding: 15px 0;
@ -456,10 +473,10 @@ export default {
each(@themes, {
:global(#container.@{value}) {
.tbody {
td {
:global(.list) {
:global(.list-item-cell) {
&:first-child {
color: ~'@{color-@{value}-theme_2-font-label}';
color: ~'@{color-@{value}-theme_2-font-label}' !important;
}
}
}

View File

@ -0,0 +1,240 @@
<template>
<component :is="containerEl" :class="containerClass" ref="dom_scrollContainer" style="height: 100%; overflow: auto; position: relative; display: block;">
<component :is="contentEl" :class="contentClass" :style="contentStyle">
<div v-for="item in views" :key="item.key" :style="item.style">
<slot name="default" v-bind="{ item: item.item, index: item.index }" />
</div>
</component>
<slot name="footer" />
</component>
</template>
<script>
const easeInOutQuad = (t, b, c, d) => {
t /= d / 2
if (t < 1) return (c / 2) * t * t + b
t--
return (-c / 2) * (t * (t - 2) - 1) + b
}
const handleScroll = (element, to, duration = 300, callback = () => {}, onCancel = () => {}) => {
if (!element) return callback()
const start = element.scrollTop || element.scrollY || 0
let cancel = false
if (to > start) {
let maxScrollTop = element.scrollHeight - element.clientHeight
if (to > maxScrollTop) to = maxScrollTop
} else if (to < start) {
if (to < 0) to = 0
} else return callback()
const change = to - start
const increment = 10
if (!change) return callback()
let currentTime = 0
let val
let cancelCallback
const animateScroll = () => {
currentTime += increment
val = parseInt(easeInOutQuad(currentTime, start, change, duration))
if (element.scrollTo) {
element.scrollTo(0, val)
} else {
element.scrollTop = val
}
if (currentTime < duration) {
if (cancel) {
cancelCallback()
onCancel()
return
}
setTimeout(animateScroll, increment)
} else {
callback()
}
}
animateScroll()
return (callback) => {
cancelCallback = callback
cancel = true
}
}
export default {
name: 'VirtualizedList',
props: {
containerEl: {
type: String,
default: 'div',
},
containerClass: {
type: String,
default: 'virtualized-list',
},
contentEl: {
type: String,
default: 'div',
},
contentClass: {
type: String,
default: 'virtualized-list-content',
},
outsideNum: {
type: Number,
default: 1,
},
itemHeight: {
type: Number,
required: true,
},
keyName: {
type: String,
require: true,
},
list: {
type: Array,
require: true,
},
},
data() {
return {
views: [],
startIndex: -1,
endIndex: -1,
scrollTop: 0,
cachedList: [],
cancelScroll: null,
isScrolling: false,
scrollToValue: 0,
}
},
computed: {
contentStyle() {
return {
display: 'block',
height: this.list.length * this.itemHeight + 'px',
}
},
},
watch: {
itemHeight() {
this.updateView()
},
list() {
this.cachedList = Array(this.list.length)
this.startIndex = -1
this.endIndex = -1
this.updateView()
},
},
mounted() {
this.$refs.dom_scrollContainer.addEventListener('scroll', this.onScroll, false)
this.cachedList = Array(this.list.length)
this.startIndex = -1
this.endIndex = -1
this.updateView()
},
beforeDestroy() {
this.$refs.dom_scrollContainer.removeEventListener('scroll', this.onScroll)
if (this.cancelScroll) this.cancelScroll()
},
methods: {
onScroll(event) {
const currentScrollTop = this.$refs.dom_scrollContainer.scrollTop
if (Math.abs(currentScrollTop - this.scrollTop) > this.itemHeight * this.outsideNum * 0.6) {
this.updateView(currentScrollTop)
}
this.$emit('scroll', event)
},
createList(startIndex, endIndex) {
const cache = this.cachedList.slice(startIndex, endIndex)
const list = this.list.slice(startIndex, endIndex).map((item, i) => {
if (cache[i]) return cache[i]
const top = (startIndex + i) * this.itemHeight
const index = startIndex + i
return this.cachedList[index] = {
item,
top,
style: { position: 'absolute', left: 0, right: 0, top: top + 'px', height: this.itemHeight + 'px' },
index,
key: item[this.keyName],
}
})
return list
},
updateView(currentScrollTop = this.$refs.dom_scrollContainer.scrollTop) {
// const currentScrollTop = this.$refs.dom_scrollContainer.scrollTop
const currentStartIndex = Math.floor(currentScrollTop / this.itemHeight)
const scrollContainerHeight = this.$refs.dom_scrollContainer.clientHeight
const currentEndIndex = currentStartIndex + Math.ceil(scrollContainerHeight / this.itemHeight)
const continuous = currentStartIndex <= this.endIndex && currentEndIndex >= this.startIndex
const currentStartRenderIndex = Math.max(Math.floor(currentScrollTop / this.itemHeight) - this.outsideNum, 0)
const currentEndRenderIndex = currentStartIndex + Math.ceil(scrollContainerHeight / this.itemHeight) + this.outsideNum
// console.log(continuous)
// debugger
if (continuous) {
// if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * this.outsideNum * 0.6) return
// console.log('update')
if (currentScrollTop > this.scrollTop) { // scroll down
// console.log('scroll down')
const list = this.createList(currentStartRenderIndex, currentEndRenderIndex)
this.views.push(...list.slice(list.indexOf(this.views[this.views.length - 1]) + 1))
// if (this.views.length > 100) {
this.$nextTick(() => {
this.views.splice(0, this.views.indexOf(list[0]))
})
// }
} else if (currentScrollTop < this.scrollTop) { // scroll up
// console.log('scroll up')
this.views = this.createList(currentStartRenderIndex, currentEndRenderIndex)
} else return
} else {
this.views = this.createList(currentStartRenderIndex, currentEndRenderIndex)
}
this.startIndex = currentStartIndex
this.endIndex = currentEndIndex
this.scrollTop = currentScrollTop
},
scrollTo(scrollTop, animate = false) {
return new Promise(resolve => {
if (this.cancelScroll) {
this.cancelScroll(resolve)
} else {
resolve()
}
}).then(() => {
return new Promise((resolve, reject) => {
if (animate) {
this.isScrolling = true
this.scrollToValue = scrollTop
this.cancelScroll = handleScroll(this.$refs.dom_scrollContainer, scrollTop, 300, () => {
this.cancelScroll = null
this.isScrolling = false
resolve()
}, () => {
this.cancelScroll = null
this.isScrolling = false
reject('canceled')
})
} else {
this.$refs.dom_scrollContainer.scrollTop = scrollTop
}
})
})
},
scrollToIndex(index, offset = 0, animate = false) {
return this.scrollTo(Math.max(index * this.itemHeight + offset, 0), animate)
},
getScrollTop() {
return this.isScrolling ? this.scrollToValue : this.$refs.dom_scrollContainer.scrollTop
},
},
}
</script>

View File

@ -1,5 +1,7 @@
{
"date_format_second": "{num} seconds ago",
"cancel_button_text": "Cancel",
"confirm_button_text": "OK",
"date_format_hour": "{num} hours ago",
"date_format_minute": "{num} minutes ago",
"date_format_hour": "{num} hours ago"
"date_format_second": "{num} seconds ago"
}

View File

@ -1,43 +1,43 @@
{
"copy_title": " (Click to copy)",
"volume": "Volume: ",
"pause": "Pause",
"play": "Play",
"prev": "Prev",
"next": "Next",
"playing": "Now playing...",
"stop": "Paused",
"end": "Stopped",
"refresh_url": "Music URL expired, refreshing...",
"error": "Error loading music. Switch to next song after 5 seconds",
"loading": "Music loading...",
"buffering": "Buffering...",
"geting_url": "Getting music link...",
"lyric_error": "Failed to get lyrics",
"hide_detail": "Hide detail page (Right-click in the view to quickly hide the details page)",
"name": "Name: ",
"singer": "Artist: ",
"album": "Album: ",
"add_music_to": "Add the current song to...",
"desktop_lyric_on": "Open Desktop Lyrics",
"desktop_lyric_off": "Close Desktop Lyrics",
"desktop_lyric_lock": "Right click to lock lyrics",
"desktop_lyric_unlock": "Right click to unlock lyrics",
"play_toggle_mode_list_loop": "List Loop",
"play_toggle_mode_random": "List Random",
"play_toggle_mode_list": "Play in order",
"play_toggle_mode_single_loop": "Single Loop",
"play_toggle_mode_off": "Disable",
"pic_tip": "Right click to locate the currently playing song in \"My List\"",
"comment_show": "Song comments",
"comment_hot_loading": "Hot comments are loading",
"comment_new_loading": "Latest comments are loading",
"album": "Album: ",
"buffering": "Buffering...",
"comment_hot_load_error": "Hot comments failed to load, click to try to reload",
"comment_new_load_error": "The latest comment failed to load, click to try to reload",
"comment_refresh": "Refresh comments",
"comment_no_content": "No comments yet",
"comment_hot_loading": "Hot comments are loading",
"comment_hot_title": "Hot Comment",
"comment_new_load_error": "The latest comment failed to load, click to try to reload",
"comment_new_loading": "Latest comments are loading",
"comment_new_title": "Latest comment",
"comment_title": "{name} comment"
"comment_no_content": "No comments yet",
"comment_refresh": "Refresh comments",
"comment_show": "Song comments",
"comment_title": "{name} comment",
"copy_title": " (Click to copy)",
"desktop_lyric_lock": "Right click to lock lyrics",
"desktop_lyric_off": "Close Desktop Lyrics",
"desktop_lyric_on": "Open Desktop Lyrics",
"desktop_lyric_unlock": "Right click to unlock lyrics",
"end": "Stopped",
"error": "Error loading music. Switch to next song after 5 seconds",
"geting_url": "Getting music link...",
"hide_detail": "Hide detail page (Right-click in the view to quickly hide the details page)",
"loading": "Music loading...",
"lyric_error": "Failed to get lyrics",
"lyric_select": "Lyric text selection",
"name": "Name: ",
"next": "Next",
"pause": "Pause",
"pic_tip": "Right click to locate the currently playing song in \"My List\"",
"play": "Play",
"play_toggle_mode_list": "Play in order",
"play_toggle_mode_list_loop": "List Loop",
"play_toggle_mode_off": "Disable",
"play_toggle_mode_random": "List Random",
"play_toggle_mode_single_loop": "Single Loop",
"playing": "Now playing...",
"prev": "Prev",
"refresh_url": "Music URL expired, refreshing...",
"singer": "Artist: ",
"stop": "Paused",
"volume": "Volume: "
}

View File

@ -1,28 +1,38 @@
{
"lists_new_list_btn": "Create list",
"lists_new_list_input": "New list...",
"lists_rename": "Rename",
"lists_moveup": "Move Up",
"lists_movedown": "Move Down",
"lists_sync": "Update",
"lists_remove": "Remove",
"action": "Manage",
"album": "Album",
"default_list": "Recently Played",
"list_add_to": "Add to ...",
"list_copy_name": "Copy name",
"list_download": "Download",
"list_move_to": "Move to ...",
"list_play": "Play",
"list_play_later": "Play later",
"list_copy_name": "Copy name",
"list_add_to": "Add to ...",
"list_move_to": "Move to ...",
"list_sort": "Adjust position",
"list_download": "Download",
"list_search": "Search",
"list_remove": "Remove",
"list_search": "Search",
"list_sort": "Adjust position",
"list_source_detail": "Song Page",
"default_list": "Recently Played",
"lists_duplicate": "Duplicate song",
"lists_export": "Export",
"lists_export_part_desc": "Choose where to save the list file",
"lists_import": "Import",
"lists_import_part_button_cancel": "No",
"lists_import_part_button_confirm": "Overwrite",
"lists_import_part_confirm": "The imported list ({importName}) has the same ID as the local list ({localName}). Do you overwrite the local list?",
"lists_import_part_desc": "Select list file",
"lists_movedown": "Move Down",
"lists_moveup": "Move Up",
"lists_new_list_btn": "Create list",
"lists_new_list_input": "New list...",
"lists_remove": "Remove",
"lists_remove_tip": "Do you really want to remove {name}?",
"lists_remove_tip_button": "Yes, that's right",
"lists_rename": "Rename",
"lists_sync": "Update",
"loding_list": "Loading...",
"love_list": "Favorites",
"name": "Name",
"no_item": "Nothing's here...",
"singer": "Artist",
"album": "Album",
"action": "Manage",
"time": "Length",
"loding_list": "Loading...",
"no_item": "Nothing's here..."
"time": "Length"
}

View File

@ -120,6 +120,7 @@
"other_resource_cache_clear_btn": "Clear resource cache",
"other_resource_cache_label": "The software has used cache size: ",
"other_tray_theme": "Tray Icon Style",
"other_tray_theme_black": "Black Color",
"other_tray_theme_native": "Solid Color",
"other_tray_theme_origin": "Primary Color",
"play": "Play",

View File

@ -1,5 +1,7 @@
{
"date_format_second": "{num}秒前",
"cancel_button_text": "我不",
"confirm_button_text": "好的",
"date_format_hour": "{num}小时前",
"date_format_minute": "{num}分钟前",
"date_format_hour": "{num}小时前"
"date_format_second": "{num}秒前"
}

View File

@ -1,43 +1,43 @@
{
"copy_title": "(点击复制)",
"volume": "当前音量:",
"pause": "暂停",
"play": "播放",
"prev": "上一首",
"next": "下一首",
"playing": "播放中...",
"stop": "暂停播放",
"end": "播放完毕",
"refresh_url": "URL过期正在刷新URL...",
"error": "音频加载出错5 秒后切换下一首",
"loading": "音乐加载中...",
"buffering": "缓冲中...",
"geting_url": "歌曲链接获取中...",
"lyric_error": "歌词获取失败",
"hide_detail": "隐藏详情页(界面内右键双击可快速隐藏详情页)",
"name": "歌曲名:",
"singer": "艺术家:",
"album": "专辑名:",
"add_music_to": "添加当前歌曲到...",
"desktop_lyric_on": "开启桌面歌词",
"desktop_lyric_off": "关闭桌面歌词",
"desktop_lyric_lock": "右击锁定歌词",
"desktop_lyric_unlock": "右击解锁歌词",
"play_toggle_mode_list_loop": "列表循环",
"play_toggle_mode_random": "列表随机",
"play_toggle_mode_list": "顺序播放",
"play_toggle_mode_single_loop": "单曲循环",
"play_toggle_mode_off": "禁用",
"pic_tip": "右击在“我的列表”定位当前播放的歌曲",
"comment_show": "歌曲评论",
"comment_hot_loading": "热门评论加载中",
"comment_new_loading": "最新评论加载中",
"album": "专辑名:",
"buffering": "缓冲中...",
"comment_hot_load_error": "热门评论加载失败,点击尝试重新加载",
"comment_new_load_error": "最新评论加载失败,点击尝试重新加载",
"comment_refresh": "刷新评论",
"comment_no_content": "暂无评论",
"comment_hot_loading": "热门评论加载中",
"comment_hot_title": "热门评论",
"comment_new_load_error": "最新评论加载失败,点击尝试重新加载",
"comment_new_loading": "最新评论加载中",
"comment_new_title": "最新评论",
"comment_title": "{name} 的评论"
"comment_no_content": "暂无评论",
"comment_refresh": "刷新评论",
"comment_show": "歌曲评论",
"comment_title": "{name} 的评论",
"copy_title": "(点击复制)",
"desktop_lyric_lock": "右击锁定歌词",
"desktop_lyric_off": "关闭桌面歌词",
"desktop_lyric_on": "开启桌面歌词",
"desktop_lyric_unlock": "右击解锁歌词",
"end": "播放完毕",
"error": "音频加载出错5 秒后切换下一首",
"geting_url": "歌曲链接获取中...",
"hide_detail": "隐藏详情页(界面内右键双击可快速隐藏详情页)",
"loading": "音乐加载中...",
"lyric_error": "歌词获取失败",
"lyric_select": "歌词文本选择",
"name": "歌曲名:",
"next": "下一首",
"pause": "暂停",
"pic_tip": "右击在“我的列表”定位当前播放的歌曲",
"play": "播放",
"play_toggle_mode_list": "顺序播放",
"play_toggle_mode_list_loop": "列表循环",
"play_toggle_mode_off": "禁用",
"play_toggle_mode_random": "列表随机",
"play_toggle_mode_single_loop": "单曲循环",
"playing": "播放中...",
"prev": "上一首",
"refresh_url": "URL过期正在刷新URL...",
"singer": "艺术家:",
"stop": "暂停播放",
"volume": "当前音量:"
}

View File

@ -1,28 +1,38 @@
{
"lists_new_list_btn": "新建列表",
"lists_new_list_input": "新列表...",
"lists_rename": "重命名",
"lists_moveup": "上移",
"lists_movedown": "下移",
"lists_sync": "更新",
"lists_remove": "删除",
"action": "操作",
"album": "专辑",
"default_list": "试听列表",
"list_add_to": "添加到...",
"list_copy_name": "复制歌曲名",
"list_download": "下载",
"list_move_to": "移动到...",
"list_play": "播放",
"list_play_later": "稍后播放",
"list_copy_name": "复制歌曲名",
"list_source_detail": "歌曲详情页",
"list_add_to": "添加到...",
"list_move_to": "移动到...",
"list_sort": "调整位置",
"list_download": "下载",
"list_search": "搜索",
"list_remove": "删除",
"default_list": "试听列表",
"list_search": "搜索",
"list_sort": "调整位置",
"list_source_detail": "歌曲详情页",
"lists_duplicate": "重复歌曲",
"lists_export": "导出",
"lists_export_part_desc": "选择列表文件保存位置",
"lists_import": "导入",
"lists_import_part_button_cancel": "不要啊",
"lists_import_part_button_confirm": "覆盖掉",
"lists_import_part_confirm": "导入的列表({importName})与本地列表({localName}的ID相同是否覆盖本地列表",
"lists_import_part_desc": "选择列表文件",
"lists_movedown": "下移",
"lists_moveup": "上移",
"lists_new_list_btn": "新建列表",
"lists_new_list_input": "新列表...",
"lists_remove": "删除",
"lists_remove_tip": "你真的想要移除 {name} 吗?",
"lists_remove_tip_button": "是的 没错",
"lists_rename": "重命名",
"lists_sync": "更新",
"loding_list": "加载中...",
"love_list": "收藏",
"name": "歌曲名",
"no_item": "列表竟然是空的...",
"singer": "歌手",
"album": "专辑",
"action": "操作",
"time": "时长",
"loding_list": "加载中...",
"no_item": "列表竟然是空的..."
"time": "时长"
}

View File

@ -120,6 +120,7 @@
"other_resource_cache_clear_btn": "清理资源缓存",
"other_resource_cache_label": "软件已使用缓存大小:",
"other_tray_theme": "托盘图标样式",
"other_tray_theme_black": "黑色",
"other_tray_theme_native": "纯色",
"other_tray_theme_origin": "原色",
"play": "播放设置",

View File

@ -1,5 +1,7 @@
{
"date_format_second": "{num}秒前",
"cancel_button_text": "取消",
"confirm_button_text": "好的",
"date_format_hour": "{num}小時前",
"date_format_minute": "{num}分鐘前",
"date_format_hour": "{num}小時前"
"date_format_second": "{num}秒前"
}

View File

@ -1,43 +1,43 @@
{
"copy_title": "(點擊複製)",
"volume": "當前音量:",
"pause": "暫停",
"play": "播放",
"prev": "上一首",
"next": "下一首",
"playing": "播放中...",
"stop": "暫停播放",
"end": "播放完畢",
"refresh_url": "URL過期正在刷新URL...",
"error": "音頻加載出錯5 秒後切換下一首",
"loading": "音樂加載中...",
"buffering": "緩衝中...",
"geting_url": "歌曲鏈接獲取中...",
"lyric_error": "歌詞獲取失敗",
"hide_detail": "隱藏詳情頁(界面內右鍵雙擊可快速隱藏詳情頁)",
"name": "歌曲名:",
"singer": "藝術家:",
"album": "專輯名:",
"add_music_to": "添加當前歌曲到...",
"desktop_lyric_on": "開啟桌面歌詞",
"desktop_lyric_off": "關閉桌面歌詞",
"desktop_lyric_lock": "右擊鎖定歌詞",
"desktop_lyric_unlock": "右擊解鎖歌詞",
"play_toggle_mode_list_loop": "列表循環",
"play_toggle_mode_random": "列表隨機",
"play_toggle_mode_list": "順序播放",
"play_toggle_mode_single_loop": "單曲循環",
"play_toggle_mode_off": "禁用",
"pic_tip": "右擊在“我的列表”定位當前播放的歌曲",
"comment_show": "歌曲評論",
"comment_hot_loading": "熱門評論加載中",
"comment_new_loading": "最新評論加載中",
"album": "專輯名:",
"buffering": "緩衝中...",
"comment_hot_load_error": "熱門評論加載失敗,點擊嘗試重新加載",
"comment_new_load_error": "最新評論加載失敗,點擊嘗試重新加載",
"comment_refresh": "刷新評論",
"comment_no_content": "暫無評論",
"comment_hot_loading": "熱門評論加載中",
"comment_hot_title": "熱門評論",
"comment_new_load_error": "最新評論加載失敗,點擊嘗試重新加載",
"comment_new_loading": "最新評論加載中",
"comment_new_title": "最新評論",
"comment_title": "{name} 的評論"
"comment_no_content": "暫無評論",
"comment_refresh": "刷新評論",
"comment_show": "歌曲評論",
"comment_title": "{name} 的評論",
"copy_title": "(點擊複製)",
"desktop_lyric_lock": "右擊鎖定歌詞",
"desktop_lyric_off": "關閉桌面歌詞",
"desktop_lyric_on": "開啟桌面歌詞",
"desktop_lyric_unlock": "右擊解鎖歌詞",
"end": "播放完畢",
"error": "音頻加載出錯5 秒後切換下一首",
"geting_url": "歌曲鏈接獲取中...",
"hide_detail": "隱藏詳情頁(界面內右鍵雙擊可快速隱藏詳情頁)",
"loading": "音樂加載中...",
"lyric_error": "歌詞獲取失敗",
"lyric_select": "歌詞文本選擇",
"name": "歌曲名:",
"next": "下一首",
"pause": "暫停",
"pic_tip": "右擊在“我的列表”定位當前播放的歌曲",
"play": "播放",
"play_toggle_mode_list": "順序播放",
"play_toggle_mode_list_loop": "列表循環",
"play_toggle_mode_off": "禁用",
"play_toggle_mode_random": "列表隨機",
"play_toggle_mode_single_loop": "單曲循環",
"playing": "播放中...",
"prev": "上一首",
"refresh_url": "URL過期正在刷新URL...",
"singer": "藝術家:",
"stop": "暫停播放",
"volume": "當前音量:"
}

View File

@ -1,28 +1,38 @@
{
"lists_new_list_btn": "新建列表",
"lists_new_list_input": "新列表...",
"lists_rename": "重命名",
"lists_moveup": "上移",
"lists_movedown": "下移",
"lists_sync": "更新",
"lists_remove": "刪除",
"action": "操作",
"album": "專輯",
"default_list": "試聽列表",
"list_add_to": "添加到...",
"list_copy_name": "複製歌曲名",
"list_download": "下載",
"list_move_to": "移動到...",
"list_play": "播放",
"list_play_later": "稍後播放",
"list_copy_name": "複製歌曲名",
"list_add_to": "添加到...",
"list_move_to": "移動到...",
"list_sort": "調整位置",
"list_download": "下載",
"list_search": "搜索",
"list_remove": "刪除",
"list_search": "搜索",
"list_sort": "調整位置",
"list_source_detail": "歌曲詳情頁",
"default_list": "試聽列表",
"lists_duplicate": "重複歌曲",
"lists_export": "導出",
"lists_export_part_desc": "選擇列表文件保存位置",
"lists_import": "導入",
"lists_import_part_button_cancel": "不要啊",
"lists_import_part_button_confirm": "覆蓋掉",
"lists_import_part_confirm": "導入的列表({importName})與本地列表({localName}的ID相同是否覆蓋本地列表",
"lists_import_part_desc": "選擇列表文件",
"lists_movedown": "下移",
"lists_moveup": "上移",
"lists_new_list_btn": "新建列表",
"lists_new_list_input": "新列表...",
"lists_remove": "刪除",
"lists_remove_tip": "你真的想要移除 {name} 嗎?",
"lists_remove_tip_button": "是的 沒錯",
"lists_rename": "重命名",
"lists_sync": "更新",
"loding_list": "加載中...",
"love_list": "收藏列表",
"name": "歌曲名",
"no_item": "列表竟然是空的...",
"singer": "歌手",
"album": "專輯",
"action": "操作",
"time": "時長",
"loding_list": "加載中...",
"no_item": "列表竟然是空的..."
"time": "時長"
}

View File

@ -120,6 +120,7 @@
"other_resource_cache_clear_btn": "清理資源緩存",
"other_resource_cache_label": "軟件已使用緩存大小:",
"other_tray_theme": "托盤圖標樣式",
"other_tray_theme_black": "黑色",
"other_tray_theme_native": "純色",
"other_tray_theme_origin": "原色",
"play": "播放設置",

View File

@ -1,5 +1,5 @@
import Vue from 'vue'
import { sync } from 'vuex-router-sync'
// import { sync } from 'vuex-router-sync'
import './event'
@ -20,7 +20,7 @@ import { getSetting } from './utils'
import languageList from '@renderer/lang/languages.json'
import { rendererSend, NAMES } from '../common/ipc'
sync(store, router)
// sync(store, router)
Vue.config.productionTip = false
Vue.config.devtools = process.env.NODE_ENV === 'development'

View File

@ -0,0 +1,75 @@
<template>
<Modal :show="visible" @close="handleCancel" @after-leave="afterLeave" :closeBtn="false">
<main :class="$style.main">{{message}}</main>
<footer :class="$style.footer">
<Btn :class="$style.btn" v-if="showCancel" @click="handleCancel">{{cancelBtnText}}</Btn>
<Btn :class="$style.btn" @click="handleComfirm">{{confirmBtnText}}</Btn>
</footer>
</Modal>
</template>
<script>
import Modal from '@renderer/components/material/Modal'
import Btn from '@renderer/components/material/Btn'
export default {
components: {
Modal,
Btn,
},
data() {
return {
visible: false,
message: '',
showCancel: false,
cancelButtonText: '',
confirmButtonText: '',
}
},
computed: {
cancelBtnText() {
return this.cancelButtonText || this.$t('base.cancel_button_text')
},
confirmBtnText() {
return this.confirmButtonText || this.$t('base.confirm_button_text')
},
},
beforeDestroy() {
const el = this.$el
el.parentNode.removeChild(el)
},
methods: {
afterLeave(el, done) {
this.$destroy()
},
handleCancel() {
},
handleComfirm() {
},
},
}
</script>
<style lang="less" module>
.main {
flex: auto;
min-height: 40px;
padding: 15px;
font-size: 14px;
max-width: 320px;
min-width: 220px;
line-height: 1.5;
white-space: pre-line;
}
.footer {
flex: none;
padding: 0 15px 15px;
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
gap: 15px;
}
</style>

View File

@ -0,0 +1,54 @@
import Dialog from './Dialog'
import i18n from '../i18n'
import store from '@renderer/store'
import Vue from 'vue'
const defaultOptions = {
message: '',
showCancel: false,
cancelButtonText: '',
confirmButtonText: '',
}
const dialog = {
install(Vue, options) {
const DialogConstructor = Vue.extend(Dialog)
const dialog = function Dialog(options) {
const { message, showCancel, cancelButtonText, confirmButtonText } =
Object.assign({}, defaultOptions, typeof options == 'string' ? { message: options } : options || {})
return new Promise((resolve, reject) => {
let instance = new DialogConstructor({ i18n, store }).$mount(document.createElement('div'))
// 属性设置
instance.visible = true
instance.message = message
instance.showCancel = showCancel
instance.cancelButtonText = cancelButtonText
instance.confirmButtonText = confirmButtonText
// 挂载
document.getElementById('container').appendChild(instance.$el)
instance.handleCancel = () => {
instance.visible = false
resolve(false)
}
instance.handleComfirm = () => {
instance.visible = false
resolve(true)
}
})
}
dialog.confirm = options => dialog(
typeof options == 'string'
? { message: options, showCancel: true }
: { ...options, showCancel: true },
)
Vue.prototype.$dialog = dialog
},
}
Vue.use(dialog)

View File

@ -3,6 +3,8 @@ import { debounce } from '../../utils'
let instance
let prevTips
let prevX = 0
let prevY = 0
const getTips = el =>
el
@ -53,6 +55,9 @@ const updateTips = event => {
}
document.body.addEventListener('mousemove', event => {
if (event.x == prevX && event.y == prevY) return
prevX = event.x
prevY = event.y
hideTips()
showTips(event)
})

View File

@ -1,2 +1,3 @@
// import './axios'
import './Dialog'
import './Tips'

View File

@ -11,9 +11,13 @@ import {
getMusicUrl as getMusicUrlFormStorage,
setMusicUrl,
assertApiSupport,
filterFileName,
} from '../../utils'
import { NAMES, rendererInvoke } from '@common/ipc'
window.downloadList = []
window.downloadListFull = []
window.downloadListFullMap = new Map()
// state
const state = {
list: window.downloadList,
@ -30,9 +34,7 @@ const state = {
const dls = {}
const tryNum = {}
let isRuningActionTask = false
const filterFileName = /[\\/:*?#"<>|]/g
// getters
const getters = {
@ -71,7 +73,7 @@ const getExt = type => {
}
}
const checkList = (list, musicInfo, type, ext) => list.some(s => s.musicInfo.songmid === musicInfo.songmid && (s.type === type || s.ext === ext))
const checkList = (list, musicInfo, type, ext) => list.some(s => s.songmid === musicInfo.songmid && (s.type === type || s.ext === ext))
const getStartTask = (list, downloadStatus, maxDownloadNum) => {
let downloadCount = 0
@ -80,82 +82,6 @@ const getStartTask = (list, downloadStatus, maxDownloadNum) => {
return downloadCount < maxDownloadNum ? waitList.shift() || null : false
}
const awaitRequestAnimationFrame = () => new Promise(resolve => window.requestAnimationFrame(() => resolve()))
const addTasks = async(store, list, type) => {
if (list.length == 0) return
let num = 3
while (num-- > 0) {
let item = list.shift()
if (!item) return
store.dispatch('createDownload', {
musicInfo: item,
type: getMusicType(item, type),
})
}
await awaitRequestAnimationFrame()
await addTasks(store, list, type)
}
const removeTasks = async(store, list) => {
let num = 20
while (num-- > 0) {
let item = list.pop()
if (!item) return
let index = store.state.list.indexOf(item)
if (index < 0) continue
store.dispatch('removeTask', item)
}
await awaitRequestAnimationFrame()
await removeTasks(store, list)
}
const startTasks = async(store, list) => {
let num = 5
while (num-- > 0) {
let item = list.shift()
if (!item) return
if (item.isComplate || item.status == state.downloadStatus.RUN || item.status == state.downloadStatus.WAITING) continue
let index = store.state.list.indexOf(item)
if (index < 0) continue
store.dispatch('startTask', item)
}
await awaitRequestAnimationFrame()
await startTasks(store, list)
}
const pauseTasks = async(store, list, runs = []) => {
let num = 6
let index
let stateList = store.state.list
while (num-- > 0) {
let item = list.shift()
if (item) {
if (item.isComplate) continue
switch (item.status) {
case state.downloadStatus.RUN:
runs.push(item)
continue
case state.downloadStatus.WAITING:
index = stateList.indexOf(item)
if (index < 0) return
store.dispatch('pauseTask', item)
continue
default:
continue
}
} else {
for (const item of runs) {
index = stateList.indexOf(item)
if (index < 0) return
await store.dispatch('pauseTask', item)
}
return
}
}
await awaitRequestAnimationFrame()
await pauseTasks(store, list, runs)
}
const handleGetMusicUrl = function(musicInfo, type, retryedSource = [], originMusic) {
// console.log(musicInfo.source)
if (!originMusic) originMusic = musicInfo
@ -182,6 +108,7 @@ const handleGetMusicUrl = function(musicInfo, type, retryedSource = [], originMu
}
const getMusicUrl = async function(downloadInfo, isUseOtherSource, isRefresh) {
downloadInfo = window.downloadListFullMap.get(downloadInfo.key)
const cachedUrl = await getMusicUrlFormStorage(downloadInfo.musicInfo, downloadInfo.type)
if (!downloadInfo.musicInfo._types[downloadInfo.type]) {
// 兼容旧版酷我源搜索列表过滤128k音质的bug
@ -224,7 +151,8 @@ const getPic = function(musicInfo, retryedSource = [], originMusic) {
})
})
}
const getLyric = function(musicInfo, retryedSource = [], originMusic) {
const handleGetLyric = function(musicInfo, retryedSource = [], originMusic) {
if (!originMusic) originMusic = musicInfo
let reqPromise
try {
@ -248,6 +176,33 @@ const getLyric = function(musicInfo, retryedSource = [], originMusic) {
})
}
const getLyric = function(musicInfo, isUseOtherSource) {
return getLyricFromStorage(musicInfo).then(lrcInfo => {
return (
lrcInfo.lyric
? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' })
: (
isUseOtherSource
? handleGetLyric.call(this, musicInfo)
: music[musicInfo.source].getLyric(musicInfo).promise
).then(({ lyric, tlyric, lxlyric }) => {
setLyric(musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
}).catch(err => {
console.log(err)
return null
})
).then(lrcs => {
if (!lrcs) return lrcs
if (global.i18n.locale != 'zh-tw') return lrcs
return rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(lrcs.lyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString()).then(lyric => {
lrcs.lyric = lyric
return lrcs
})
})
})
}
// 修复 1.1.x版本 酷狗源歌词格式
const fixKgLyric = lrc => /\[00:\d\d:\d\d.\d+\]/.test(lrc) ? lrc.replace(/(?:\[00:(\d\d:\d\d.\d+\]))/gm, '[$1') : lrc
@ -259,6 +214,7 @@ const fixKgLyric = lrc => /\[00:\d\d:\d\d.\d+\]/.test(lrc) ? lrc.replace(/(?:\[0
*/
const saveMeta = function(downloadInfo, filePath, isUseOtherSource, isEmbedPic, isEmbedLyric) {
if (downloadInfo.type === 'ape') return
downloadInfo = window.downloadListFullMap.get(downloadInfo.key)
const tasks = [
isEmbedPic
? downloadInfo.musicInfo.img
@ -273,21 +229,7 @@ const saveMeta = function(downloadInfo, filePath, isUseOtherSource, isEmbedPic,
})
: Promise.resolve(),
isEmbedLyric
? getLyricFromStorage(downloadInfo.musicInfo).then(lrcInfo => {
return lrcInfo.lyric
? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' })
: (
isUseOtherSource
? getLyric.call(this, downloadInfo.musicInfo)
: music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise
).then(({ lyric, tlyric, lxlyric }) => {
setLyric(downloadInfo.musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
}).catch(err => {
console.log(err)
return null
})
})
? getLyric.call(this, downloadInfo.musicInfo, isUseOtherSource)
: Promise.resolve(),
]
Promise.all(tasks).then(([imgUrl, lyrics = {}]) => {
@ -307,17 +249,10 @@ const saveMeta = function(downloadInfo, filePath, isUseOtherSource, isEmbedPic,
* @param {*} downloadInfo
* @param {*} filePath
*/
const downloadLyric = (downloadInfo, filePath, lrcFormat) => {
const promise = getLyric(downloadInfo.musicInfo).then(lrcInfo => {
return lrcInfo.lyric
? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' })
: music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise.then(({ lyric, tlyric, lxlyric }) => {
setLyric(downloadInfo.musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
})
})
promise.then(lrcs => {
if (lrcs.lyric) {
const downloadLyric = function(downloadInfo, isUseOtherSource, filePath, lrcFormat) {
downloadInfo = window.downloadListFullMap.get(downloadInfo.key)
getLyric.call(this, downloadInfo.musicInfo, isUseOtherSource).then(lrcs => {
if (lrcs?.lyric) {
lrcs.lyric = fixKgLyric(lrcs.lyric)
saveLrc(filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'), lrcs.lyric, lrcFormat)
}
@ -351,7 +286,7 @@ const refreshUrl = function(commit, downloadInfo, isUseOtherSource) {
*/
const deleteFile = path => new Promise((resolve, reject) => {
fs.access(path, fs.constants.F_OK, err => {
if (err) return reject(err)
if (err) return err.code == 'ENOENT' ? resolve() : reject(err)
fs.unlink(path, err => {
if (err) return reject(err)
resolve()
@ -359,54 +294,91 @@ const deleteFile = path => new Promise((resolve, reject) => {
})
})
const createDownloadInfo = ({ musicInfo, type, list, fileName, savePath }) => {
type = getMusicType(musicInfo, type)
let ext = getExt(type)
const key = `${musicInfo.songmid}${ext}`
if (checkList(list, musicInfo, type, ext)) return null
const downloadInfo = {
isComplate: false,
status: state.downloadStatus.WAITING,
statusText: '待下载',
url: null,
songmid: musicInfo.songmid,
fileName: filterFileName(`${fileName
.replace('歌名', musicInfo.name)
.replace('歌手', musicInfo.singer)}.${ext}`),
progress: {
downloaded: 0,
total: 0,
progress: 0,
},
type,
ext,
name: `${musicInfo.name} - ${musicInfo.singer}`,
key,
}
downloadInfo.filePath = path.join(savePath, downloadInfo.fileName)
// commit('addTask', downloadInfo)
// 删除同路径下的同名文件
deleteFile(downloadInfo.filePath)
// .catch(err => {
// if (err.code !== 'ENOENT') return commit('setStatusText', { downloadInfo, text: '文件删除失败' })
// })
if (dls[downloadInfo.key]) {
const dl = dls[downloadInfo.key]
delete dls[downloadInfo.key]
dl.stop()
}
return downloadInfo
}
// let waitingUpdateTasks = {}
// const delayUpdateProgress = throttle(function(commit) {
// commit('setProgressDelay')
// }, 1000)
// actions
const actions = {
async createDownload({ state, rootState, commit, dispatch }, { musicInfo, type }) {
let ext = getExt(type)
if (checkList(state.list, musicInfo, type, ext)) return
const downloadInfo = {
isComplate: false,
status: state.downloadStatus.WAITING,
statusText: '待下载',
url: null,
// songmid: musicInfo.songmid,
fileName: `${rootState.setting.download.fileName
.replace('歌名', musicInfo.name)
.replace('歌手', musicInfo.singer)}.${ext}`.replace(filterFileName, ''),
progress: {
downloaded: 0,
total: 0,
progress: 0,
},
type,
ext,
const downloadInfo = createDownloadInfo({
musicInfo,
key: `${musicInfo.songmid}${ext}`,
}
downloadInfo.filePath = path.join(rootState.setting.download.savePath, downloadInfo.fileName)
commit('addTask', downloadInfo)
try { // 删除同路径下的同名文件
await deleteFile(downloadInfo.filePath)
} catch (err) {
if (err.code !== 'ENOENT') return commit('setStatusText', { downloadInfo, text: '文件删除失败' })
}
if (dls[downloadInfo.key]) {
dls[downloadInfo.key].stop().finally(() => {
delete dls[downloadInfo.key]
dispatch('startTask', downloadInfo)
})
} else {
// console.log(downloadInfo)
dispatch('startTask', downloadInfo)
type,
fileName: rootState.setting.download.fileName,
savePath: rootState.setting.download.savePath,
list: state.list,
})
if (!downloadInfo) return
commit('addTask', { downloadInfo, musicInfo, addMusicLocationType: rootState.setting.list.addMusicLocationType })
let result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
while (result) {
dispatch('startTask', result)
result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
}
},
createDownloadMultiple(store, { list, type }) {
if (!list.length || isRuningActionTask) return
isRuningActionTask = true
return addTasks(store, [...list], type).finally(() => {
isRuningActionTask = false
})
createDownloadMultiple({ state, rootState, commit, dispatch }, { list, type }) {
if (!list.length) return
const downloadList = []
for (const musicInfo of list) {
const downloadInfo = createDownloadInfo({
musicInfo,
type,
fileName: rootState.setting.download.fileName,
savePath: rootState.setting.download.savePath,
list: state.list,
})
if (downloadInfo) downloadList.push({ downloadInfo, musicInfo })
}
commit('addTasks', { list: downloadList, addMusicLocationType: rootState.setting.list.addMusicLocationType })
let result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
while (result) {
dispatch('startTask', result)
result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
}
},
async handleStartTask({ commit, dispatch, rootState }, downloadInfo) {
// 开始任务
@ -416,7 +388,7 @@ const actions = {
await checkPath(rootState.setting.download.savePath)
} catch (error) {
commit('onError', { downloadInfo, errorMsg: error.message })
commit('setStatusText', '检查下载目录出错: ' + error.message)
commit('setStatusText', { downloadInfo, text: '检查下载目录出错: ' + error.message })
await dispatch('startTask')
return
}
@ -436,13 +408,13 @@ const actions = {
dispatch('startTask')
saveMeta.call(_this, downloadInfo, downloadInfo.filePath, rootState.setting.download.isUseOtherSource, rootState.setting.download.isEmbedPic, rootState.setting.download.isEmbedLyric)
if (rootState.setting.download.isDownloadLrc) downloadLyric(downloadInfo, downloadInfo.filePath, rootState.setting.download.lrcFormat)
if (rootState.setting.download.isDownloadLrc) downloadLyric.call(_this, downloadInfo, rootState.setting.download.isUseOtherSource, downloadInfo.filePath, rootState.setting.download.lrcFormat)
console.log('on complate')
},
onError(err) {
// console.log(err)
console.log(err)
if (err.code == 'EPERM') {
commit('onError', { downloadInfo, errorMsg: '歌曲下载目录没有写入权限,请尝试更改歌曲保存路径' })
commit('onError', { downloadInfo, errorMsg: '歌曲保存位置被占用或没有写入权限,请尝试更改歌曲保存目录或重启软件或重启电脑,错误详情:' + err.message })
return
}
// console.log(tryNum[downloadInfo.key])
@ -531,28 +503,38 @@ const actions = {
await dispatch('startTask')
}
},
removeTasks(store, list) {
let { rootState, state } = store
if (isRuningActionTask) return
isRuningActionTask = true
return removeTasks(store, [...list]).finally(() => {
let result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
while (result) {
store.dispatch('startTask', result)
result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
removeTasks({ rootState, commit, dispatch }, list) {
for (const item of list) {
if (dls[item.key]) {
if (item.status == state.downloadStatus.RUN) {
dls[item.key].stop().finally(() => {
delete dls[item.key]
})
} else {
delete dls[item.key]
}
}
isRuningActionTask = false
})
if (item.status != state.downloadStatus.COMPLETED) {
deleteFile(item.filePath).catch(_ => _)
}
}
commit('removeTasks', list)
let result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
while (result) {
dispatch('startTask', result)
result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
}
},
async startTask({ state, rootState, commit, dispatch }, downloadInfo) {
// 检查是否可以开始任务
let result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
if (downloadInfo && !downloadInfo.isComplate && downloadInfo.status != state.downloadStatus.RUN) {
const result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
if (result === false) {
commit('setStatus', { downloadInfo, status: state.downloadStatus.WAITING })
return
}
} else {
const result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
if (!result) return
downloadInfo = result
}
@ -575,52 +557,152 @@ const actions = {
await dispatch('handleStartTask', downloadInfo)
}
},
startTasks(store, list) {
if (isRuningActionTask) return
isRuningActionTask = true
return startTasks(store, list.filter(item => !(item.isComplate || item.status == state.downloadStatus.RUN || item.status == state.downloadStatus.WAITING))).finally(() => {
isRuningActionTask = false
})
startTasks({ commit, rootState, dispatch }, list) {
list = list.filter(item => !(item.isComplate || item.status == state.downloadStatus.RUN || item.status == state.downloadStatus.WAITING))
commit('setStatus', { list, status: state.downloadStatus.WAITING })
let result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
while (result) {
dispatch('startTask', result)
result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
}
},
async pauseTask(store, item) {
async pauseTask({ commit }, item) {
if (item.isComplate) return
let dl = dls[item.key]
if (dl) {
try {
await dl.stop()
} catch (_) {}
}
store.commit('pauseTask', item)
if (dl) dl.stop()
commit('setStatus', { downloadInfo: item, status: state.downloadStatus.PAUSE })
},
pauseTasks(store, list) {
if (isRuningActionTask) return
isRuningActionTask = true
return pauseTasks(store, [...list]).finally(() => {
isRuningActionTask = false
})
pauseTasks({ commit, rootState, dispatch }, list) {
const waitingTasks = list.filter(item => item.status == state.downloadStatus.WAITING)
commit('setStatus', { list: waitingTasks, status: state.downloadStatus.PAUSE })
const runningTasks = list.filter(item => item.status == state.downloadStatus.RUN)
for (const item of runningTasks) {
if (item.isComplate) return
let dl = dls[item.key]
if (dl) dl.stop()
}
commit('setStatus', { list: runningTasks, status: state.downloadStatus.PAUSE })
},
}
// mitations
const mutations = {
addTask(state, downloadInfo) {
state.list.unshift(downloadInfo)
addTask(state, { downloadInfo, musicInfo, addMusicLocationType }) {
const downloadInfoFull = { ...downloadInfo, musicInfo }
window.downloadListFullMap.set(downloadInfo.key, downloadInfoFull)
switch (addMusicLocationType) {
case 'top':
window.downloadListFull.unshift(downloadInfoFull)
state.list.unshift(downloadInfo)
break
case 'bottom':
default:
window.downloadListFull.push(downloadInfoFull)
state.list.push(downloadInfo)
break
}
},
addTasks(state, { list, addMusicLocationType }) {
const downloadInfoList = []
const curDownloadListFull = []
for (const { downloadInfo, musicInfo } of list) {
downloadInfoList.push(downloadInfo)
curDownloadListFull.push({ ...downloadInfo, musicInfo })
}
let newList
let newListFull
const map = {}
const fullMap = {}
const ids = []
switch (addMusicLocationType) {
case 'top':
newList = [...downloadInfoList, ...state.list]
newListFull = [...curDownloadListFull, ...window.downloadListFull]
for (let i = newList.length - 1; i > -1; i--) {
const item = newList[i]
if (map[item.key]) continue
ids.unshift(item.key)
map[item.key] = item
fullMap[item.key] = newListFull[i]
}
break
case 'bottom':
default:
newList = [...state.list, ...downloadInfoList]
newListFull = [...window.downloadListFull, ...curDownloadListFull]
newList.forEach((item, index) => {
if (map[item.key]) return
ids.push(item.key)
map[item.key] = item
fullMap[item.key] = newListFull[index]
})
break
}
window.downloadListFullMap.clear()
window.downloadListFull = ids.map(id => {
const info = fullMap[id]
window.downloadListFullMap.set(info.key, info)
return info
})
state.list.splice(0, state.list.length, ...ids.map(id => map[id]))
},
removeTask({ list }, downloadInfo) {
list.splice(list.indexOf(downloadInfo), 1)
const index = list.findIndex(m => m.key == downloadInfo.key)
if (index < 0) return
window.downloadListFull.splice(index, 1)
window.downloadListFullMap.delete(downloadInfo.key)
list.splice(index, 1)
},
removeTasks(state, list) {
let map = {}
let ids = []
for (const item of state.list) {
ids.push(item.key)
map[item.key] = item
}
for (const { key } of list) {
if (map[key]) delete map[key]
}
let newList = []
let newListFull = []
for (const id of ids) {
if (map[id]) {
newList.push(map[id])
newListFull.push(window.downloadListFullMap.get(id))
}
}
window.downloadListFull = newListFull
window.downloadListFullMap.clear()
for (const item of newListFull) {
window.downloadListFullMap.set(item.key, item)
}
state.list.splice(0, state.list.length, ...newList)
},
pauseTask(state, downloadInfo) {
const index = state.list.findIndex(m => m.key == downloadInfo.key)
if (index < 0) return
const downloadInfoFull = window.downloadListFull[index]
downloadInfoFull.status = state.downloadStatus.PAUSE
downloadInfoFull.statusText = '暂停下载'
downloadInfo.status = state.downloadStatus.PAUSE
downloadInfo.statusText = '暂停下载'
},
setStatusText(state, { downloadInfo, index, text }) { // 设置状态文本
if (downloadInfo) {
const index = state.list.findIndex(m => m.key == downloadInfo.key)
if (index < 0) return
const downloadInfoFull = window.downloadListFull[index]
if (downloadInfoFull) downloadInfoFull.statusText = text
downloadInfo.statusText = text
} else {
state.list[index].statusText = text
const downloadInfoFull = window.downloadListFull[index]
if (downloadInfoFull) downloadInfoFull.statusText = text
}
},
setStatus(state, { downloadInfo, index, status }) { // 设置状态及状态文本
setStatus(state, { downloadInfo, index, status, list }) { // 设置状态及状态文本
let text
switch (status) {
case state.downloadStatus.RUN:
@ -639,43 +721,107 @@ const mutations = {
text = '下载完成'
break
}
if (downloadInfo) {
downloadInfo.statusText = text
downloadInfo.status = status
if (list) {
for (const downloadInfo of list) {
const index = state.list.findIndex(m => m.key == downloadInfo.key)
if (index < 0) return
const downloadInfoFull = window.downloadListFull[index]
downloadInfoFull.statusText = text
downloadInfoFull.status = status
downloadInfo.statusText = text
downloadInfo.status = status
}
} else {
state.list[index].statusText = text
state.list[index].status = status
if (downloadInfo) {
const index = state.list.findIndex(m => m.key == downloadInfo.key)
if (index < 0) return
const downloadInfoFull = window.downloadListFull[index]
downloadInfoFull.statusText = text
downloadInfoFull.status = status
downloadInfo.statusText = text
downloadInfo.status = status
} else {
const downloadInfoFull = window.downloadListFull[index]
downloadInfoFull.statusText = text
downloadInfoFull.status = status
state.list[index].statusText = text
state.list[index].status = status
}
}
},
onCompleted(state, downloadInfo) {
const index = state.list.findIndex(m => m.key == downloadInfo.key)
console.log(index)
if (index < 0) return
const downloadInfoFull = window.downloadListFull[index]
downloadInfoFull.isComplate = true
downloadInfoFull.status = state.downloadStatus.COMPLETED
downloadInfoFull.statusText = '下载完成'
downloadInfo.isComplate = true
downloadInfo.status = state.downloadStatus.COMPLETED
downloadInfo.statusText = '下载完成'
},
onError(state, { downloadInfo, errorMsg }) {
const index = state.list.findIndex(m => m.key == downloadInfo.key)
if (index < 0) return
const downloadInfoFull = window.downloadListFull[index]
downloadInfoFull.status = state.downloadStatus.ERROR
downloadInfoFull.statusText = errorMsg || '任务出错'
downloadInfo.status = state.downloadStatus.ERROR
downloadInfo.statusText = errorMsg || '任务出错'
},
onStart(state, downloadInfo) {
const index = state.list.findIndex(m => m.key == downloadInfo.key)
if (index < 0) return
const downloadInfoFull = window.downloadListFull[index]
downloadInfoFull.status = state.downloadStatus.RUN
downloadInfoFull.statusText = '正在下载'
downloadInfo.status = state.downloadStatus.RUN
downloadInfo.statusText = '正在下载'
},
onProgress(state, { downloadInfo, status }) {
const index = state.list.findIndex(m => m.key == downloadInfo.key)
if (index < 0) return
const downloadInfoFull = window.downloadListFull[index]
downloadInfoFull.progress.progress = status.progress
downloadInfoFull.progress.downloaded = status.downloaded
downloadInfoFull.progress.total = status.total
downloadInfo.progress.progress = status.progress
downloadInfo.progress.downloaded = status.downloaded
downloadInfo.progress.total = status.total
},
setTotal(state, { order, downloadInfo }) {
downloadInfo.order = order
},
updateDownloadList(state, list) {
state.list = window.downloadList = list
window.downloadListFullMap.clear()
const stateList = list.map(downloadInfoFull => {
window.downloadListFullMap.set(downloadInfoFull.key, downloadInfoFull)
const downloadInfo = { ...downloadInfoFull }
delete downloadInfo.musicInfo
return downloadInfo
})
window.downloadListFull = list
state.list = window.downloadList = stateList
},
updateUrl(state, { downloadInfo, url }) {
const index = state.list.findIndex(m => m.key == downloadInfo.key)
if (index < 0) return
const downloadInfoFull = window.downloadListFull[index]
downloadInfoFull.url = url
downloadInfo.url = url
},
updateFilePath(state, { downloadInfo, filePath }) {
if (downloadInfo.filePath === filePath) return
const index = state.list.findIndex(m => m.key == downloadInfo.key)
if (index < 0) return
const downloadInfoFull = window.downloadListFull[index]
downloadInfoFull.filePath = filePath
downloadInfo.filePath = filePath
},
}

View File

@ -9,9 +9,7 @@ for (const source of music.sources) {
sources.push(source)
}
// state
const state = {
boards: sourceList,
const listInfo = {
list: [],
total: 0,
page: 1,
@ -19,6 +17,11 @@ const state = {
key: null,
}
// state
const state = {
boards: sourceList,
}
// getters
const getters = {
sources(state, getters, rootState, { sourceNames }) {
@ -27,16 +30,6 @@ const getters = {
boards(state) {
return state.boards
},
list(state) {
return state.list
},
info(state) {
return {
total: state.total,
limit: state.limit,
page: state.page,
}
},
}
// actions
@ -47,7 +40,6 @@ const actions = {
// let tabId = rootState.setting.leaderboard.tabId
// let key = `${source}${tabId}${page}`
// if (state.list.length && state.key == key) return true
// commit('clearList')
if (state.boards[source].length) return
return music[source].leaderboard.getBoards().then(result => commit('setBoardsList', { boards: result, source }))
},
@ -56,14 +48,22 @@ const actions = {
let tabId = rootState.setting.leaderboard.tabId
let [source, bangId] = tabId.split('__')
let key = `${source}${tabId}${page}`
if (state.list.length && state.key == key) return Promise.resolve()
commit('clearList')
if (listInfo.list.length && listInfo.key == key) return Promise.resolve(listInfo)
// commit('clearList')
// return (
// cache.has(key)
// ? Promise.resolve(cache.get(key))
// : music[source].leaderboard.getList(bangId, page)
// ).then(result => commit('setList', { result, key }))
return music[source].leaderboard.getList(bangId, page).then(result => commit('setList', { result, key }))
return music[source].leaderboard.getList(bangId, page).then(result => {
cache.set(key, result)
listInfo.list = result.list
listInfo.total = result.total
listInfo.limit = result.limit
listInfo.page = result.page
listInfo.key = key
return listInfo
})
},
getListAll({ state, rootState }, id) {
// console.log(source, id)
@ -96,18 +96,6 @@ const mutations = {
setBoardsList(state, { boards, source }) {
state.boards[source] = boards.list
},
setList(state, { result, key }) {
state.list = result.list
state.total = result.total
state.limit = result.limit
state.page = result.page
state.key = key
cache.set(key, result)
},
clearList(state) {
state.list = []
state.total = 0
},
}
export default {

View File

@ -1,6 +1,7 @@
import musicSdk from '../../utils/music'
import { clearLyric, clearMusicUrl } from '../../utils'
import { sync as eventSyncName } from '@renderer/event/names'
import { removeListPosition, setListPrevSelectId } from '@renderer/utils/data'
let allList = {}
window.allList = allList
@ -27,19 +28,16 @@ const state = {
id: 'default',
name: '试听列表',
list: [],
location: 0,
},
loveList: {
id: 'love',
name: '我的收藏',
list: [],
location: 0,
},
tempList: {
id: 'temp',
name: '临时列表',
list: [],
location: 0,
},
userList: [],
}
@ -53,21 +51,29 @@ const getters = {
allList: () => allList,
}
const getOtherSourcePromises = new Map()
// actions
const actions = {
getOtherSource({ state, commit }, musicInfo) {
return (musicInfo.otherSource && musicInfo.otherSource.length ? Promise.resolve(musicInfo.otherSource) : musicSdk.findMusic(musicInfo)).then(otherSource => {
if (musicInfo.otherSource?.length) return Promise.resolve(musicInfo.otherSource)
let key = `${musicInfo.source}_${musicInfo.songmid}`
if (getOtherSourcePromises.has(key)) return getOtherSourcePromises.get(key)
const promise = musicSdk.findMusic(musicInfo).then(otherSource => {
commit('setOtherSource', { musicInfo, otherSource })
if (getOtherSourcePromises.has(key)) getOtherSourcePromises.delete(key)
return otherSource
})
getOtherSourcePromises.set(key, promise)
return promise
},
}
// mitations
const mutations = {
initList(state, { defaultList, loveList, userList }) {
if (defaultList != null) Object.assign(state.defaultList, { list: defaultList.list, location: defaultList.location })
if (loveList != null) Object.assign(state.loveList, { list: loveList.list, location: loveList.location })
if (defaultList != null) Object.assign(state.defaultList, { list: defaultList.list })
if (loveList != null) Object.assign(state.loveList, { list: loveList.list })
if (userList != null) state.userList = userList
allListInit(state.defaultList, state.loveList, state.userList)
state.isInitedList = true
@ -95,18 +101,17 @@ const mutations = {
state.userList = userList
allListInit(state.defaultList, state.loveList, state.userList)
},
setList(state, { id, list, name, location, source, sourceListId, isSync }) {
setList(state, { id, list, name, source, sourceListId, isSync }) {
const targetList = allList[id]
if (targetList) {
if (name && targetList.name === name) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'set_list',
data: { id, list, name, location, source, sourceListId },
data: { id, list, name, source, sourceListId },
})
}
targetList.list.splice(0, targetList.list.length, ...list)
targetList.location = location
return
}
@ -115,14 +120,13 @@ const mutations = {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'set_list',
data: { id, list, name, location, source, sourceListId },
data: { id, list, name, source, sourceListId },
})
}
let newList = {
name,
id,
list,
location,
source,
sourceListId,
}
@ -293,11 +297,11 @@ const mutations = {
const targetMusicInfo = targetList.list.find(item => item.songmid == id)
if (targetMusicInfo) Object.assign(targetMusicInfo, data)
},
createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, isSync }) {
createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, position, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'create_user_list',
data: { name, id, list, source, sourceListId },
data: { name, id, list, source, sourceListId, position },
})
}
@ -307,11 +311,14 @@ const mutations = {
name,
id,
list: [],
location: 0,
source,
sourceListId,
}
state.userList.push(newList)
if (position == null) {
state.userList.push(newList)
} else {
state.userList.splice(position + 1, 0, newList)
}
allListUpdate(newList)
}
this.commit('list/listAddMultiple', { id, list, isSync: true })
@ -328,6 +335,7 @@ const mutations = {
if (index < 0) return
let list = state.userList.splice(index, 1)[0]
allListRemove(list)
removeListPosition(id)
},
setUserListName(state, { id, name, isSync }) {
if (!isSync) {
@ -368,9 +376,6 @@ const mutations = {
state.userList.splice(index, 1)
state.userList.splice(index + 1, 0, targetList)
},
setListScroll(state, { id, location }) {
if (allList[id]) allList[id].location = location
},
setMusicPosition(state, { id, position, list, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
@ -406,6 +411,9 @@ const mutations = {
setOtherSource(state, { musicInfo, otherSource }) {
musicInfo.otherSource = otherSource
},
setPrevSelectListId(state, val) {
setListPrevSelectId(val)
},
}
export default {

View File

@ -18,7 +18,6 @@ const state = {
},
playIndex: -1,
changePlay: false,
isShowPlayerDetail: false,
playedList: [],
playMusicInfo: null,
@ -40,7 +39,7 @@ const filterList = async({ playedList, listInfo, savePath, commit }) => {
canPlayList.push(item)
// 排除已播放音乐
let index = filteredPlayedList.findIndex(m => (m.songmid || m.musicInfo.songmid) == item.musicInfo.songmid)
let index = filteredPlayedList.findIndex(m => m.songmid == item.songmid)
if (index > -1) {
filteredPlayedList.splice(index, 1)
continue
@ -52,7 +51,7 @@ const filterList = async({ playedList, listInfo, savePath, commit }) => {
// if (!assertApiSupport(s.source)) return false
canPlayList.push(s)
let index = filteredPlayedList.findIndex(m => (m.songmid || m.musicInfo.songmid) == s.songmid)
let index = filteredPlayedList.findIndex(m => m.songmid == s.songmid)
if (index > -1) {
filteredPlayedList.splice(index, 1)
return false
@ -156,15 +155,23 @@ const getters = {
let listPlayIndex = Math.min(state.playIndex, state.listInfo.list.length - 1)
if (listId != '__temp__') {
const currentSongmid = state.playMusicInfo.musicInfo.songmid || state.playMusicInfo.musicInfo.musicInfo.songmid
if (isPlayList) {
playIndex = state.listInfo.list.findIndex(m => (m.songmid || m.musicInfo.songmid) == currentSongmid)
if (!isTempPlay) listPlayIndex = playIndex
} else if (listId == 'download') {
playIndex = window.downloadList.findIndex(m => m.musicInfo.songmid == currentSongmid)
if (state.playMusicInfo.musicInfo.key) {
const currentKey = state.playMusicInfo.musicInfo.key
if (isPlayList) {
playIndex = state.listInfo.list.findIndex(m => m.key == currentKey)
if (!isTempPlay) listPlayIndex = playIndex
} else if (listId == 'download') {
playIndex = window.downloadList.findIndex(m => m.key == currentKey)
}
} else {
let list = window.allList[listId]
if (list) playIndex = list.list.findIndex(m => m.songmid == currentSongmid)
const currentSongmid = state.playMusicInfo.musicInfo.songmid
if (isPlayList) {
playIndex = state.listInfo.list.findIndex(m => m.songmid == currentSongmid)
if (!isTempPlay) listPlayIndex = playIndex
} else {
let list = window.allList[listId]
if (list) playIndex = list.list.findIndex(m => m.songmid == currentSongmid)
}
}
}
if (listPlayIndex >= 0) prevListPlayIndex = listPlayIndex
@ -187,6 +194,8 @@ const getters = {
// isTempPlay,
// // musicInfo: state.playMusicInfo.musicInfo,
// })
// console.log(state.playMusicInfo)
return {
listId,
playIndex,
@ -197,7 +206,6 @@ const getters = {
musicInfo: state.playMusicInfo.musicInfo,
}
},
isShowPlayerDetail: state => state.isShowPlayerDetail,
playMusicInfo: state => state.playMusicInfo,
playedList: state => state.playedList,
tempPlayList: state => state.tempPlayList,
@ -267,16 +275,16 @@ const actions = {
let currentSongmid
if (state.playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.listPlayIndex]
if (musicInfo) currentSongmid = musicInfo.songmid || musicInfo.musicInfo.songmid
if (musicInfo) currentSongmid = musicInfo.songmid
} else {
currentSongmid = state.playMusicInfo.musicInfo.songmid || state.playMusicInfo.musicInfo.musicInfo.songmid
currentSongmid = state.playMusicInfo.musicInfo.songmid
}
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = state.playedList.findIndex(m => (m.musicInfo.songmid || m.musicInfo.musicInfo.songmid) === currentSongmid) - 1; index > -1; index--) {
for (index = state.playedList.findIndex(m => m.musicInfo.songmid === currentSongmid) - 1; index > -1; index--) {
const playMusicInfo = state.playedList[index]
const currentSongmid = playMusicInfo.musicInfo.songmid || playMusicInfo.musicInfo.musicInfo.songmid
if (playMusicInfo.listId == currentListId && !currentList.some(m => (m.songmid || m.musicInfo.songmid) === currentSongmid)) {
const currentSongmid = playMusicInfo.musicInfo.songmid
if (playMusicInfo.listId == currentListId && !currentList.some(m => m.songmid === currentSongmid)) {
commit('removePlayedList', index)
continue
}
@ -343,16 +351,16 @@ const actions = {
let currentSongmid
if (state.playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.listPlayIndex]
if (musicInfo) currentSongmid = musicInfo.songmid || musicInfo.musicInfo.songmid
if (musicInfo) currentSongmid = musicInfo.songmid
} else {
currentSongmid = state.playMusicInfo.musicInfo.songmid || state.playMusicInfo.musicInfo.musicInfo.songmid
currentSongmid = state.playMusicInfo.musicInfo.songmid
}
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = state.playedList.findIndex(m => (m.musicInfo.songmid || m.musicInfo.musicInfo.songmid) === currentSongmid) + 1; index < state.playedList.length; index++) {
for (index = state.playedList.findIndex(m => m.musicInfo.songmid === currentSongmid) + 1; index < state.playedList.length; index++) {
const playMusicInfo = state.playedList[index]
const currentSongmid = playMusicInfo.musicInfo.songmid || playMusicInfo.musicInfo.musicInfo.songmid
if (playMusicInfo.listId == currentListId && !currentList.some(m => (m.songmid || m.musicInfo.songmid) === currentSongmid)) {
const currentSongmid = playMusicInfo.musicInfo.songmid
if (playMusicInfo.listId == currentListId && !currentList.some(m => m.songmid === currentSongmid)) {
commit('removePlayedList', index)
continue
}
@ -457,9 +465,6 @@ const mutations = {
clearPlayedList(state) {
state.playedList.splice(0, state.playedList.length)
},
visiblePlayerDetail(state, visible) {
state.isShowPlayerDetail = visible
},
setTempPlayList(state, list) {
state.tempPlayList.push(...list.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true })))
if (!state.playMusicInfo) this.commit('player/playNext')
@ -478,8 +483,8 @@ const mutations = {
} else {
let listId = playMusicInfo.listId
if (listId != '__temp__' && !playMusicInfo.isTempPlay && listId === state.listInfo.id) {
const currentSongmid = playMusicInfo.musicInfo.songmid || playMusicInfo.musicInfo.musicInfo.songmid
playIndex = state.listInfo.list.findIndex(m => (m.songmid || m.musicInfo.songmid) == currentSongmid)
const currentSongmid = playMusicInfo.musicInfo.songmid
playIndex = state.listInfo.list.findIndex(m => m.songmid == currentSongmid)
}
}

View File

@ -61,9 +61,6 @@ export default {
setMediaDeviceId(state, val) {
state.setting.player.mediaDeviceId = val
},
setPrevSelectListId(state, val) {
state.setting.list.prevSelectListId = val
},
setDesktopLyricConfig(state, config) {
state.setting.desktopLyric = Object.assign(state.setting.desktopLyric, config)
},

View File

@ -0,0 +1,45 @@
import { rendererSend, rendererInvoke, NAMES } from '../../common/ipc'
import { throttle } from './index'
let listPosition = {}
let listPrevSelectId
const saveListPosition = throttle(() => {
rendererSend(NAMES.mainWindow.save_data, {
path: 'listPosition',
data: listPosition,
})
}, 1000)
export const initListPosition = () => {
return rendererInvoke(NAMES.mainWindow.get_data, 'listPosition').then(data => {
if (!data) data = {}
listPosition = data
})
}
export const getListPosition = id => listPosition[id] || 0
export const setListPosition = (id, position) => {
listPosition[id] = position || 0
saveListPosition()
}
export const removeListPosition = id => {
delete listPosition[id]
saveListPosition()
}
const saveListPrevSelectId = throttle(() => {
rendererSend(NAMES.mainWindow.save_data, {
path: 'listPrevSelectId',
data: listPrevSelectId,
})
}, 200)
export const initListPrevSelectId = () => {
return rendererInvoke(NAMES.mainWindow.get_data, 'listPrevSelectId').then(id => {
listPrevSelectId = id
})
}
export const getListPrevSelectId = () => listPrevSelectId
export const setListPrevSelectId = id => {
listPrevSelectId = id
saveListPrevSelectId()
}

View File

@ -4,6 +4,7 @@ import { shell, clipboard } from 'electron'
import crypto from 'crypto'
import { rendererSend, rendererInvoke, NAMES } from '../../common/ipc'
import iconv from 'iconv-lite'
import { gzip, gunzip } from 'zlib'
/**
* 获取两个数之间的随机整数大于等于min小于max
@ -433,3 +434,50 @@ export const setMusicUrl = (musicInfo, type, url) => rendererSend(NAMES.mainWind
url,
})
export const clearMusicUrl = () => rendererSend(NAMES.mainWindow.clear_music_url)
export const gzipData = str => {
return new Promise((resolve, reject) => {
gzip(str, (err, result) => {
if (err) return reject(err)
resolve(result)
})
})
}
export const gunzipData = buf => {
return new Promise((resolve, reject) => {
gunzip(buf, (err, result) => {
if (err) return reject(err)
resolve(result.toString())
})
})
}
export const saveLxConfigFile = async(path, data) => {
if (!path.endsWith('.lxmc')) path += '.lxmc'
fs.writeFile(path, await gzipData(JSON.stringify(data)), 'binary', err => {
console.log(err)
})
}
export const readLxConfigFile = async path => {
let isJSON = path.endsWith('.json')
let data = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary')
if (!data || isJSON) return data
data = await gunzipData(Buffer.from(data, 'binary'))
data = JSON.parse(data.toString('utf8'))
// 修复v1.14.0出现的导出数据被序列化两次的问题
if (typeof data != 'object') {
try {
data = JSON.parse(data)
} catch (err) {
return data
}
}
return data
}
const fileNameRxp = /[\\/:*?#"<>|]/g
export const filterFileName = name => name.replace(fileNameRxp, '')

View File

@ -120,6 +120,7 @@ module.exports = class Lyric {
return {
text: line.text,
time: line.time,
translation: line.translation,
dom_line: fontPlayer.lineContent,
}
})

View File

@ -48,7 +48,7 @@ module.exports = class LinePlayer {
_initLines() {
this.lines = []
this.translationLines = []
const lines = this.lyric.split('\n')
const lines = this.lyric.split(/\r\n|\r|\n/)
const linesMap = {}
// const translationLines = this.translationLyric.split('\n')
for (let i = 0; i < lines.length; i++) {

View File

@ -29,7 +29,8 @@ export default {
const { body, statusCode } = await _requestObj2.promise
// console.log(body)
if (statusCode != 200 || body.err_code !== 0) throw new Error('获取热门评论失败')
return { source: 'kg', comments: this.filterComment(body.weightList || []) }
const total = body.weightList?.length ?? 0
return { source: 'kg', comments: this.filterComment(body.weightList || []), total, page, limit, maxPage: 1 }
},
async getReplyComment({ songmid, audioId }, replyId, page = 1, limit = 100) {
if (this._requestObj2) this._requestObj2.cancelHttp()

View File

@ -1,6 +1,17 @@
import { httpFetch } from '../../request'
import { decodeName, formatPlayTime, sizeFormate } from '../../index'
import { toMD5 } from '../utils'
import infSign from './vendors/infSign.min'
const handleSignature = (id, page, limit) => new Promise((resolve, reject) => {
infSign({ appid: 1058, type: 0, module: 'playlist', page, pagesize: limit, specialid: id }, null, {
useH5: !0,
isCDN: !0,
callback(i) {
resolve(i.signature)
},
})
})
export default {
_requestObj_tags: null,
@ -35,6 +46,7 @@ export default {
id: '8',
},
],
cache: new Map(),
regExps: {
listData: /global\.data = (\[.+\]);/,
listInfo: /global = {[\s\S]+?name: "(.+)"[\s\S]+?pic: "(.+)"[\s\S]+?};/,
@ -249,7 +261,7 @@ export default {
})
if (!songInfo.list) {
if (songInfo.global_collection_id) return this.getUserListDetail2(songInfo.global_collection_id)
else throw new Error('fail')
else return this.getUserListDetail4(songInfo, chain, page).catch(() => this.getUserListDetail5(chain))
}
let result = await Promise.all(this.createTask(songInfo.list.map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
// console.log(info, songInfo)
@ -362,6 +374,94 @@ export default {
}
},
async getListInfoByChain(chain) {
if (this.cache.has(chain)) return this.cache.get(chain)
const { body } = await httpFetch(`https://m.kugou.com/share/?chain=${chain}&id=${chain}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
},
}).promise
let result = body.match(/var\sphpParam\s=\s({.+?});/)
if (result) result = JSON.parse(result[1])
this.cache.set(chain, result)
return result
},
async getUserListDetailByPcChain(chain) {
let key = `${chain}_pc_list`
if (this.cache.has(key)) return this.cache.get(key)
const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
},
}).promise
let result = body.match(/var\sdataFromSmarty\s=\s(\[.+?\])/)
if (result) result = JSON.parse(result[1])
this.cache.set(chain, result)
result = await Promise.all(this.createTask(result.map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
// console.log(info, songInfo)
return this.filterData2(result)
},
async getUserListDetail4(songInfo, chain, page) {
const limit = 100
const [listInfo, list] = await Promise.all([
this.getListInfoByChain(chain),
this.getUserListDetailById(songInfo.id, page, limit),
])
return {
list: list || [],
page,
limit,
total: listInfo.songcount,
source: 'kg',
info: {
name: listInfo.specialname,
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.nickname,
// play_count: this.formatPlayCount(info.count),
},
}
},
async getUserListDetail5(chain) {
const [listInfo, list] = await Promise.all([
this.getListInfoByChain(chain),
this.getUserListDetailByPcChain(chain),
])
return {
list: list || [],
page: 1,
limit: this.listDetailLimit,
total: listInfo.songcount,
source: 'kg',
info: {
name: listInfo.specialname,
img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.nickname,
// play_count: this.formatPlayCount(info.count),
},
}
},
async getUserListDetailById(id, page, limit) {
const signature = await handleSignature(id, page, limit)
let info = await this.createHttp(`https://pubsongscdn.kugou.com/v2/get_other_list_file?srcappid=2919&clientver=20000&appid=1058&type=0&module=playlist&page=${page}&pagesize=${limit}&specialid=${id}&signature=${signature}`, {
headers: {
Referer: 'https://m3ws.kugou.com/share/index.php',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
dfid: '-',
},
})
// console.log(info)
let result = await Promise.all(this.createTask(info.info.map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
// console.log(info, songInfo)
return this.filterData2(result)
},
async getUserListDetail(link, page, retryNum = 0) {
if (retryNum > 3) return Promise.reject(new Error('link try max num'))
if (link.includes('#')) link = link.replace(/#.*$/, '')

File diff suppressed because one or more lines are too long

View File

@ -28,7 +28,7 @@ export default {
const { body, statusCode } = await _requestObj2.promise
if (statusCode != 200 || body.result !== 'ok') throw new Error('获取热门评论失败')
// console.log(body)
return { source: 'kw', comments: this.filterComment(body.rows) }
return { source: 'kw', comments: this.filterComment(body.rows), total: body.total, page, limit, maxPage: Math.ceil(body.total / limit) || 1 }
},
filterComment(rawList) {
if (!rawList) return []

View File

@ -53,7 +53,7 @@ export default {
const { body, statusCode } = await _requestObj2.promise
// console.log(body)
if (statusCode != 200 || body.returnCode !== '000000') throw new Error('获取热门评论失败')
return { source: 'mg', comments: this.filterComment(body.data.items) }
return { source: 'mg', comments: this.filterComment(body.data.items), total: body.data.itemTotal, page, limit, maxPage: Math.ceil(body.data.itemTotal / limit) || 1 }
},
async getReplyComment(musicInfo, replyId, page = 1, limit = 10) {
if (this._requestObj2) this._requestObj2.cancelHttp()

View File

@ -1,5 +1,5 @@
import { apis } from '../api-source'
import leaderboard from './leaderboard2'
import leaderboard from './leaderboard'
import songList from './songList'
import musicSearch from './musicSearch'
import pic from './pic'

View File

@ -2,7 +2,9 @@ import { httpFetch } from '../../request'
import { sizeFormate } from '../../index'
const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]
// const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]
const boardList = [{ id: 'mg__27553319', name: '尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: 'KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '网络榜', bangid: '15140034' }, { id: 'mg__21958042', name: '美国iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: '美国billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]
export default {
limit: 200,
@ -59,7 +61,7 @@ export default {
},
],
getUrl(id, page) {
return `https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/rank-detail/release?columnId=${id}`
return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/querycontentbyId.do?columnId=${id}&needAll=0`
// return `http://m.music.migu.cn/migu/remoting/cms_list_tag?nid=${id}&pageSize=${this.limit}&pageNo=${page - 1}`
},
successCode: '000000',
@ -67,16 +69,12 @@ export default {
requestObj: null,
getBoardsData() {
if (this.requestBoardsObj) this._requestBoardsObj.cancelHttp()
this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/indexrank.do?templateVersion=8', {
this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/rank-list/release', {
// this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/indexrank.do?templateVersion=8', {
headers: {
sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
timestamp: 1578225871982,
appId: 'yyapp2',
mode: 'android',
ua: 'Android_migu',
version: '6.9.4',
osVersion: 'android 7.0',
'User-Agent': 'okhttp/3.9.1',
Referer: 'https://app.c.nf.migu.cn/',
'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36',
channel: '0146921',
},
})
return this.requestBoardsObj.promise
@ -98,13 +96,13 @@ export default {
// console.log(rawData)
let ids = new Set()
const list = []
rawData.forEach(item => {
rawData.forEach(({ objectInfo: item }) => {
if (ids.has(item.copyrightId)) return
ids.add(item.copyrightId)
const types = []
const _types = {}
item.rateFormats && item.rateFormats.forEach(type => {
item.newRateFormats && item.newRateFormats.forEach(type => {
let size
switch (type.formatType) {
case 'PQ':
@ -131,6 +129,8 @@ export default {
}
})
const intervalTest = /(\d\d:\d\d)$/.test(item.length)
list.push({
singer: this.getSinger(item.artists),
name: item.songName,
@ -140,7 +140,7 @@ export default {
songId: item.songId,
copyrightId: item.copyrightId,
source: 'mg',
interval: null,
interval: intervalTest ? RegExp.$1 : null,
img: item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : null,
lrc: null,
lrcUrl: item.lrcUrl,
@ -197,9 +197,9 @@ export default {
getList(bangid, page, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
return this.getData(this.getUrl(bangid, page)).then(({ statusCode, body }) => {
console.log(body)
// console.log(body)
if (statusCode !== 200 || body.code !== this.successCode) return this.getList(bangid, page, retryNum)
const list = this.filterData(body.data.columnInfo.dataList)
const list = this.filterData(body.columnInfo.contents)
return {
total: list.length,
list,

View File

@ -45,7 +45,7 @@ export default {
ids.add(item.id)
const types = []
const _types = {}
item.rateFormats && item.rateFormats.forEach(type => {
item.newRateFormats && item.newRateFormats.forEach(type => {
let size
switch (type.formatType) {
case 'PQ':

File diff suppressed because one or more lines are too long

View File

@ -77,6 +77,7 @@ export default {
if (this._requestObj) this._requestObj.cancelHttp()
const _requestObj = httpFetch('http://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg', {
method: 'POST',
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
},
@ -110,6 +111,7 @@ export default {
if (this._requestObj2) this._requestObj2.cancelHttp()
const _requestObj2 = httpFetch('http://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg', {
method: 'POST',
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
},
@ -134,6 +136,9 @@ export default {
source: 'tx',
comments: this.filterComment(comment.commentlist),
total: comment.commenttotal,
page,
limit,
maxPage: Math.ceil(comment.commenttotal / limit) || 1,
}
},
replaceEmoji(msg) {
@ -149,7 +154,8 @@ export default {
},
filterComment(rawList) {
return rawList.map(item => {
let time = item.rootcommentid ? parseInt(item.rootcommentid.substring(item.rootcommentid.lastIndexOf('_') + 1) + '000') : null
let time = parseInt(item.time + '000')
let timeStr = dateFormat2(time)
if (item.middlecommentcontent) {
let firstItem = item.middlecommentcontent[0]
firstItem.avatarurl = item.avatarurl
@ -159,23 +165,23 @@ export default {
item.middlecommentcontent.reverse()
}
return {
id: item.subcommentid,
id: `${item.rootcommentid}_${item.commentid}`,
rootId: item.rootcommentid,
text: item.rootcommentcontent ? this.replaceEmoji(item.rootcommentcontent).replace(/\\n/g, '\n').split('\n') : [],
time,
timeStr: time ? dateFormat2(time) : null,
time: item.rootcommentid == item.commentid ? time : null,
timeStr: item.rootcommentid == item.commentid ? timeStr : null,
userName: item.rootcommentnick ? item.rootcommentnick.substring(1) : '',
avatar: item.avatarurl,
userId: item.encrypt_rootcommentuin,
likedCount: item.praisenum,
reply: item.middlecommentcontent
? item.middlecommentcontent.map(c => {
let index = c.subcommentid.lastIndexOf('_')
// let index = c.subcommentid.lastIndexOf('_')
return {
id: c.subcommentid,
id: `sub_${item.rootcommentid}_${c.subcommentid}`,
text: this.replaceEmoji(c.subcommentcontent).replace(/\\n/g, '\n').split('\n'),
time: parseInt(c.subcommentid.substring(index + 1) + '000'),
timeStr: dateFormat2(parseInt(c.subcommentid.substring(index + 1) + '000')),
time: c.subcommentid == item.commentid ? time : null,
timeStr: c.subcommentid == item.commentid ? timeStr : null,
userName: c.replynick.substring(1),
avatar: c.avatarurl,
userId: c.encrypt_replyuin,

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