Merge branch 'dev'

pull/459/head v1.8.0
lyswhut 2021-03-07 12:38:11 +08:00
commit ba2b1dac92
91 changed files with 5512 additions and 4026 deletions

View File

@ -19,6 +19,7 @@
"plugins": [ "plugins": [
"@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-modules-umd", "@babel/plugin-transform-modules-umd",
"@babel/plugin-transform-runtime" "@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties"
] ]
} }

View File

@ -6,6 +6,43 @@ Project versioning adheres to [Semantic Versioning](http://semver.org/).
Commit convention is based on [Conventional Commits](http://conventionalcommits.org). Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
Change log format is based on [Keep a Changelog](http://keepachangelog.com/). Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
## [1.8.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.7.1...v1.8.0) - 2021-03-07
### 新增
- 新增设置-其他-列表缓存信息清理功能,注:此功能一般情况下不要使用
- 新增启动参数`-play`可以在启动软件时播放指定歌单使用方法看Readme.md的"启动参数"部分
- 新增逐字歌词播放,默认开启,可到设置界面关闭,注:本功能目前仅对酷狗源的歌曲有效
- 新增自定义源功能,源编写规则可以去常见问题查看
### 优化
- 允许播放除了搜索列表以外的所有歌曲,即原来没有播放按钮或者灰色的歌曲都可以去尝试点击播放。注:该功能的原理是尝试自动切换到其他源播放,所以不一定会播放成功,特别是对于那些独家的资源
- 优化单首歌曲的“添加到列表”弹窗歌曲列表状态的显示;现在在收藏单首歌曲时,若列表存在本歌曲则列表名字将变成灰色不可点击状态。总的来说,在添加单首歌曲时若列表名是灰色,则证明当前歌曲已在那个列表中
- 将歌词翻译放到原文的下方,同时新增当前播放翻译的高亮功能
### 移除
- 移除虾米源。注:虽然已移除该源,但仍可尝试去播放之前添加的歌曲,虽然不一定会成功
### 修复
- 修复音乐搜索列表的稍后播放功能无效的问题
- 修复搜索列表双击不支持播放的源时会导致切歌的问题
- 修复歌单列表加载失败时无法进入歌单打开界面的问题
- 修复mg源歌单列表无法加载的问题
- 修复kg跳转到官方歌曲详情页的歌曲无法播放的问题
- 修复我的列表的歌曲添加到其他列表时不排除当前列表的问题
- 修复在下载列表右击未下载完成的歌曲弹出的右击菜单中没有开始下载选项的问题
### 变更
- 歌词翻译显示功能修改为默认关闭,注:此变更仅影响首次安装软件的用户
### 其他
- 更新electron到v9.4.4
## [1.7.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.7.0...v1.7.1) - 2021-01-30 ## [1.7.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.7.0...v1.7.1) - 2021-01-30
### 修复 ### 修复

162
FAQ.md
View File

@ -171,3 +171,165 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看下面的
从`v0.17.0`起,由于加入了音频输出设备切换功能,该功能调用了 [MediaDevices.enumerateDevices()](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/enumerateDevices),可能导致安全软件提示洛雪要访问摄像头(目前发现卡巴斯基会提示),但实际上没有用到摄像头,并且摄像头的提示灯也不会亮,你可以选择阻止访问。 从`v0.17.0`起,由于加入了音频输出设备切换功能,该功能调用了 [MediaDevices.enumerateDevices()](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/enumerateDevices),可能导致安全软件提示洛雪要访问摄像头(目前发现卡巴斯基会提示),但实际上没有用到摄像头,并且摄像头的提示灯也不会亮,你可以选择阻止访问。
最后,若出现杀毒软件报毒、存在恶意行为,请自行判断选择是否继续使用本软件! 最后,若出现杀毒软件报毒、存在恶意行为,请自行判断选择是否继续使用本软件!
## 自定义源脚本编写说明
文件请使用UTF-8编码格式编写脚本所用编程语言为JavaScript可以使用ES6+语法,脚本与应用的交互是使用类似事件收发的方式进行,这是一个基本的脚本例子:
```js
/**
* @name 测试音乐源
* @description 我只是一个测试音乐源哦
* @version 1.0.0
* @author xxx
* @homepage http://xxx
*/
const { EVENT_NAMES, request, on, send } = window.lx
const qualitys = {
kw: {
'128k': '128',
'320k': '320',
flac: 'flac',
},
}
const httpRequest = (url, options) => new Promise((resolve, reject) => {
request(url, options, (err, resp) => {
if (err) return reject(err)
resolve(resp.body)
})
})
const apis = {
kw: {
musicUrl({ songmid }, quality) {
return httpRequest('http://xxx').then(data => {
return data.url
})
},
},
}
// 注册应用API请求事件
// source 音乐源可能的值取决于初始化时传入的sources对象的源key值
// info 请求附加信息内容根据action变化
// action 请求操作类型目前只有musicUrl即获取音乐URL链接
// 当action为musicUrl时info的结构{type, musicInfo}
// info.type音乐质量可能的值有128k / 320k / flac取决于初始化时对应源传入的qualitys值中的一个
// info.musicInfo音乐信息对象里面有音乐ID、名字等信息
on(EVENT_NAMES.request, ({ source, action, info }) => {
// 回调必须返回 Promise 对象
switch (action) {
// action 为 musicUrl 时需要在 Promise 返回歌曲 url
case 'musicUrl':
return apis[source].musicUrl(info.musicInfo, qualitys[source][info.type]).catch(err => {
console.log(err)
return Promise.reject(err)
})
}
})
// 脚本初始化完成后需要发送inited事件告知应用
send(EVENT_NAMES.inited, {
status: true, // 初始化成功 or 失败
openDevTools: false, // 是否打开开发者工具,方便用于调试脚本
sources: { // 当前脚本支持的源
kw: { // 支持的源对象可用key值kw/kg/tx/wy/mg
name: '酷我音乐',
type: 'music', // 目前固定为 music
actions: ['musicUrl'], // 目前固定为 ['musicUrl']
qualitys: ['128k', '320k', 'flac'], // 当前脚本的该源所支持获取的Url音质有效的值有['128k', '320k', 'flac']
},
},
})
```
### 自定义源信息
文件的开头必须包含以下注释:
```js
/**
* @name 测试脚本
* @description 我只是一个测试脚本
* @version 1.0.0
* @author xxx
* @homepage http://xxx
*/
```
- `@name `源的名字建议不要过长10个字符以内
- `@description `源的描述建议不要过长20个字符以内可不填不填时必须保留 @description
- `@version`:源的版本号,可不填,不填时可以删除 @version
- `@author `:脚本作者名字,可不填,不填时可以删除 @author
- `@homepage `:脚本主页,可不填,不填时可以删除 @homepage
### `window.lx`
应用为脚本暴露的API对象。
#### `window.lx.EVENT_NAMES`
常量事件名称对象,发送、注册事件时传入事件名时使用,可用值:
| 事件名 | 描述
| --- | ---
| `inited` | 脚本初始化完成后发送给应用的事件名,发送该事件时需要传入以下信息:`{status, sources, openDevTools}`<br>`status`:初始化结果(`true`成功,`false`失败)<br>`openDevTools`是否打开DevTools此选项可用于开发脚本时的调试<br>`sources`:支持的源信息对象,<br>`sources[kw/kg/tx/wy/mg].name`:源的名字(目前非必须)<br>`sources[kw/kg/tx/wy/mg].type`:源类型,目前固定值需为`music`<br>`sources[kw/kg/tx/wy/mg].actions`支持的actions由于目前只支持`musicUrl`,所以固定传`['musicUrl']`即可<br>`sources[kw/kg/tx/wy/mg].qualitys`:该源支持的音质列表,支持的值`['128k', '320k', 'flac']`,该字段用于控制应用可用的音质类型
| `request` | 应用API请求事件名回调入参`handler({ source, action, info})`,回调必须返回`Promise`对象<br>`source`:音乐源,可能的值取决于初始化时传入的`sources`对象的源key值<br>`info`:请求附加信息,内容根据`action`变化<br>`action`:请求操作类型,目前只有`musicUrl`即获取音乐URL链接需要在 Promise 返回歌曲 url`info`的结构:`{type, musicInfo}``info.type`:音乐质量,可能的值有`128k` / `320k` / `flac`(取决于初始化时对应源传入的`qualitys`值中的一个),`info.musicInfo`音乐信息对象里面有音乐ID、名字等信息
#### `window.lx.on`
事件注册方法,应用主动与脚本通信时使用:
```js
/**
* @param event_name 事件名
* @param handler 事件处理回调 -- 注意:注册的回调必须返回 Promise 对象
*/
window.lx.on(event_name, handler)
```
**注意:** 注册的回调必须返回 `Promise` 对象。
#### `window.lx.send`
事件发送方法,脚本主动与应用通信时使用:
```js
/**
* @param event_name 事件名
* @param datas 要传给应用的数据
*/
window.lx.send(event_name, datas)
```
#### `window.lx.request`
HTTP请求方法用于发送HTTP请求此HTTP请求方法不受跨域规则限制
```js
/**
* @param url 请求的URL
* @param options 请求选项,可用选项有 method / headers / body / form / formData / timeout
* @param callback 请求结果的回调 入参err, resp, body
* @return 返回一个方法调用此方法可以终止HTTP请求
*/
const cancelHttp = window.lx.request(url, options, callback)
```
#### `window.lx.utils`
应用提供给脚本的工具方法:
- `window.lx.utils.buffer.from`对应Node.js的 `Buffer.from`
- `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)`
- `window.lx.utils.crypto.rsaEncrypt`RSA加密 `rsaEncrypt(buffer, key)`
目前仅提供以上工具方法如果需要其他方法可以开issue讨论。

View File

@ -47,7 +47,7 @@
软件变化请查看:[更新日志](https://github.com/lyswhut/lx-music-desktop/blob/master/CHANGELOG.md)<br> 软件变化请查看:[更新日志](https://github.com/lyswhut/lx-music-desktop/blob/master/CHANGELOG.md)<br>
软件下载请转到:[发布页面](https://github.com/lyswhut/lx-music-desktop/releases)<br> 软件下载请转到:[发布页面](https://github.com/lyswhut/lx-music-desktop/releases)<br>
或者到网盘下载网盘内有MAC、windows版`https://www.lanzoux.com/b0bf2cfa/` 密码:`glqw`<br> 或者到网盘下载网盘内有MAC、windows版`https://www.lanzoux.com/b0bf2cfa/` 密码:`glqw`(若链接无法打开请百度:蓝奏云链接打不开)<br>
使用常见问题请转至:[常见问题](https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md) 使用常见问题请转至:[常见问题](https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md)
### 源码使用方法 ### 源码使用方法
@ -82,7 +82,13 @@ npm run pack:linux
- `-search` 启动软件时自动在搜索框搜索指定的内容,例如:`-search="突然的自我 - 伍佰"` - `-search` 启动软件时自动在搜索框搜索指定的内容,例如:`-search="突然的自我 - 伍佰"`
- `-dha` 禁用硬件加速启动Disable Hardware Acceleration窗口显示有问题时可以尝试添加此参数启动v1.6.0起新增) - `-dha` 禁用硬件加速启动Disable Hardware Acceleration窗口显示有问题时可以尝试添加此参数启动v1.6.0起新增)
- `-dt` 以非透明模式启动Disable Transparent对于未开启AERO效果的win7系统可加此参数启动以确保界面正常显示原来的`-nt`参数已重命名为`-dt`v1.6.0起重命名) - `-dt` 以非透明模式启动Disable Transparent对于未开启AERO效果的win7系统可加此参数启动以确保界面正常显示该参数对桌面歌词无效原来的`-nt`参数已重命名为`-dt`v1.6.0起重命名)
- `-play` 启动时播放指定列表的音乐,参数说明:
- `type`:播放类型,目前固定为`songList`
- `source`:播放源,可用值为`kw/kg/tx/wy/mg/myList`,其中`kw/kg/tx/wy/mg`对应各源的在线列表,`myList`为本地列表
- `link`要播放的在线列表歌单链接、或IDsource为`kw/kg/tx/wy/mg`之一(在线列表)时必传,举例:`./lx-music-desktop -play="type=songList&source=kw&link=歌单URL or ID"注意如果传入URL时必须对URL进行编码后再传入
- `name`要播放的本地列表歌单名字source为`myList`时必传,举例:`./lx-music-desktop -play="type=songList&source=myList&name=默认列表"
- `index`:从列表的哪个位置开始播放,选传,若不传默认播放第一首歌曲,举例:`./lx-music-desktop -play="type=songList&source=myList&name=默认列表&index=2"
### 常见问题 ### 常见问题

View File

@ -17,8 +17,8 @@ module.exports = merge(baseConfig, {
NODE_ENV: '"development"', NODE_ENV: '"development"',
}, },
__static: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`, __static: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`,
__userApi: `"${path.join(__dirname, '../../src/main/modules/userApi').replace(/\\/g, '\\\\')}"`,
}), }),
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin({ new FriendlyErrorsPlugin({
onErrors(severity, errors) { // Silent warning from electron-debug onErrors(severity, errors) { // Silent warning from electron-debug
if (severity != 'warning') return if (severity != 'warning') return

View File

@ -1,6 +1,7 @@
const path = require('path') const path = require('path')
const { merge } = require('webpack-merge') const { merge } = require('webpack-merge')
const webpack = require('webpack') const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const baseConfig = require('./webpack.config.base') const baseConfig = require('./webpack.config.base')
@ -20,6 +21,18 @@ module.exports = merge(baseConfig, {
__filename: false, __filename: false,
}, },
plugins: [ plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../../src/main/modules/userApi/renderer'),
to: path.join(__dirname, '../../dist/electron/userApi/renderer'),
},
{
from: path.join(__dirname, '../../src/main/modules/userApi/rendererEvent/name.js'),
to: path.join(__dirname, '../../dist/electron/userApi/rendererEvent/name.js'),
},
],
}),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {
NODE_ENV: '"production"', NODE_ENV: '"production"',

View File

@ -11,7 +11,6 @@ module.exports = merge(baseConfig, {
devtool: 'eval-source-map', devtool: 'eval-source-map',
plugins: [ plugins: [
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin(), new FriendlyErrorsPlugin(),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {

View File

@ -11,7 +11,6 @@ module.exports = merge(baseConfig, {
devtool: 'eval-source-map', devtool: 'eval-source-map',
plugins: [ plugins: [
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin(), new FriendlyErrorsPlugin(),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {

View File

@ -179,6 +179,9 @@ function startElectron() {
function electronLog(data, color) { function electronLog(data, color) {
let log = data.toString() let log = data.toString()
if (/[0-9A-z]+/.test(log)) { if (/[0-9A-z]+/.test(log)) {
// 抑制 user api 窗口使用 data url 加载页面时 vue扩展 的报错日志刷屏的问题
if (color == 'red' && typeof log === 'string' && log.includes('"Extension server error: Operation failed: Permission denied", source: devtools://devtools/bundled/extensions/extensions.js')) return
console.log(chalk[color](log)) console.log(chalk[color](log))
} }
} }

4527
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "lx-music-desktop", "name": "lx-music-desktop",
"version": "1.7.1", "version": "1.8.0",
"description": "一个免费的音乐查找助手", "description": "一个免费的音乐查找助手",
"main": "./dist/electron/main.js", "main": "./dist/electron/main.js",
"productName": "lx-music-desktop", "productName": "lx-music-desktop",
@ -34,18 +34,20 @@
"publish:gh:linux": "node build-config/pack.js && npm run publish:linux", "publish:gh:linux": "node build-config/pack.js && npm run publish:linux",
"publish:linux": "npm run publish:linux:deb && npm run publish:linux:appImage && npm run publish:linux:rpm && npm run publish:linux:pacman", "publish:linux": "npm run publish:linux:deb && npm run publish:linux:appImage && npm run publish:linux:rpm && npm run publish:linux:pacman",
"publish:linux:appImage": "cross-env ARCH=x64 electron-builder -l=AppImage -p onTagOrDraft", "publish:linux:appImage": "cross-env ARCH=x64 electron-builder -l=AppImage -p onTagOrDraft",
"publish:linux:deb": "npm run publish:linux:deb:x64 && npm run publish:linux:deb:x86 && npm run publish:linux:deb:arm64", "publish:linux:deb": "npm run publish:linux:deb:x64 && npm run publish:linux:deb:x86 && npm run publish:linux:deb:arm64 && npm run publish:linux:deb:armv7l",
"publish:linux:deb:x64": "cross-env ARCH=x64 electron-builder -l=deb --x64 -p onTagOrDraft", "publish:linux:deb:x64": "cross-env ARCH=x64 electron-builder -l=deb --x64 -p onTagOrDraft",
"publish:linux:deb:x86": "cross-env ARCH=x86 electron-builder -l=deb --ia32 -p onTagOrDraft", "publish:linux:deb:x86": "cross-env ARCH=x86 electron-builder -l=deb --ia32 -p onTagOrDraft",
"publish:linux:deb:arm64": "cross-env ARCH=arm64 electron-builder -l=deb --arm64 -p onTagOrDraft", "publish:linux:deb:arm64": "cross-env ARCH=arm64 electron-builder -l=deb --arm64 -p onTagOrDraft",
"publish:linux:deb:armv7l": "cross-env ARCH=armv7l electron-builder -l=deb --armv7l -p onTagOrDraft",
"publish:linux:rpm": "cross-env ARCH=x64 electron-builder -l=rpm --x64 -p onTagOrDraft", "publish:linux:rpm": "cross-env ARCH=x64 electron-builder -l=rpm --x64 -p onTagOrDraft",
"publish:linux:pacman": "cross-env ARCH=x64 electron-builder -l=pacman --x64 -p onTagOrDraft", "publish:linux:pacman": "cross-env ARCH=x64 electron-builder -l=pacman --x64 -p onTagOrDraft",
"pack:linux": "node build-config/pack.js && npm run pack:linux:deb && npm run pack:linux:appImage && npm run pack:linux:rpm && npm run pack:linux:pacman", "pack:linux": "node build-config/pack.js && npm run pack:linux:deb && npm run pack:linux:appImage && npm run pack:linux:rpm && npm run pack:linux:pacman",
"pack:linux:appImage": "cross-env ARCH=x64 electron-builder -l=AppImage", "pack:linux:appImage": "cross-env ARCH=x64 electron-builder -l=AppImage",
"pack:linux:deb": "npm run pack:linux:deb:x64 && npm run pack:linux:deb:x86 && npm run pack:linux:deb:arm64", "pack:linux:deb": "npm run pack:linux:deb:x64 && npm run pack:linux:deb:x86 && npm run pack:linux:deb:arm64 && npm run pack:linux:deb:armv7l",
"pack:linux:deb:x64": "cross-env ARCH=x64 electron-builder -l=deb --x64", "pack:linux:deb:x64": "cross-env ARCH=x64 electron-builder -l=deb --x64",
"pack:linux:deb:x86": "cross-env ARCH=x86 electron-builder -l=deb --ia32", "pack:linux:deb:x86": "cross-env ARCH=x86 electron-builder -l=deb --ia32",
"pack:linux:deb:arm64": "cross-env ARCH=arm64 electron-builder -l=deb --arm64", "pack:linux:deb:arm64": "cross-env ARCH=arm64 electron-builder -l=deb --arm64",
"pack:linux:deb:armv7l": "cross-env ARCH=armv7l electron-builder -l=deb --armv7l",
"pack:linux:rpm": "cross-env ARCH=x64 electron-builder -l=rpm --x64", "pack:linux:rpm": "cross-env ARCH=x64 electron-builder -l=rpm --x64",
"pack:linux:pacman": "cross-env ARCH=x64 electron-builder -l=pacman --x64", "pack:linux:pacman": "cross-env ARCH=x64 electron-builder -l=pacman --x64",
"pack:mac": "node build-config/pack.js && electron-builder -m=dmg", "pack:mac": "node build-config/pack.js && electron-builder -m=dmg",
@ -58,7 +60,8 @@
"build:renderer-lyric": "cross-env NODE_ENV=production webpack --config build-config/renderer-lyric/webpack.config.prod.js --progress --hide-modules", "build:renderer-lyric": "cross-env NODE_ENV=production webpack --config build-config/renderer-lyric/webpack.config.prod.js --progress --hide-modules",
"build": "npm run clean:electron && npm run build:main && npm run build:renderer && npm run build:renderer-lyric", "build": "npm run clean:electron && npm run build:main && npm run build:renderer && npm run build:renderer-lyric",
"lint": "eslint --ext .js,.vue -f ./node_modules/eslint-formatter-friendly src", "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-formatter-friendly src",
"lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-formatter-friendly --fix src" "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-formatter-friendly --fix src",
"up": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://localhost:1081 npm update"
}, },
"browserslist": [ "browserslist": [
"Electron 9.3.4" "Electron 9.3.4"
@ -160,12 +163,13 @@
}, },
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme", "homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.10", "@babel/core": "^7.13.8",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-umd": "^7.12.1", "@babel/plugin-transform-modules-umd": "^7.13.0",
"@babel/plugin-transform-runtime": "^7.12.10", "@babel/plugin-transform-runtime": "^7.13.9",
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.12.11", "@babel/preset-env": "^7.13.9",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"babel-minify-webpack-plugin": "^0.3.1", "babel-minify-webpack-plugin": "^0.3.1",
@ -174,26 +178,26 @@
"chalk": "^4.1.0", "chalk": "^4.1.0",
"changelog-parser": "^2.8.0", "changelog-parser": "^2.8.0",
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"core-js": "^3.8.3", "core-js": "^3.9.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^4.3.0", "css-loader": "^4.3.0",
"del": "^6.0.0", "del": "^6.0.0",
"electron": "^9.4.2", "electron": "^9.4.4",
"electron-builder": "^22.9.1", "electron-builder": "^22.10.5",
"electron-debug": "^3.2.0", "electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"eslint": "^7.18.0", "eslint": "^7.21.0",
"eslint-config-standard": "^14.1.1", "eslint-config-standard": "^14.1.1",
"eslint-formatter-friendly": "^7.0.0", "eslint-formatter-friendly": "^7.0.0",
"eslint-loader": "^4.0.2", "eslint-loader": "^4.0.2",
"eslint-plugin-html": "^6.1.1", "eslint-plugin-html": "^6.1.1",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^4.1.0", "eslint-plugin-standard": "^4.1.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "^1.7.0", "friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^4.5.1", "html-webpack-plugin": "^4.5.2",
"less": "^3.13.1", "less": "^3.13.1",
"less-loader": "^7.3.0", "less-loader": "^7.3.0",
"markdown-it": "^12.0.4", "markdown-it": "^12.0.4",
@ -201,7 +205,7 @@
"optimize-css-assets-webpack-plugin": "^5.0.4", "optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-loader": "^4.2.0", "postcss-loader": "^4.2.0",
"postcss-pxtorem": "^5.1.1", "postcss-pxtorem": "^5.1.1",
"pug": "^3.0.0", "pug": "^3.0.2",
"pug-loader": "^2.4.0", "pug-loader": "^2.4.0",
"pug-plain-loader": "^1.1.0", "pug-plain-loader": "^1.1.0",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
@ -222,18 +226,18 @@
"dependencies": { "dependencies": {
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"dnscache": "^1.0.2", "dnscache": "^1.0.2",
"electron-log": "^4.3.1", "electron-log": "^4.3.2",
"electron-store": "^6.0.1", "electron-store": "^6.0.1",
"electron-updater": "^4.3.5", "electron-updater": "^4.3.8",
"iconv-lite": "^0.6.2", "iconv-lite": "^0.6.2",
"image-size": "^0.9.3", "image-size": "^0.9.4",
"js-htmlencode": "^0.3.0", "js-htmlencode": "^0.3.0",
"lrc-file-parser": "^1.0.5", "lrc-file-parser": "^1.0.7",
"needle": "^2.6.0", "needle": "^2.6.0",
"node-id3": "^0.2.2", "node-id3": "^0.2.2",
"request": "^2.88.2", "request": "^2.88.2",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-i18n": "^8.22.4", "vue-i18n": "^8.23.0",
"vue-router": "^3.5.1", "vue-router": "^3.5.1",
"vuex": "^3.6.2", "vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0" "vuex-router-sync": "^5.0.0"

View File

@ -1,4 +1,34 @@
### 新增
- 新增设置-其他-列表缓存信息清理功能,注:此功能一般情况下不要使用
- 新增启动参数`-play`可以在启动软件时播放指定歌单使用方法看Readme.md的"启动参数"部分
- 新增逐字歌词播放,默认开启,可到设置界面关闭,注:本功能目前仅对酷狗源的歌曲有效
- 新增自定义源功能,源编写规则可以去常见问题查看
### 优化
- 允许播放除了搜索列表以外的所有歌曲,即原来没有播放按钮或者灰色的歌曲都可以去尝试点击播放。注:该功能的原理是尝试自动切换到其他源播放,所以不一定会播放成功,特别是对于那些独家的资源
- 优化单首歌曲的“添加到列表”弹窗歌曲列表状态的显示;现在在收藏单首歌曲时,若列表存在本歌曲则列表名字将变成灰色不可点击状态。总的来说,在添加单首歌曲时若列表名是灰色,则证明当前歌曲已在那个列表中
- 将歌词翻译放到原文的下方,同时新增当前播放翻译的高亮功能
### 移除
- 移除虾米源。注:虽然已移除该源,但仍可尝试去播放之前添加的歌曲,虽然不一定会成功
### 修复 ### 修复
- 修复非透明模式下右侧滚动条无法拖动的问题 - 修复音乐搜索列表的稍后播放功能无效的问题
- 修复MAC下xm音乐滑块验证问题 - 修复搜索列表双击不支持播放的源时会导致切歌的问题
- 修复歌单列表加载失败时无法进入歌单打开界面的问题
- 修复mg源歌单列表无法加载的问题
- 修复kg跳转到官方歌曲详情页的歌曲无法播放的问题
- 修复我的列表的歌曲添加到其他列表时不排除当前列表的问题
- 修复在下载列表右击未下载完成的歌曲弹出的右击菜单中没有开始下载选项的问题
### 变更
- 歌词翻译显示功能修改为默认关闭,注:此变更仅影响首次安装软件的用户
### 其他
- 更新electron到v9.4.4

File diff suppressed because one or more lines are too long

View File

@ -51,6 +51,6 @@ module.exports = {
}, },
], ],
navigationUrlWhiteList: [ navigationUrlWhiteList: [
/^https:\/\/www\.xiami\.com/,
], ],
} }

View File

@ -2,7 +2,7 @@ const path = require('path')
const os = require('os') const os = require('os')
const defaultSetting = { const defaultSetting = {
version: '1.0.39', version: '1.0.40',
player: { player: {
togglePlayMethod: 'listLoop', togglePlayMethod: 'listLoop',
highQuality: false, highQuality: false,
@ -11,7 +11,8 @@ const defaultSetting = {
isMute: false, isMute: false,
mediaDeviceId: 'default', mediaDeviceId: 'default',
isMediaDeviceRemovedStopPlay: false, isMediaDeviceRemovedStopPlay: false,
isShowLyricTransition: true, isShowLyricTransition: false,
isPlayLxlrc: true,
isSavePlayTime: false, isSavePlayTime: false,
}, },
desktopLyric: { desktopLyric: {

View File

@ -2,11 +2,17 @@ const { ipcMain, ipcRenderer } = require('electron')
const names = require('./ipcNames') const names = require('./ipcNames')
exports.mainOn = (event, callback) => { exports.mainOn = (name, callback) => {
ipcMain.on(event, callback) ipcMain.on(name, callback)
} }
exports.mainOnce = (event, callback) => { exports.mainOnce = (name, callback) => {
ipcMain.once(event, callback) ipcMain.once(name, callback)
}
exports.mainOff = (name, callback) => {
ipcMain.removeListener(name, callback)
}
exports.mainOffAll = name => {
ipcMain.removeAllListeners(name)
} }
exports.mainHandle = (name, callback) => { exports.mainHandle = (name, callback) => {
@ -15,6 +21,9 @@ exports.mainHandle = (name, callback) => {
exports.mainHandleOnce = (name, callback) => { exports.mainHandleOnce = (name, callback) => {
ipcMain.handleOnce(name, callback) ipcMain.handleOnce(name, callback)
} }
exports.mainHandleRemove = name => {
ipcMain.removeListener(name)
}
exports.mainSend = (window, name, params) => { exports.mainSend = (window, name, params) => {
window.webContents.send(name, params) window.webContents.send(name, params)
@ -33,5 +42,11 @@ exports.rendererOn = (name, callback) => {
exports.rendererOnce = (name, callback) => { exports.rendererOnce = (name, callback) => {
ipcRenderer.once(name, callback) ipcRenderer.once(name, callback)
} }
exports.rendererOff = (name, callback) => {
ipcRenderer.removeListener(name, callback)
}
exports.rendererOffAll = name => {
ipcRenderer.removeAllListeners(name)
}
exports.NAMES = names exports.NAMES = names

View File

@ -50,6 +50,15 @@ const names = {
get_data: 'get_data', get_data: 'get_data',
save_data: 'save_data', save_data: 'save_data',
get_hot_key: 'get_hot_key', get_hot_key: 'get_hot_key',
import_user_api: 'import_user_api',
remove_user_api: 'remove_user_api',
set_user_api: 'set_user_api',
get_user_api_list: 'get_user_api_list',
request_user_api: 'request_user_api',
request_user_api_cancel: 'request_user_api_cancel',
get_user_api_status: 'get_user_api_status',
user_api_status: 'user_api_status',
}, },
winLyric: { winLyric: {
close: 'close', close: 'close',

View File

@ -1,7 +1,7 @@
const log = require('electron-log') const log = require('electron-log')
const Store = require('electron-store') const Store = require('electron-store')
const { defaultSetting, overwriteSetting } = require('./defaultSetting') const { defaultSetting, overwriteSetting } = require('./defaultSetting')
const apiSource = require('../renderer/utils/music/api-source-info') // const apiSource = require('../renderer/utils/music/api-source-info')
const defaultHotKey = require('./defaultHotKey') const defaultHotKey = require('./defaultHotKey')
const { dialog, app } = require('electron') const { dialog, app } = require('electron')
const path = require('path') const path = require('path')
@ -115,10 +115,10 @@ exports.mergeSetting = (setting, version) => {
setting = defaultSettingCopy setting = defaultSettingCopy
} }
if (!apiSource.some(api => api.id === setting.apiSource && !api.disabled)) { // if (!apiSource.some(api => api.id === setting.apiSource && !api.disabled)) {
let api = apiSource.find(api => !api.disabled) // let api = apiSource.find(api => !api.disabled)
if (api) setting.apiSource = api.id // if (api) setting.apiSource = api.id
} // }
return { setting, version: defaultVersion } return { setting, version: defaultVersion }
} }
@ -150,6 +150,9 @@ exports.initSetting = () => {
const electronStore_config = new Store({ const electronStore_config = new Store({
name: 'config', name: 'config',
}) })
const electronStore_downloadList = new Store({
name: 'downloadList',
})
let setting = electronStore_config.get('setting') let setting = electronStore_config.get('setting')
if (setting) { if (setting) {
let version = electronStore_config.get('version') let version = electronStore_config.get('version')
@ -165,7 +168,7 @@ exports.initSetting = () => {
} }
const downloadList = electronStore_config.get('download') const downloadList = electronStore_config.get('download')
if (downloadList) { if (downloadList) {
if (downloadList.list) electronStore_list.set('downloadList', downloadList.list) if (downloadList.list) electronStore_downloadList.set('list', downloadList.list)
electronStore_config.delete('download') electronStore_config.delete('download')
} }
} }
@ -181,6 +184,13 @@ exports.initSetting = () => {
} }
} }
// 从我的列表分离下载列表 v1.7.0 后
let downloadList = electronStore_list.get('downloadList')
if (downloadList) {
electronStore_downloadList.set('list', downloadList)
electronStore_list.delete('downloadList')
}
const { version: settingVersion, setting: newSetting } = exports.mergeSetting(setting, electronStore_config.get('version')) const { version: settingVersion, setting: newSetting } = exports.mergeSetting(setting, electronStore_config.get('version'))
// 重置 ^0.18.2 排行榜ID // 重置 ^0.18.2 排行榜ID

View File

@ -6,8 +6,12 @@ const Tray = require('./Tray')
const WinLyric = require('./WinLyric') const WinLyric = require('./WinLyric')
const HotKey = require('./HotKey') const HotKey = require('./HotKey')
const { Event: UserApi } = require('../modules/userApi')
if (!global.lx_event.common) global.lx_event.common = new Common() if (!global.lx_event.common) global.lx_event.common = new Common()
if (!global.lx_event.mainWindow) global.lx_event.mainWindow = new MainWindow() if (!global.lx_event.mainWindow) global.lx_event.mainWindow = new MainWindow()
if (!global.lx_event.tray) global.lx_event.tray = new Tray() if (!global.lx_event.tray) global.lx_event.tray = new Tray()
if (!global.lx_event.winLyric) global.lx_event.winLyric = new WinLyric() if (!global.lx_event.winLyric) global.lx_event.winLyric = new WinLyric()
if (!global.lx_event.hotKey) global.lx_event.hotKey = new HotKey() if (!global.lx_event.hotKey) global.lx_event.hotKey = new HotKey()
if (!global.lx_event.userApi) global.lx_event.userApi = new UserApi()

View File

@ -2,3 +2,4 @@ require('./appMenu')
require('./winLyric') require('./winLyric')
require('./tray') require('./tray')
require('./hotKey') require('./hotKey')
require('./userApi')

View File

@ -0,0 +1,2 @@
exports.userApis = []

View File

@ -0,0 +1,11 @@
const { EventEmitter } = require('events')
const USER_API_EVENT_NAME = require('./name')
class UserApi extends EventEmitter {
status(info) {
this.emit(USER_API_EVENT_NAME.status, info)
}
}
module.exports = UserApi

View File

@ -0,0 +1,3 @@
module.exports = {
status: 'status',
}

View File

@ -0,0 +1,44 @@
const Event = require('./event/event')
const eventNames = require('./event/name')
const { closeWindow } = require('./main')
const { getUserApis, importApi, removeApi } = require('./utils')
const { request, cancelRequest, getStatus, loadApi } = require('./rendererEvent/rendererEvent')
// const { getApiList, importApi, removeApi, setApi, getStatus, request, eventNames }
let userApiId
exports.Event = Event
exports.eventNames = eventNames
exports.getApiList = getUserApis
exports.importApi = script => {
return {
apiInfo: importApi(script),
apiList: getUserApis(),
}
}
exports.request = request
exports.cancelRequest = cancelRequest
exports.getStatus = getStatus
exports.removeApi = async ids => {
if (userApiId && ids.includes(userApiId)) {
userApiId = null
await closeWindow()
}
removeApi(ids)
return getUserApis()
}
exports.setApi = async id => {
if (userApiId) {
userApiId = null
await closeWindow()
}
const apiList = getUserApis()
if (!apiList.some(a => a.id === id)) return
userApiId = id
await loadApi(id)
}

View File

@ -0,0 +1,88 @@
const { BrowserWindow } = require('electron')
const fs = require('fs')
const path = require('path')
// eslint-disable-next-line no-undef
const dir = global.isDev ? __userApi : path.join(__dirname, 'userApi')
const wait = time => new Promise(resolve => setTimeout(() => resolve(), time))
let html = ''
fs.readFile(path.join(dir, 'renderer/user-api.html'), 'utf8', (err, data) => {
if (err) throw new Error('api html read failed, info: ' + err.message)
html = data
})
const denyEvents = [
'new-window',
'will-navigate',
'will-redirect',
'will-attach-webview',
'will-prevent-unload',
'media-started-playing',
]
const winEvent = win => {
win.on('closed', () => {
win = global.modules.userApiWindow = null
})
}
exports.createWindow = async userApi => {
if (global.modules.userApiWindow) return
while (true) {
if (html) break
await wait(100)
}
/**
* Initial window options
*/
global.modules.userApiWindow = new BrowserWindow({
enableRemoteModule: false,
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
show: false,
webPreferences: {
contextIsolation: true,
worldSafeExecuteJavaScript: true,
nodeIntegration: false,
preload: path.join(dir, 'renderer/preload.js'),
},
})
for (const eventName of denyEvents) {
global.modules.userApiWindow.webContents.on(eventName, event => {
event.preventDefault()
})
}
global.modules.userApiWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
callback(false)
})
winEvent(global.modules.userApiWindow)
// console.log(html.replace('</body>', `<script>${userApi.script}</script></body>`))
const randomNum = Math.random().toString().substring(2, 10)
global.modules.userApiWindow.loadURL(
'data:text/html;charset=UTF-8,' + encodeURIComponent(html
.replace('<meta http-equiv="Content-Security-Policy" content="default-src \'none\'">',
`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${randomNum}';">`)
.replace('</body>', `<script nonce="${randomNum}">${userApi.script}</script></body>`)))
// global.modules.userApiWindow.loadFile(path.join(dir, 'renderer/user-api.html'))
// global.modules.userApiWindow.webContents.openDevTools()
}
exports.closeWindow = async() => {
if (!global.modules.userApiWindow) return
await Promise.all([
global.modules.userApiWindow.webContents.session.clearAuthCache(),
global.modules.userApiWindow.webContents.session.clearStorageData(),
global.modules.userApiWindow.webContents.session.clearCache(),
])
global.modules.userApiWindow.destroy()
global.modules.userApiWindow = null
}

View File

@ -0,0 +1,227 @@
const { contextBridge, ipcRenderer } = require('electron')
const needle = require('needle')
const { createCipheriv, publicEncrypt, constants, randomBytes, createHash } = require('crypto')
const USER_API_RENDERER_EVENT_NAME = require('../rendererEvent/name')
const sendMessage = (action, status, data, message) => {
ipcRenderer.send(action, { status, data, message })
}
let isInitedApi = false
const EVENT_NAMES = {
request: 'request',
inited: 'inited',
}
const eventNames = Object.values(EVENT_NAMES)
const events = {
request: null,
}
const allSources = ['kw', 'kg', 'tx', 'wy', 'mg']
const supportQualitys = {
kw: ['128k', '320k', 'flac'],
kg: ['128k', '320k', 'flac'],
tx: ['128k', '320k', 'flac'],
wy: ['128k', '320k', 'flac'],
mg: ['128k', '320k', 'flac'],
}
const supportActions = {
kw: ['musicUrl'],
kg: ['musicUrl'],
tx: ['musicUrl'],
wy: ['musicUrl'],
mg: ['musicUrl'],
xm: ['musicUrl'],
}
const handleRequest = (context, { requestKey, data }) => {
// console.log(data)
if (!events.request) return sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, 'Request event is not defined')
try {
events.request.call(context, { source: data.source, action: data.action, info: data.info }).then(response => {
let sendData = {
requestKey,
}
switch (data.action) {
case 'musicUrl':
sendData.result = {
source: data.source,
action: data.action,
data: {
type: data.info.type,
url: response,
},
}
break
}
sendMessage(USER_API_RENDERER_EVENT_NAME.response, true, sendData)
}).catch(err => {
sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, err.message)
})
} catch (err) {
sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, err.message)
}
}
/**
*
* @param {*} context
* @param {*} info {
* status: true,
* message: 'xxx',
* sources: {
* kw: ['128k', '320k', 'flac'],
* kg: ['128k', '320k', 'flac'],
* tx: ['128k', '320k', 'flac'],
* wy: ['128k', '320k', 'flac'],
* mg: ['128k', '320k', 'flac'],
* }
* }
*/
const handleInit = (context, info) => {
if (!info) {
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, 'Init failed')
// sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
return
}
if (info.openDevTools === true) {
sendMessage(USER_API_RENDERER_EVENT_NAME.openDevTools)
}
if (!info.status) {
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, 'Init failed')
// sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
return
}
const sourceInfo = {
sources: {},
}
try {
for (const source of allSources) {
const userSource = info.sources[source]
if (!userSource || userSource.type !== 'music') continue
const qualitys = supportQualitys[source]
const actions = supportActions[source]
sourceInfo.sources[source] = {
type: 'music',
actions: actions.filter(a => userSource.actions.includes(a)),
qualitys: qualitys.filter(q => userSource.qualitys.includes(q)),
}
}
} catch (error) {
console.log(error)
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, error.message)
return
}
sendMessage(USER_API_RENDERER_EVENT_NAME.init, true, sourceInfo)
ipcRenderer.on(USER_API_RENDERER_EVENT_NAME.request, (event, data) => {
handleRequest(context, data)
})
}
contextBridge.exposeInMainWorld('lx', {
EVENT_NAMES,
request(url, { method = 'get', timeout, headers, body, form, formData }, callback) {
let options = { headers }
let data
if (body) {
data = body
} else if (form) {
data = form
// data.content_type = 'application/x-www-form-urlencoded'
options.json = false
} else if (formData) {
data = formData
// data.content_type = 'multipart/form-data'
options.json = false
}
options.response_timeout = timeout
let request = needle.request(method, url, data, options, (err, resp, body) => {
if (!err) {
body = resp.body = resp.raw.toString()
try {
resp.body = JSON.parse(resp.body)
} catch (_) {}
body = resp.body
}
callback(err, {
statusCode: resp.statusCode,
statusMessage: resp.statusMessage,
headers: resp.headers,
bytes: resp.bytes,
raw: resp.raw,
body: body,
}, body)
}).request
return () => {
if (!request.aborted) request.abort()
request = null
}
},
send(eventName, data) {
return new Promise((resolve, reject) => {
if (!eventNames.includes(eventName)) return reject(new Error('The event is not supported: ' + eventName))
switch (eventName) {
case EVENT_NAMES.inited:
if (isInitedApi) return
isInitedApi = true
handleInit(this, data)
break
default:
resolve(new Error('Unknown event name: ' + eventName))
}
})
},
on(eventName, handler) {
if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
switch (eventName) {
case EVENT_NAMES.request:
events.request = handler
break
}
},
utils: {
crypto: {
aesEncrypt(buffer, mode, key, iv) {
const cipher = createCipheriv('aes-128-' + mode, key, iv)
return Buffer.concat([cipher.update(buffer), cipher.final()])
},
rsaEncrypt(buffer, key) {
buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer])
return publicEncrypt({ key: key, padding: constants.RSA_NO_PADDING }, buffer)
},
randomBytes(size) {
return randomBytes(size)
},
md5(str) {
return createHash('md5').update(str).digest('hex')
},
},
buffer: {
from(...args) {
return Buffer.from(...args)
},
},
},
// removeEvent(eventName, handler) {
// if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
// let handlers
// switch (eventName) {
// case EVENT_NAMES.request:
// handlers = events.request
// break
// }
// for (let index = 0; index < handlers.length; index++) {
// if (handlers[index] === handler) {
// handlers.splice(index, 1)
// break
// }
// }
// },
// removeAllEvents() {
// for (const handlers of Object.values(events)) {
// handlers.splice(0, handlers.length)
// }
// },
})

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,12 @@
const names = {
init: '',
request: '',
response: '',
openDevTools: '',
}
for (const key of Object.keys(names)) {
names[key] = `userApi_${key}`
}
module.exports = names

View File

@ -0,0 +1,82 @@
const { mainOn, mainSend } = require('@common/ipc')
const USER_API_RENDERER_EVENT_NAME = require('../rendererEvent/name')
const { createWindow } = require('../main')
const { getUserApis } = require('../utils')
let userApi
let status = { status: true }
const requestQueue = new Map()
const timeouts = {}
const handleInit = (event, { status, message, data: apiInfo }) => {
// console.log('inited')
if (!status) {
console.log('init failed:', message)
global.lx_event.userApi.status(status = { status: false, apiInfo: userApi, message })
return
}
global.lx_event.userApi.status(status = { status: true, apiInfo: { ...userApi, sources: apiInfo.sources } })
}
const handleResponse = (event, { status, data: { requestKey, result }, message }) => {
const request = requestQueue.get(requestKey)
if (!request) return
requestQueue.delete(requestKey)
clearTimeout(timeouts[requestKey])
delete timeouts[requestKey]
if (status) {
request[0](result)
} else {
request[1](new Error(message))
}
}
const handleOpenDevTools = () => {
if (global.modules.userApiWindow) {
global.modules.userApiWindow.webContents.openDevTools()
}
}
mainOn(USER_API_RENDERER_EVENT_NAME.init, handleInit)
mainOn(USER_API_RENDERER_EVENT_NAME.response, handleResponse)
mainOn(USER_API_RENDERER_EVENT_NAME.openDevTools, handleOpenDevTools)
exports.loadApi = async apiId => {
if (!apiId) return global.lx_event.userApi.status(status = { status: false, message: 'api id is null' })
userApi = getUserApis().find(api => api.id == apiId)
console.log('load api', userApi.name)
await createWindow(userApi)
// if (!userApi) return global.lx_event.userApi.status(status = { status: false, message: 'api script is not found' })
// if (!global.modules.userApiWindow) {
// global.lx_event.userApi.status(status = { status: false, message: 'user api runtime is not defined' })
// throw new Error('user api window is not defined')
// }
// // const path = require('path')
// // // eslint-disable-next-line no-undef
// // userApi.script = require('fs').readFileSync(path.join(global.isDev ? __userApi : __dirname, 'renderer/test-api.js')).toString()
// console.log('load api', userApi.name)
// mainSend(global.modules.userApiWindow, USER_API_RENDERER_EVENT_NAME.init, { userApi })
}
exports.cancelRequest = requestKey => {
if (!requestQueue.has(requestKey)) return
const request = requestQueue.get(requestKey)
request[1](new Error('Cancel request'))
requestQueue.delete(requestKey)
clearTimeout(timeouts[requestKey])
delete timeouts[requestKey]
}
exports.request = ({ requestKey, data }) => new Promise((resolve, reject) => {
if (!userApi) return reject(new Error('user api is not load'))
// const requestKey = `request__${Math.random().toString().substring(2)}`
timeouts[requestKey] = setTimeout(() => {
exports.cancelRequest(requestKey)
}, 20000)
requestQueue.set(requestKey, [resolve, reject, data])
mainSend(global.modules.userApiWindow, USER_API_RENDERER_EVENT_NAME.request, { requestKey, data })
})
exports.getStatus = () => status

View File

@ -0,0 +1,46 @@
const { userApis: defaultUserApis } = require('../config')
const Store = require('electron-store')
let userApis
const electronStore_userApi = new Store({
name: 'userApi',
})
exports.getUserApis = () => {
if (userApis) return userApis
userApis = electronStore_userApi.get('userApis')
if (!userApis) {
userApis = defaultUserApis
electronStore_userApi.set('userApis', userApis)
}
return userApis
}
exports.importApi = script => {
let scriptInfo = script.split(/\r?\n/)
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) + '...'
description = description.startsWith(' * @description ') ? description.replace(' * @description ', '').trim() : ''
if (description.length > 20) description = description.substring(0, 20) + '...'
const apiInfo = {
id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`,
name,
description,
script,
}
userApis.push(apiInfo)
electronStore_userApi.set('userApis', userApis)
return apiInfo
}
exports.removeApi = ids => {
for (let index = userApis.length - 1; index > -1; index--) {
if (ids.includes(userApis[index].id)) {
userApis.splice(index, 1)
ids.splice(index, 1)
}
}
electronStore_userApi.set('userApis', userApis)
}

View File

@ -14,6 +14,7 @@ const setLrcConfig = () => {
config: desktopLyric, config: desktopLyric,
languageId: global.appSetting.langId, languageId: global.appSetting.langId,
isShowLyricTransition: global.appSetting.player.isShowLyricTransition, isShowLyricTransition: global.appSetting.player.isShowLyricTransition,
isPlayLxlrc: global.appSetting.player.isPlayLxlrc,
}) })
if (isLock != desktopLyric.isLock) { if (isLock != desktopLyric.isLock) {
isLock = desktopLyric.isLock isLock = desktopLyric.isLock

View File

@ -24,7 +24,12 @@ mainOn(ipcWinLyricNames.set_lyric_config, (event, config) => {
}) })
mainHandle(ipcWinLyricNames.get_lyric_config, async() => { mainHandle(ipcWinLyricNames.get_lyric_config, async() => {
return { config: global.appSetting.desktopLyric, languageId: global.appSetting.langId, isShowLyricTransition: global.appSetting.player.isShowLyricTransition } return {
config: global.appSetting.desktopLyric,
languageId: global.appSetting.langId,
isShowLyricTransition: global.appSetting.player.isShowLyricTransition,
isPlayLxlrc: global.appSetting.player.isPlayLxlrc,
}
}) })
mainOn(ipcWinLyricNames.set_win_bounds, (event, options) => { mainOn(ipcWinLyricNames.set_win_bounds, (event, options) => {

View File

@ -18,6 +18,6 @@ require('./showDialog')
require('./playList') require('./playList')
require('./data') require('./data')
require('./xm_verify')
require('./kw_decodeLyric') require('./kw_decodeLyric')
require('./userApi')

View File

@ -3,6 +3,7 @@ const { mainOn, NAMES: { mainWindow: ipcMainWindowNames }, mainHandle } = requir
let electronStore_list let electronStore_list
let electronStore_downloadList
mainHandle(ipcMainWindowNames.get_playlist, async(event, isIgnoredError = false) => { mainHandle(ipcMainWindowNames.get_playlist, async(event, isIgnoredError = false) => {
if (!electronStore_list) { if (!electronStore_list) {
@ -11,13 +12,35 @@ mainHandle(ipcMainWindowNames.get_playlist, async(event, isIgnoredError = false)
clearInvalidConfig: !isIgnoredError, clearInvalidConfig: !isIgnoredError,
}) })
} }
if (!electronStore_downloadList) {
electronStore_downloadList = new Store({
name: 'downloadList',
})
}
return { return {
defaultList: electronStore_list.get('defaultList'), defaultList: electronStore_list.get('defaultList'),
loveList: electronStore_list.get('loveList'), loveList: electronStore_list.get('loveList'),
userList: electronStore_list.get('userList'), userList: electronStore_list.get('userList'),
downloadList: electronStore_list.get('downloadList'), downloadList: electronStore_downloadList.get('list'),
} }
}) })
mainOn(ipcMainWindowNames.save_playlist, (event, { type, data }) => electronStore_list && electronStore_list.set(type, data)) const handleSaveList = ({ defaultList, loveList, userList }) => {
if (!electronStore_list) return
let data = {}
if (defaultList != null) data.defaultList = defaultList
if (loveList != null) data.loveList = loveList
if (userList != null) data.userList = userList
electronStore_list.set(data)
}
mainOn(ipcMainWindowNames.save_playlist, (event, { type, data }) => {
switch (type) {
case 'myList':
handleSaveList(data)
break
case 'downloadList':
electronStore_downloadList && electronStore_downloadList.set('list', data)
break
}
})

View File

@ -0,0 +1,35 @@
const { mainSend, mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('@common/ipc')
const { getApiList, importApi, removeApi, setApi, getStatus, request, cancelRequest, eventNames } = require('../modules/userApi')
const handleStatusChange = status => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.user_api_status, status)
}
global.lx_event.userApi.on(eventNames.status, handleStatusChange)
mainHandle(ipcMainWindowNames.import_user_api, async(event, script) => {
return importApi(script)
})
mainHandle(ipcMainWindowNames.remove_user_api, (event, apiIds) => {
return removeApi(apiIds)
})
mainHandle(ipcMainWindowNames.set_user_api, (event, apiId) => {
return setApi(apiId)
})
mainHandle(ipcMainWindowNames.get_user_api_list, event => {
return getApiList()
})
mainHandle(ipcMainWindowNames.get_user_api_status, event => {
return getStatus()
})
mainHandle(ipcMainWindowNames.request_user_api, (event, musicInfo) => {
return request(musicInfo)
})
mainHandle(ipcMainWindowNames.request_user_api_cancel, (event, requestKey) => {
return cancelRequest(requestKey)
})

View File

@ -1,4 +0,0 @@
const { isMac } = require('../../../common/utils')
// mac下的 BrowserView 无法拖动验证栏,改用 BrowserWindow
require(isMac ? './xm_verify_win' : './xm_verify_view')

View File

@ -1,4 +0,0 @@
const { isMac } = require('../../../common/utils')
// mac下的 BrowserView 无法拖动验证栏,改用 BrowserWindow
require(isMac ? './xm_verify_win' : './xm_verify_view')

View File

@ -1,79 +0,0 @@
const { BrowserView } = require('electron')
const { mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../../common/ipc')
const { getWindowSizeInfo } = require('../../utils')
let view
let isActioned = false
let rejectFn
const closeView = async() => {
if (!view) return
// await view.webContents.session.clearCache()
if (global.modules.mainWindow) global.modules.mainWindow.removeBrowserView(view)
await view.webContents.session.clearStorageData()
view.destroy()
view = null
}
mainHandle(ipcMainWindowNames.handle_xm_verify_open, (event, url) => new Promise((resolve, reject) => {
if (!global.modules.mainWindow) return reject(new Error('mainWindow is undefined'))
if (view) {
global.modules.mainWindow.removeBrowserView(view)
view.destroy()
}
rejectFn = reject
isActioned = false
view = new BrowserView({
webPreferences: {
enableRemoteModule: false,
disableHtmlFullscreenWindowResize: true,
},
})
// view.webContents.on('did-finish-load', () => {
// if (/punish\?/.test(view.webContents.getURL())) return
// let ses = view.webContents.session
// ses.cookies.get({ name: 'x5sec' })
// .then(async([x5sec]) => {
// isActioned = true
// await closeView()
// if (!x5sec) return reject(new Error('get x5sec failed'))
// resolve(x5sec.value)
// }).catch(async err => {
// isActioned = true
// await closeView()
// reject(err)
// })
// })
view.webContents.session.webRequest.onCompleted({ urls: ['*://www.xiami.com/*'] }, details => {
if (/\/_____tmd_____\/slide\?/.test(details.url)) {
for (const item of details.responseHeaders['set-cookie']) {
if (!/^x5sec=/.test(item)) continue
const x5sec = /x5sec=(\w+);.+$/.exec(item)
isActioned = true
closeView().finally(() => {
if (!x5sec) return reject(new Error('get x5sec failed'))
resolve(x5sec[1])
})
}
}
})
// console.log(url)
global.modules.mainWindow.setBrowserView(view)
const windowSizeInfo = getWindowSizeInfo(global.appSetting)
view.setBounds({ x: (windowSizeInfo.width - 380) / 2, y: ((windowSizeInfo.height - 320 + 52) / 2), width: 380, height: 320 })
view.webContents.loadURL(url, {
httpReferrer: 'https://www.xiami.com/',
})
// view.webContents.openDevTools()
}))
mainHandle(ipcMainWindowNames.handle_xm_verify_close, async() => {
await closeView()
if (!rejectFn) return
if (!isActioned) rejectFn(new Error('canceled verify'))
rejectFn = null
})

View File

@ -1,91 +0,0 @@
const { BrowserWindow } = require('electron')
const { mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../../common/ipc')
const { getWindowSizeInfo } = require('../../utils')
let win
const closeWin = async() => {
if (!win) return
// await win.webContents.session.clearCache()
// if (global.modules.mainWindow) global.modules.mainWindow.removeBrowserView(win)
if (win.isDestroyed()) {
win = null
return
}
await win.webContents.session.clearStorageData()
win.destroy()
win = null
}
mainHandle(ipcMainWindowNames.handle_xm_verify_open, (event, url) => new Promise((resolve, reject) => {
if (!global.modules.mainWindow) return reject(new Error('mainWindow is undefined'))
if (win) win.destroy()
let isActioned = false
const mainWindowSizeInfo = global.modules.mainWindow.getBounds()
const windowSizeInfo = getWindowSizeInfo(global.appSetting)
win = new BrowserWindow({
parent: global.modules.mainWindow,
width: 1000,
height: 800,
resizable: false,
// transparent: true,
x: mainWindowSizeInfo.x + (windowSizeInfo.width - 1000) / 2,
y: mainWindowSizeInfo.y + (windowSizeInfo.height - 800 + 52) / 2,
minimizable: false,
maximizable: false,
// movable: false,
// frame: false,
// modal: true,
webPreferences: {
enableRemoteModule: false,
disableHtmlFullscreenWindowResize: true,
},
})
// win.webContents.on('did-finish-load', () => {
// if (/punish\?/.test(win.webContents.getURL())) return
// let ses = win.webContents.session
// ses.cookies.get({ name: 'x5sec' })
// .then(async([x5sec]) => {
// isActioned = true
// await closeWin()
// if (!x5sec) return reject(new Error('get x5sec failed'))
// resolve(x5sec.value)
// }).catch(async err => {
// isActioned = true
// await closeWin()
// reject(err)
// })
// })
win.webContents.session.webRequest.onCompleted({ urls: ['*://www.xiami.com/*'] }, details => {
if (/\/_____tmd_____\/slide\?/.test(details.url)) {
for (const item of details.responseHeaders['set-cookie']) {
if (!/^x5sec=/.test(item)) continue
const x5sec = /x5sec=(\w+);.+$/.exec(item)
isActioned = true
closeWin().finally(() => {
if (!x5sec) return reject(new Error('get x5sec failed'))
resolve(x5sec[1])
})
}
}
})
win.webContents.loadURL(url, {
httpReferrer: 'https://www.xiami.com/',
})
win.on('closed', async() => {
await closeWin()
if (isActioned) return
reject(new Error('canceled verify'))
})
// win.webContents.openDevTools()
}))
mainHandle(ipcMainWindowNames.handle_xm_verify_close, async() => {
await closeWin()
})

View File

@ -4,7 +4,7 @@
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut") transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
.control-bar(v-show="!lrcConfig.isLock") .control-bar(v-show="!lrcConfig.isLock")
core-control-bar(:lrcConfig="lrcConfig" :themes="themeList") core-control-bar(:lrcConfig="lrcConfig" :themes="themeList")
core-lyric(:lrcConfig="lrcConfig" :isShowLyricTransition="isShowLyricTransition") core-lyric(:lrcConfig="lrcConfig" :isPlayLxlrc="isPlayLxlrc" :isShowLyricTransition="isShowLyricTransition")
div.resize-left(@mousedown.self="handleMouseDown('left', $event)") div.resize-left(@mousedown.self="handleMouseDown('left', $event)")
div.resize-top(@mousedown.self="handleMouseDown('top', $event)") div.resize-top(@mousedown.self="handleMouseDown('top', $event)")
div.resize-right(@mousedown.self="handleMouseDown('right', $event)") div.resize-right(@mousedown.self="handleMouseDown('right', $event)")
@ -45,6 +45,7 @@ export default {
}, },
}, },
isShowLyricTransition: true, isShowLyricTransition: true,
isPlayLxlrc: true,
themeList: [ themeList: [
{ {
id: 0, id: 0,
@ -118,9 +119,10 @@ export default {
document.removeEventListener('mouseup', this.handleMouseUp) document.removeEventListener('mouseup', this.handleMouseUp)
}, },
methods: { methods: {
handleUpdateConfig({ config, languageId, isShowLyricTransition }) { handleUpdateConfig({ config, languageId, isShowLyricTransition, isPlayLxlrc }) {
this.lrcConfig = config this.lrcConfig = config
this.isShowLyricTransition = isShowLyricTransition this.isShowLyricTransition = isShowLyricTransition
this.isPlayLxlrc = isPlayLxlrc
if (this.$i18n.locale !== languageId && languageId != null) this.$i18n.locale = languageId if (this.$i18n.locale !== languageId && languageId != null) this.$i18n.locale = languageId
}, },
handleMouseDown(origin, event) { handleMouseDown(origin, event) {

View File

@ -1,7 +1,8 @@
<template lang="pug"> <template lang="pug">
div(:class="[$style.lyric, lyricEvent.isMsDown ? $style.draging : null]" :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" ref="dom_lyric")
div(:class="$style.lyricSpace") div(:class="$style.lyricSpace")
div(v-for="(info, index) in lyricLines" :key="index" :class="[$style.lineContent, lyric.line == index ? (lrcConfig.style.isZoomActiveLrc ? $style.lrcActiveZoom : $style.lrcActive) : null]") 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]")
p(:class="$style.lrcLine") {{info.text}} p(:class="$style.lrcLine") {{info.text}}
div(:class="$style.lyricSpace") div(:class="$style.lyricSpace")
</template> </template>
@ -9,7 +10,7 @@ div(:class="[$style.lyric, lyricEvent.isMsDown ? $style.draging : null]" :style=
<script> <script>
import { rendererOn, rendererSend, NAMES } from '../../../common/ipc' import { rendererOn, rendererSend, NAMES } from '../../../common/ipc'
import { scrollTo } from '../../../renderer/utils' import { scrollTo } from '../../../renderer/utils'
import Lyric from 'lrc-file-parser' import Lyric from '@renderer/utils/lyric-font-player'
let cancelScrollFn = null let cancelScrollFn = null
@ -27,6 +28,10 @@ export default {
} }
}, },
}, },
isPlayLxlrc: {
type: Boolean,
default: true,
},
isShowLyricTransition: { isShowLyricTransition: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -65,6 +70,7 @@ export default {
lyrics: { lyrics: {
lyric: '', lyric: '',
tlyric: '', tlyric: '',
lxlyric: '',
}, },
} }
}, },
@ -85,7 +91,7 @@ export default {
if (n.length) { if (n.length) {
this.lyricLines = n this.lyricLines = n
this.$nextTick(() => { this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p') this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc() this.handleScrollLrc()
}) })
} else { } else {
@ -97,7 +103,7 @@ export default {
if (this.lyricLines === this._lyricLines && this._lyricLines.length) return if (this.lyricLines === this._lyricLines && this._lyricLines.length) return
this.lyricLines = this._lyricLines this.lyricLines = this._lyricLines
this.$nextTick(() => { this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p') this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc() this.handleScrollLrc()
}) })
}, 50) }, 50)
@ -105,7 +111,7 @@ export default {
} else { } else {
this.lyricLines = n this.lyricLines = n
this.$nextTick(() => { this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p') this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc() this.handleScrollLrc()
}) })
} }
@ -120,8 +126,11 @@ export default {
}, },
immediate: true, immediate: true,
}, },
isShowLyricTransition(n) { isShowLyricTransition() {
console.log(n) this.setLyric()
rendererSend(NAMES.winLyric.get_lyric_info, 'status')
},
isPlayLxlrc() {
this.setLyric() this.setLyric()
rendererSend(NAMES.winLyric.get_lyric_info, 'status') rendererSend(NAMES.winLyric.get_lyric_info, 'status')
}, },
@ -129,6 +138,11 @@ export default {
created() { created() {
rendererOn(NAMES.winLyric.set_lyric_info, (event, data) => this.handleSetInfo(data)) rendererOn(NAMES.winLyric.set_lyric_info, (event, data) => this.handleSetInfo(data))
window.lrc = new Lyric({ window.lrc = new Lyric({
lineClassName: 'lrc-content',
fontClassName: 'font',
shadowClassName: 'shadow',
shadowContent: true,
activeLineClassName: 'active',
onPlay: (line, text) => { onPlay: (line, text) => {
this.lyric.text = text this.lyric.text = text
this.lyric.line = line this.lyric.line = line
@ -136,6 +150,12 @@ export default {
}, },
onSetLyric: lines => { // listening lyrics seting event onSetLyric: lines => { // listening lyrics seting event
// console.log(lines) // lines is array of all lyric text // console.log(lines) // lines is array of all lyric text
this.$refs.dom_lyric_text.textContent = ''
const dom_lines = document.createDocumentFragment()
for (const line of lines) {
dom_lines.appendChild(line.dom_line)
}
this.$refs.dom_lyric_text.appendChild(dom_lines)
this.lyric.lines = lines this.lyric.lines = lines
this.lyric.line = 0 this.lyric.line = 0
}, },
@ -159,6 +179,7 @@ export default {
case 'lyric': case 'lyric':
this.lyrics.lyric = data.lrc this.lyrics.lyric = data.lrc
this.lyrics.tlyric = data.tlrc this.lyrics.tlyric = data.tlrc
this.lyrics.lxlyric = data.lxlrc
this.setLyric() this.setLyric()
break break
case 'play': case 'play':
@ -171,8 +192,9 @@ export default {
break break
case 'info': case 'info':
// console.log('info', data) // console.log('info', data)
this.lyrics.lyric = data.lyric this.lyrics.lyric = data.lrc
this.lyrics.tlyric = data.tlyric this.lyrics.tlyric = data.tlrc
this.lyrics.lxlyric = data.lxlrc
this.setLyric() this.setLyric()
this.$nextTick(() => { this.$nextTick(() => {
this.lyric.line = data.line this.lyric.line = data.line
@ -211,7 +233,10 @@ export default {
cancelScrollFn = scrollTo(this.$refs.dom_lyric, dom_p ? (dom_p.offsetTop - this.$refs.dom_lyric.clientHeight * 0.5 + dom_p.clientHeight / 2) : 0) 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) { handleLyricMouseDown(e) {
if (e.target.classList.contains(this.$style.lrcLine)) { if (e.target.classList.contains('font') ||
e.target.parentNode.classList.contains('font') ||
e.target.classList.contains('translation') ||
e.target.parentNode.classList.contains('translation')) {
this.lyricEvent.isMsDown = true this.lyricEvent.isMsDown = true
this.lyricEvent.msDownY = e.clientY this.lyricEvent.msDownY = e.clientY
this.lyricEvent.msDownScrollY = this.$refs.dom_lyric.scrollTop this.lyricEvent.msDownScrollY = this.$refs.dom_lyric.scrollTop
@ -279,7 +304,11 @@ export default {
rendererSend(NAMES.winLyric.close) rendererSend(NAMES.winLyric.close)
}, },
setLyric() { setLyric() {
window.lrc.setLyric((this.isShowLyricTransition && this.lyrics.tlyric ? (this.lyrics.tlyric + '\n') : '') + (this.lyrics.lyric || '')) window.lrc.setLyric(
this.isPlayLxlrc && this.lyrics.lxlyric ? this.lyrics.lxlyric : this.lyrics.lyric,
this.isShowLyricTransition && this.lyrics.tlyric ? this.lyrics.tlyric : '',
// (this.isShowLyricTransition && this.lyrics.tlyric ? (this.lyrics.tlyric + '\n') : '') + (this.lyrics.lyric || ''),
)
}, },
}, },
} }
@ -292,34 +321,93 @@ export default {
text-align: center; text-align: center;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
font-size: 68px; font-size: 16px;
padding: 0 5px; color: @color-theme-lyric;
opacity: .6; cursor: move;
transition: opacity @transition-theme;
&.draging { :global {
.lrc-line { .lrc-content {
cursor: grabbing; line-height: 1.2;
margin: 16px 0;
overflow-wrap: break-word;
.font {
display: inline-block;
}
.font, .translation {
cursor: grab;
}
.translation {
transition: @transition-theme !important;
transition-property: font-size, color;
font-size: 0.8em;
margin-top: 5px;
}
.line {
transition-property: font-size, color !important;
background: none !important;
-webkit-text-fill-color: unset;
// -webkit-text-fill-color: none !important;
}
&.active {
.line {
color: @color-theme;
}
.translation {
color: @color-theme;
}
// span {
// color: @color-theme;
// }
}
span {
transition: @transition-theme !important;
transition-property: font-size !important;
font-size: 1em;
background-repeat: no-repeat;
background-color: @color-theme-lyric;
background-image: -webkit-linear-gradient(top, @color-theme, @color-theme);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-size: 0 100%;
}
}
.shadow {
text-shadow: 1px 1px 2px rgb(32, 32, 32);
} }
} }
// p {
// padding: 8px 0;
// line-height: 1.2;
// overflow-wrap: break-word;
// transition: @transition-theme !important;
// transition-property: color, font-size;
// }
} }
.lrc-line { // .lrc-line {
display: inline-block; // display: inline-block;
padding: 8px 0; // padding: 8px 0;
line-height: 1.2; // line-height: 1.2;
overflow-wrap: break-word; // overflow-wrap: break-word;
transition: @transition-theme; // transition: @transition-theme;
transition-property: color, font-size, text-shadow; // transition-property: color, font-size, text-shadow;
cursor: grab; // cursor: grab;
// font-weight: bold; // // font-weight: bold;
// background-clip: text; // // background-clip: text;
color: @color-theme-lyric; // color: @color-theme-lyric;
text-shadow: 1px 1px 2px #000; // text-shadow: 1px 1px 2px #000;
// background: linear-gradient(@color-theme-lyric, @color-theme-lyric); // // background: linear-gradient(@color-theme-lyric, @color-theme-lyric);
// background-clip: text; // // background-clip: text;
// -webkit-background-clip: text; // // -webkit-background-clip: text;
// -webkit-text-fill-color: #fff; // // -webkit-text-fill-color: #fff;
// -webkit-text-stroke: thin #124628; // // -webkit-text-stroke: thin #124628;
} // }
.lyric-space { .lyric-space {
height: 70%; height: 70%;
} }
@ -334,9 +422,28 @@ export default {
// -webkit-text-stroke: thin #124628; // -webkit-text-stroke: thin #124628;
} }
} }
.draging {
:global {
.lrc-content {
.font, .translation {
cursor: grabbing;
}
}
}
}
.lrc-active-zoom { .lrc-active-zoom {
.lrc-active; :global {
font-size: 1.2em; .lrc-content {
&.active {
.translation {
font-size: 1em;
}
span {
font-size: 1.2em;
}
}
}
}
} }
.footer { .footer {
flex: 0 0 100px; flex: 0 0 100px;
@ -348,14 +455,30 @@ export default {
each(@themes, { each(@themes, {
:global(#container.@{value}) { :global(#container.@{value}) {
.lrc-line { // .lrc-line {
color: ~'@{color-@{value}-theme-lyric}'; // color: ~'@{color-@{value}-theme-lyric}';
} // }
.lrc-active, .lrc-active-zoom { .lrc-active, .lrc-active-zoom {
.lrc-line { .lrc-line {
color: ~'@{color-@{value}-theme-lyric_2}'; color: ~'@{color-@{value}-theme-lyric_2}';
} }
} }
.lyric {
color: ~'@{color-@{value}-theme-lyric}';
:global {
.lrc-content {
&.active {
.line {
color: ~'@{color-@{value}-theme}';
}
}
span {
// background-color: ~'@{color-@{value}-theme_2-font}';
background-image: -webkit-linear-gradient(top, ~'@{color-@{value}-theme}', ~'@{color-@{value}-theme}');
}
}
}
}
} }
}) })

View File

@ -6,7 +6,6 @@
core-view#view core-view#view
core-player#player core-player#player
core-icons core-icons
material-xm-verify-modal(v-show="globalObj.xm.isShowVerify" :show="globalObj.xm.isShowVerify" :bg-close="false" @close="handleXMVerifyModalClose")
material-version-modal(v-show="version.showModal") material-version-modal(v-show="version.showModal")
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact") material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
#container(v-else :class="theme") #container(v-else :class="theme")
@ -16,7 +15,6 @@
core-view#view core-view#view
core-player#player core-player#player
core-icons core-icons
material-xm-verify-modal(v-show="globalObj.xm.isShowVerify" :show="globalObj.xm.isShowVerify" :bg-close="false" @close="handleXMVerifyModalClose")
material-version-modal(v-show="version.showModal") material-version-modal(v-show="version.showModal")
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact") material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
</template> </template>
@ -27,8 +25,9 @@ import { mapMutations, mapGetters, mapActions } from 'vuex'
import { rendererOn, rendererSend, rendererInvoke, NAMES } from '../common/ipc' import { rendererOn, rendererSend, rendererInvoke, NAMES } from '../common/ipc'
import { isLinux } from '../common/utils' import { isLinux } from '../common/utils'
import music from './utils/music' import music from './utils/music'
import { throttle, openUrl, compareVer, getPlayList } from './utils' import { throttle, openUrl, compareVer, getPlayList, parseUrlParams } from './utils'
import { base as eventBaseName } from './event/names' import { base as eventBaseName } from './event/names'
import apiSourceInfo from './utils/music/api-source-info'
window.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS window.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS
dnscache({ dnscache({
@ -37,6 +36,20 @@ dnscache({
cachesize: 1000, cachesize: 1000,
}) })
const listThrottle = (fn, delay = 100) => {
let timer = null
let _data = {}
return function(data) {
Object.assign(_data, data)
if (timer) return
timer = setTimeout(() => {
timer = null
fn.call(this, _data)
_data = {}
}, delay)
}
}
export default { export default {
data() { data() {
return { return {
@ -44,12 +57,15 @@ export default {
isDT: false, isDT: false,
isLinux, isLinux,
globalObj: { globalObj: {
apiSource: 'test', apiSource: null,
proxy: {}, proxy: {},
isShowPact: false, isShowPact: false,
qualityList: {}, qualityList: {},
xm: { userApi: {
isShowVerify: false, list: [],
status: false,
message: 'initing',
apis: {},
}, },
}, },
updateTimeout: null, updateTimeout: null,
@ -70,24 +86,12 @@ export default {
}), }),
}, },
created() { created() {
this.saveDefaultList = throttle(n => { this.saveMyList = listThrottle(data => {
rendererSend(NAMES.mainWindow.save_playlist, { rendererSend(NAMES.mainWindow.save_playlist, {
type: 'defaultList', type: 'myList',
data: n, data,
}) })
}, 500) }, 300)
this.saveLoveList = throttle(n => {
rendererSend(NAMES.mainWindow.save_playlist, {
type: 'loveList',
data: n,
})
}, 500)
this.saveUserList = throttle(n => {
rendererSend(NAMES.mainWindow.save_playlist, {
type: 'userList',
data: n,
})
}, 500)
this.saveDownloadList = throttle(n => { this.saveDownloadList = throttle(n => {
rendererSend(NAMES.mainWindow.save_playlist, { rendererSend(NAMES.mainWindow.save_playlist, {
type: 'downloadList', type: 'downloadList',
@ -115,19 +119,19 @@ export default {
}, },
defaultList: { defaultList: {
handler(n) { handler(n) {
this.saveDefaultList(n) this.saveMyList({ defaultList: n })
}, },
deep: true, deep: true,
}, },
loveList: { loveList: {
handler(n) { handler(n) {
this.saveLoveList(n) this.saveMyList({ loveList: n })
}, },
deep: true, deep: true,
}, },
userList: { userList: {
handler(n) { handler(n) {
this.saveUserList(n) this.saveMyList({ userList: n })
}, },
deep: true, deep: true,
}, },
@ -140,8 +144,20 @@ export default {
searchHistoryList(n) { searchHistoryList(n) {
this.saveSearchHistoryList(n) this.saveSearchHistoryList(n)
}, },
'globalObj.apiSource'(n) { 'globalObj.apiSource'(n, o) {
this.globalObj.qualityList = music.supportQuality[n] if (/^user_api/.test(n)) {
this.globalObj.qualityList = {}
this.globalObj.userApi.status = false
this.globalObj.userApi.message = 'initing'
} else {
this.globalObj.qualityList = music.supportQuality[n]
}
if (o === null) return
rendererInvoke(NAMES.mainWindow.set_user_api, n).catch(err => {
console.log(err)
let api = apiSourceInfo.find(api => !api.disabled)
if (api) this.globalObj.apiSource = api.id
})
if (n != this.setting.apiSource) { if (n != this.setting.apiSource) {
this.setSetting(Object.assign({}, this.setting, { this.setSetting(Object.assign({}, this.setting, {
apiSource: n, apiSource: n,
@ -177,10 +193,12 @@ export default {
...mapMutations('player', { ...mapMutations('player', {
setPlayList: 'setList', setPlayList: 'setList',
}), }),
...mapActions('songList', ['getListDetailAll']),
init() { init() {
document.documentElement.style.fontSize = this.windowSizeActive.fontSize document.documentElement.style.fontSize = this.windowSizeActive.fontSize
rendererInvoke(NAMES.mainWindow.get_env_params).then(this.handleEnvParamsInit) const asyncTask = []
asyncTask.push(rendererInvoke(NAMES.mainWindow.get_env_params).then(this.handleEnvParamsInit))
document.body.addEventListener('click', this.handleBodyClick, true) document.body.addEventListener('click', this.handleBodyClick, true)
rendererOn(NAMES.mainWindow.update_available, (e, info) => { rendererOn(NAMES.mainWindow.update_available, (e, info) => {
@ -244,14 +262,23 @@ export default {
}, 60 * 30 * 1000) }, 60 * 30 * 1000)
this.listenEvent() this.listenEvent()
this.initData() asyncTask.push(this.initData())
asyncTask.push(this.initUserApi())
this.globalObj.apiSource = this.setting.apiSource this.globalObj.apiSource = this.setting.apiSource
this.globalObj.qualityList = music.supportQuality[this.setting.apiSource] if (/^user_api/.test(this.setting.apiSource)) {
rendererInvoke(NAMES.mainWindow.set_user_api, this.setting.apiSource)
} else {
this.globalObj.qualityList = music.supportQuality[this.setting.apiSource]
}
this.globalObj.proxy = Object.assign({}, this.setting.network.proxy) this.globalObj.proxy = Object.assign({}, this.setting.network.proxy)
window.globalObj = this.globalObj window.globalObj = this.globalObj
// sdk // sdk
music.init() asyncTask.push(music.init())
Promise.all(asyncTask).then(() => {
this.handleInitEnvParamSearch()
this.handleInitEnvParamPlay()
})
}, },
enableIgnoreMouseEvents() { enableIgnoreMouseEvents() {
if (this.isDT) return if (this.isDT) return
@ -265,12 +292,14 @@ export default {
}, },
initData() { // initData() { //
this.initLocalList() // return Promise.all([
this.initMyList(), //
this.initSearchHistoryList(), //
])
// this.initDownloadList() // // this.initDownloadList() //
this.initSearchHistoryList() //
}, },
initLocalList() { initMyList() {
getPlayList().then(({ defaultList, loveList, userList, downloadList }) => { return getPlayList().then(({ defaultList, loveList, userList, downloadList }) => {
if (!defaultList) defaultList = this.defaultList if (!defaultList) defaultList = this.defaultList
if (!loveList) loveList = this.loveList if (!loveList) loveList = this.loveList
if (userList) { if (userList) {
@ -340,6 +369,72 @@ export default {
}) })
}) })
}, },
initUserApi() {
return Promise.all([
rendererOn(NAMES.mainWindow.user_api_status, (event, { status, message, apiInfo }) => {
// console.log(apiInfo)
this.globalObj.userApi.status = status
this.globalObj.userApi.message = message
if (status) {
if (apiInfo.id === this.setting.apiSource) {
let apis = {}
let qualitys = {}
for (const [source, { actions, type, qualitys: sourceQualitys }] of Object.entries(apiInfo.sources)) {
if (type != 'music') continue
apis[source] = {}
for (const action of actions) {
switch (action) {
case 'musicUrl':
apis[source].getMusicUrl = (songInfo, type) => {
const requestKey = `request__${Math.random().toString().substring(2)}`
return {
canceleFn() {
rendererInvoke(NAMES.mainWindow.request_user_api_cancel, requestKey)
},
promise: rendererInvoke(NAMES.mainWindow.request_user_api, {
requestKey,
data: {
source: source,
action: 'musicUrl',
info: {
type,
musicInfo: songInfo,
},
},
}).then(res => {
// console.log(res)
if (!/^https?:/.test(res.data.url)) return Promise.reject(new Error('Get url failed'))
return { type, url: res.data.url }
}).catch(err => {
console.log(err.message)
return Promise.reject(err)
}),
}
}
break
default:
break
}
}
qualitys[source] = sourceQualitys
}
this.globalObj.qualityList = qualitys
this.globalObj.userApi.apis = apis
}
}
}),
rendererInvoke(NAMES.mainWindow.get_user_api_list).then(res => {
// console.log(res)
if (![...apiSourceInfo.map(s => s.id), ...res.map(s => s.id)].includes(this.setting.apiSource)) {
console.warn('reset api')
let api = apiSourceInfo.find(api => !api.disabled)
if (api) this.globalObj.apiSource = api.id
}
this.globalObj.userApi.list = res
}),
])
},
showUpdateModal() { showUpdateModal() {
(this.version.newVersion && this.version.newVersion.history (this.version.newVersion && this.version.newVersion.history
? Promise.resolve(this.version.newVersion) ? Promise.resolve(this.version.newVersion)
@ -389,18 +484,91 @@ export default {
document.body.addEventListener('mouseenter', this.dieableIgnoreMouseEvents) document.body.addEventListener('mouseenter', this.dieableIgnoreMouseEvents)
document.body.addEventListener('mouseleave', this.enableIgnoreMouseEvents) document.body.addEventListener('mouseleave', this.enableIgnoreMouseEvents)
} }
this.handleInitEnvParamSearch()
this.handleInitEnvParamPlay()
},
// search
handleInitEnvParamSearch() {
if (this.envParams.search == null) return
this.$router.push({
path: 'search',
query: {
text: this.envParams.search,
},
})
},
// play
handleInitEnvParamPlay() {
if (this.envParams.play == null || typeof this.envParams.play != 'string') return
// -play="source=kw&link=ID"
// -play="source=myList&name="
// -play="source=myList&name=&index="
const params = parseUrlParams(this.envParams.play)
if (params.type != 'songList') return
this.handlePlaySongList(params)
},
handlePlaySongList(params) {
switch (params.source) {
case 'myList':
if (params.name != null) {
let targetList
const lists = Object.values(window.allList)
for (const list of lists) {
if (list.name === params.name) {
targetList = list
break
}
}
if (!targetList) return
if (this.envParams.search != null) {
this.$router.push({ this.setPlayList({
path: 'search', list: {
query: { list: targetList.list,
text: this.envParams.search, id: targetList.id,
}, },
}) index: this.getListPlayIndex(targetList.list, params.index),
})
}
break
case 'kw':
case 'kg':
case 'tx':
case 'mg':
case 'wy':
this.playSongListDetail(params.source, params.link, params.index)
break
} }
}, },
handleXMVerifyModalClose() { async playSongListDetail(source, link, playIndex) {
music.xm.closeVerifyModal() if (link == null) return
let list
try {
list = await this.getListDetailAll({ source, id: decodeURIComponent(link) })
} catch (err) {
console.log(err)
}
this.setPlayList({
list: {
list,
id: null,
},
index: this.getListPlayIndex(list, playIndex),
})
},
getListPlayIndex(list, index) {
if (index == null) {
index = 1
} else {
index = parseInt(index)
if (Number.isNaN(index)) {
index = 1
} else {
if (index < 1) index = 1
else if (index > list.length) index = list.length
}
}
return index - 1
}, },
listenEvent() { listenEvent() {
window.eventHub.$on('key_escape_down', this.handle_key_esc_down) window.eventHub.$on('key_escape_down', this.handle_key_esc_down)

View File

@ -85,13 +85,12 @@ div(:class="$style.player")
</template> </template>
<script> <script>
import Lyric from 'lrc-file-parser' import Lyric from '@renderer/utils/lyric-font-player'
import { rendererSend, rendererOn, NAMES } from '../../../common/ipc' import { rendererSend, rendererOn, NAMES } from '../../../common/ipc'
import { formatPlayTime2, getRandom, checkPath, setTitle, clipboardWriteText, debounce, throttle, assertApiSupport } from '../../utils' import { formatPlayTime2, getRandom, checkPath, setTitle, clipboardWriteText, debounce, throttle, assertApiSupport } from '../../utils'
import { mapGetters, mapActions, mapMutations } from 'vuex' import { mapGetters, mapActions, mapMutations } from 'vuex'
import { requestMsg } from '../../utils/message' import { requestMsg } from '../../utils/message'
import { player as eventPlayerNames } from '../../../common/hotKey' import { player as eventPlayerNames } from '../../../common/hotKey'
import musicSdk from '@renderer/utils/music'
import path from 'path' import path from 'path'
let audio let audio
@ -216,8 +215,9 @@ export default {
singer: this.musicInfo.singer, singer: this.musicInfo.singer,
name: this.musicInfo.name, name: this.musicInfo.name,
album: this.musicInfo.album, album: this.musicInfo.album,
lyric: this.musicInfo.lrc, lrc: this.musicInfo.lrc,
tlyric: this.musicInfo.tlrc, tlrc: this.musicInfo.tlrc,
lxlrc: this.musicInfo.lxlrc,
isPlay: this.isPlay, isPlay: this.isPlay,
line: this.lyric.line, line: this.lyric.line,
played_time: audio.currentTime * 1000, played_time: audio.currentTime * 1000,
@ -275,6 +275,9 @@ export default {
'setting.player.isShowLyricTransition'() { 'setting.player.isShowLyricTransition'() {
this.setLyric() this.setLyric()
}, },
'setting.player.isPlayLxlrc'() {
this.setLyric()
},
async list(n, o) { async list(n, o) {
if (n === o && this.musicInfo.songmid) { if (n === o && this.musicInfo.songmid) {
let index = this.listId == 'download' let index = this.listId == 'download'
@ -327,6 +330,7 @@ export default {
}, },
methods: { methods: {
...mapActions('player', ['getUrl', 'getPic', 'getLrc', 'playPrev', 'playNext']), ...mapActions('player', ['getUrl', 'getPic', 'getLrc', 'playPrev', 'playNext']),
...mapActions('list', ['getOtherSource']),
...mapMutations('player', [ ...mapMutations('player', [
'setPlayMusicInfo', 'setPlayMusicInfo',
'setPlayIndex', 'setPlayIndex',
@ -452,6 +456,10 @@ export default {
}) })
window.lrc = new Lyric({ window.lrc = new Lyric({
lineClassName: 'lrc-content',
fontClassName: 'font',
shadowContent: false,
activeLineClassName: 'active',
onPlay: (line, text) => { onPlay: (line, text) => {
this.lyric.text = text this.lyric.text = text
this.lyric.line = line this.lyric.line = line
@ -463,7 +471,7 @@ export default {
this.lyric.lines = lines this.lyric.lines = lines
this.lyric.line = 0 this.lyric.line = 0
}, },
offset: 80, // offset: 80,
}) })
this.handleRegisterEvent('on') this.handleRegisterEvent('on')
@ -492,7 +500,7 @@ export default {
this.setImg(targetSong.musicInfo) this.setImg(targetSong.musicInfo)
this.setLrc(targetSong.musicInfo) this.setLrc(targetSong.musicInfo)
} else { } else {
if (!this.assertApiSupport(targetSong.source)) return this.playNext() // if (!this.assertApiSupport(targetSong.source)) return this.playNext()
this.musicInfo.songmid = targetSong.songmid this.musicInfo.songmid = targetSong.songmid
this.musicInfo.singer = targetSong.singer this.musicInfo.singer = targetSong.singer
this.musicInfo.name = targetSong.name this.musicInfo.name = targetSong.name
@ -573,7 +581,7 @@ export default {
togglePlay() { togglePlay() {
if (!audio.src) { if (!audio.src) {
if (this.restorePlayTime != null) { if (this.restorePlayTime != null) {
if (!this.assertApiSupport(this.targetSong.source)) return this.playNext() // if (!this.assertApiSupport(this.targetSong.source)) return this.playNext()
this.setUrl(this.targetSong) this.setUrl(this.targetSong)
} }
return return
@ -613,10 +621,7 @@ export default {
this.status = this.statusText = 'Try toggle source...' this.status = this.statusText = 'Try toggle source...'
return (originMusic.otherSource && originMusic.otherSource.length ? Promise.resolve(originMusic.otherSource) : musicSdk.findMusic(originMusic)).then(res => { return this.getOtherSource(originMusic).then(otherSource => {
this.updateMusicInfo({ id: this.listId, index: this.playIndex, data: { otherSource: res }, musicInfo: originMusic })
return res
}).then(otherSource => {
console.log('find otherSource', otherSource) console.log('find otherSource', otherSource)
if (otherSource.length) { if (otherSource.length) {
for (const item of otherSource) { for (const item of otherSource) {
@ -644,10 +649,11 @@ export default {
this.getLrc(targetSong).then(() => { this.getLrc(targetSong).then(() => {
this.musicInfo.lrc = targetSong.lrc this.musicInfo.lrc = targetSong.lrc
this.musicInfo.tlrc = targetSong.tlrc this.musicInfo.tlrc = targetSong.tlrc
this.musicInfo.lxlrc = targetSong.lxlrc
}).catch(() => { }).catch(() => {
this.status = this.statusText = this.$t('core.player.lyric_error') this.status = this.statusText = this.$t('core.player.lyric_error')
}).finally(() => { }).finally(() => {
this.handleUpdateWinLyricInfo('lyric', { lrc: this.musicInfo.lrc, tlrc: this.musicInfo.tlrc }) this.handleUpdateWinLyricInfo('lyric', { lrc: this.musicInfo.lrc, tlrc: this.musicInfo.tlrc, lxlrc: this.musicInfo.lxlrc })
this.setLyric() this.setLyric()
}) })
}, },
@ -661,6 +667,7 @@ export default {
this.musicInfo.songmid = null this.musicInfo.songmid = null
this.musicInfo.lrc = null this.musicInfo.lrc = null
this.musicInfo.tlrc = null this.musicInfo.tlrc = null
this.musicInfo.lxlrc = null
this.musicInfo.url = null this.musicInfo.url = null
this.nowPlayTime = 0 this.nowPlayTime = 0
this.maxPlayTime = 0 this.maxPlayTime = 0
@ -851,7 +858,15 @@ export default {
}) })
}, },
setLyric() { setLyric() {
window.lrc.setLyric((this.setting.player.isShowLyricTransition && this.musicInfo.tlrc ? (this.musicInfo.tlrc + '\n') : '') + (this.musicInfo.lrc || '')) window.lrc.setLyric(
this.setting.player.isPlayLxlrc && this.musicInfo.lxlrc ? this.musicInfo.lxlrc : this.musicInfo.lrc,
this.setting.player.isShowLyricTransition && this.musicInfo.tlrc ? this.musicInfo.tlrc : '',
// (
// this.setting.player.isShowLyricTransition && this.musicInfo.tlrc
// ? (this.musicInfo.tlrc + '\n')
// : ''
// ) + (this.musicInfo.lrc || ''),
)
if (this.isPlay && (this.musicInfo.url || this.listId == 'download')) { if (this.isPlay && (this.musicInfo.url || this.listId == 'download')) {
window.lrc.play(audio.currentTime * 1000) window.lrc.play(audio.currentTime * 1000)
this.handleUpdateWinLyricInfo('play', audio.currentTime * 1000) this.handleUpdateWinLyricInfo('play', audio.currentTime * 1000)

View File

@ -29,7 +29,8 @@
div(:class="$style.right") div(:class="$style.right")
div(:class="[$style.lyric, lyricEvent.isMsDown ? $style.draging : null]" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric") div(:class="[$style.lyric, lyricEvent.isMsDown ? $style.draging : null]" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="$style.lyricSpace") div(:class="$style.lyricSpace")
p(v-for="(info, index) in lyricLines" :key="index" :class="lyric.line == index ? $style.lrcActive : null") {{info.text}} 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") div(:class="$style.lyricSpace")
material-music-comment(:class="$style.comment" :titleFormat="this.setting.download.fileName" :musicInfo="musicInfo" v-model="isShowComment") material-music-comment(:class="$style.comment" :titleFormat="this.setting.download.fileName" :musicInfo="musicInfo" v-model="isShowComment")
@ -158,11 +159,14 @@ export default {
handler(n, o) { handler(n, o) {
this.isSetedLines = true this.isSetedLines = true
if (o) { if (o) {
this.$refs.dom_lyric_text.textContent = ''
this.setLyric(n)
this._lyricLines = n this._lyricLines = n
if (n.length) { if (n.length) {
this.lyricLines = n this.lyricLines = n
this.$nextTick(() => { this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p') this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc() this.handleScrollLrc()
}) })
} else { } else {
@ -174,7 +178,7 @@ export default {
if (this.lyricLines === this._lyricLines && this._lyricLines.length) return if (this.lyricLines === this._lyricLines && this._lyricLines.length) return
this.lyricLines = this._lyricLines this.lyricLines = this._lyricLines
this.$nextTick(() => { this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p') this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc() this.handleScrollLrc()
}) })
}, 50) }, 50)
@ -182,7 +186,7 @@ export default {
} else { } else {
this.lyricLines = n this.lyricLines = n
this.$nextTick(() => { this.$nextTick(() => {
this.dom_lines = this.$refs.dom_lyric.querySelectorAll('p') this.dom_lines = this.$refs.dom_lyric.querySelectorAll('.lrc-content')
this.handleScrollLrc() this.handleScrollLrc()
}) })
} }
@ -235,6 +239,8 @@ export default {
document.addEventListener('mousemove', this.handleMouseMsMove) document.addEventListener('mousemove', this.handleMouseMsMove)
document.addEventListener('mouseup', this.handleMouseMsUp) document.addEventListener('mouseup', this.handleMouseMsUp)
window.addEventListener('resize', this.handleResize) window.addEventListener('resize', this.handleResize)
// console.log('object', this.$refs.dom_lyric_text)
this.setLyric(this.lyricLines)
}, },
beforeDestroy() { beforeDestroy() {
this.clearLyricScrollTimeout() this.clearLyricScrollTimeout()
@ -250,6 +256,13 @@ export default {
...mapMutations('player', [ ...mapMutations('player', [
'visiblePlayerDetail', 'visiblePlayerDetail',
]), ]),
setLyric(lines) {
const dom_lines = document.createDocumentFragment()
for (const line of lines) {
dom_lines.appendChild(line.dom_line)
}
this.$refs.dom_lyric_text.appendChild(dom_lines)
},
handleResize() { handleResize() {
this.setProgressWidth() this.setProgressWidth()
}, },
@ -540,6 +553,7 @@ export default {
&:before { &:before {
position: absolute; position: absolute;
z-index: 1;
top: 0; top: 0;
left: 0; left: 0;
content: ' '; content: ' ';
@ -565,16 +579,62 @@ export default {
overflow: hidden; overflow: hidden;
font-size: 16px; font-size: 16px;
cursor: grab; cursor: grab;
color: @color-theme_2-font;
&.draging { &.draging {
cursor: grabbing; cursor: grabbing;
} }
p { :global {
padding: 8px 0; .lrc-content {
line-height: 1.2; line-height: 1.2;
overflow-wrap: break-word; margin: 16px 0;
transition: @transition-theme !important; overflow-wrap: break-word;
transition-property: color, font-size;
.translation {
transition: @transition-theme !important;
transition-property: font-size, color;
font-size: 1em;
margin-top: 5px;
}
.line {
transition-property: font-size, color !important;
background: none !important;
-webkit-text-fill-color: unset;
// -webkit-text-fill-color: none !important;
}
&.active {
.line {
color: @color-theme;
}
.translation {
font-size: 1.2em;
color: @color-theme;
}
span {
// color: @color-theme;
font-size: 1.2em;
}
}
span {
transition: @transition-theme !important;
transition-property: font-size !important;
font-size: 1em;
background-repeat: no-repeat;
background-color: rgba(77, 77, 77, 0.9);
background-image: -webkit-linear-gradient(top, @color-theme, @color-theme);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-size: 0 100%;
}
}
} }
// p {
// padding: 8px 0;
// line-height: 1.2;
// overflow-wrap: break-word;
// transition: @transition-theme !important;
// transition-property: color, font-size;
// }
} }
.lyric-space { .lyric-space {
height: 70%; height: 70%;
@ -736,6 +796,7 @@ each(@themes, {
.container { .container {
border-left-color: ~'@{color-@{value}-theme}'; border-left-color: ~'@{color-@{value}-theme}';
background-color: ~'@{color-@{value}-theme_2-background_1}'; background-color: ~'@{color-@{value}-theme_2-background_1}';
// color: ~'@{color-@{value}-theme_2-font}';
} }
.right { .right {
&:before { &:before {
@ -769,9 +830,24 @@ each(@themes, {
box-shadow: 0 0 4px ~'@{color-@{value}-theme-hover}'; box-shadow: 0 0 4px ~'@{color-@{value}-theme-hover}';
// border-color: ~'@{color-@{value}-theme-hover}'; // border-color: ~'@{color-@{value}-theme-hover}';
} }
.lrc-active { .lyric {
color: ~'@{color-@{value}-theme}'; :global {
.lrc-content {
&.active {
.line {
color: ~'@{color-@{value}-theme}';
}
}
span {
// background-color: ~'@{color-@{value}-theme_2-font}';
background-image: -webkit-linear-gradient(top, ~'@{color-@{value}-theme}', ~'@{color-@{value}-theme}');
}
}
}
} }
// .lrc-active {
// color: ~'@{color-@{value}-theme}';
// }
.footerLeftControlBtns { .footerLeftControlBtns {
color: ~'@{color-@{value}-theme_2-font}'; color: ~'@{color-@{value}-theme_2-font}';
} }

View File

@ -1,319 +0,0 @@
<template lang="pug">
div(:class="$style.lists")
h2(:class="$style.listsTitle") {{$t('core.aside.my_list')}}
ul.scroll(:class="$style.listsContent")
li(:class="$style.listsItem" v-for="item in lists" :key="item.id") {{item.name}}
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
div(v-show="!list.length" :class="$style.noitem")
p(v-html="noItem")
material-flow-btn(:show="isShowEditBtn && assertApiSupport(source)" :remove-btn="false" @btn-click="handleFlowBtnClick")
</template>
<script>
import { mapGetters } from 'vuex'
import { scrollTo, clipboardWriteText, assertApiSupport } from '../../utils'
export default {
name: 'MaterialSongList',
model: {
prop: 'selectdData',
event: 'input',
},
props: {
list: {
type: Array,
default() {
return []
},
},
page: {
type: Number,
required: true,
},
limit: {
type: Number,
required: true,
},
total: {
type: Number,
required: true,
},
selectdData: {
type: Array,
required: true,
},
source: {
type: String,
},
noItem: {
type: String,
default: '列表加载中...',
},
},
computed: {
...mapGetters(['setting']),
},
watch: {
selectdList(n) {
const len = n.length
if (len) {
this.isShowEditBtn = true
} else {
this.isShowEditBtn = false
}
},
selectdData(n) {
const len = n.length
if (len) {
this.isShowEditBtn = true
this.selectdList = [...n]
} else {
this.isShowEditBtn = false
this.removeAllSelect()
}
},
list(n) {
this.removeAllSelect()
if (!this.list.length) return
this.$nextTick(() => scrollTo(this.$refs.dom_scrollContent, 0))
},
},
data() {
return {
clickTime: 0,
clickIndex: -1,
isShowEditBtn: false,
selectdList: [],
keyEvent: {
isShiftDown: false,
isModDown: false,
isADown: false,
aDownTimeout: null,
},
lastSelectIndex: 0,
}
},
created() {
this.listenEvent()
},
beforeDestroy() {
this.unlistenEvent()
},
methods: {
listenEvent() {
window.eventHub.$on('key_shift_down', this.handle_key_shift_down)
window.eventHub.$on('key_shift_up', this.handle_key_shift_up)
window.eventHub.$on('key_mod_down', this.handle_key_mod_down)
window.eventHub.$on('key_mod_up', this.handle_key_mod_up)
window.eventHub.$on('key_mod+a_down', this.handle_key_mod_a_down)
window.eventHub.$on('key_mod+a_up', this.handle_key_mod_a_up)
},
unlistenEvent() {
window.eventHub.$off('key_shift_down', this.handle_key_shift_down)
window.eventHub.$off('key_shift_up', this.handle_key_shift_up)
window.eventHub.$off('key_mod_down', this.handle_key_mod_down)
window.eventHub.$off('key_mod_up', this.handle_key_mod_up)
window.eventHub.$off('key_mod+a_down', this.handle_key_mod_a_down)
window.eventHub.$off('key_mod+a_up', this.handle_key_mod_a_up)
},
handle_key_shift_down() {
if (!this.keyEvent.isShiftDown) this.keyEvent.isShiftDown = true
},
handle_key_shift_up() {
if (this.keyEvent.isShiftDown) this.keyEvent.isShiftDown = false
},
handle_key_mod_down() {
if (!this.keyEvent.isModDown) this.keyEvent.isModDown = true
},
handle_key_mod_up() {
if (this.keyEvent.isModDown) this.keyEvent.isModDown = false
},
handle_key_mod_a_down() {
if (!this.keyEvent.isADown) {
this.keyEvent.isModDown = false
this.keyEvent.isADown = true
this.handleSelectAllData()
if (this.keyEvent.aDownTimeout) clearTimeout(this.keyEvent.aDownTimeout)
this.keyEvent.aDownTimeout = setTimeout(() => {
this.keyEvent.aDownTimeout = null
this.keyEvent.isADown = false
}, 500)
}
},
handle_key_mod_a_up() {
if (this.keyEvent.isADown) {
if (this.keyEvent.aDownTimeout) {
clearTimeout(this.keyEvent.aDownTimeout)
this.keyEvent.aDownTimeout = null
}
this.keyEvent.isADown = false
}
},
handleDoubleClick(event, index) {
if (event.target.classList.contains('select')) return
this.handleSelectData(event, index)
if (
window.performance.now() - this.clickTime > 400 ||
this.clickIndex !== index
) {
this.clickTime = window.performance.now()
this.clickIndex = index
return
}
this.emitEvent(this.assertApiSupport(this.source) ? 'testPlay' : 'search', index)
this.clickTime = 0
this.clickIndex = -1
},
handleSelectData(event, clickIndex) {
if (this.keyEvent.isShiftDown) {
if (this.selectdList.length) {
let lastSelectIndex = this.lastSelectIndex
this.removeAllSelect()
if (lastSelectIndex != clickIndex) {
let isNeedReverse = false
if (clickIndex < lastSelectIndex) {
let temp = lastSelectIndex
lastSelectIndex = clickIndex
clickIndex = temp
isNeedReverse = true
}
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
}
} else if (this.keyEvent.isModDown) {
this.lastSelectIndex = clickIndex
let item = this.list[clickIndex]
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()
} else return
this.$emit('input', [...this.selectdList])
},
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)
},
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) {
this.emitEvent('togglePage', page)
},
handleFlowBtnClick(action) {
this.emitEvent('flowBtnClick', action)
},
emitEvent(action, data) {
this.$emit('action', { action, data })
},
handleChangeSelect() {
this.$emit('input', [...this.selectdList])
},
handleContextMenu(event) {
if (!event.target.classList.contains('select')) return
let classList = this.$refs.dom_scrollContent.classList
classList.add(this.$style.copying)
window.requestAnimationFrame(() => {
let str = window.getSelection().toString()
classList.remove(this.$style.copying)
str = str.trim()
if (!str.length) return
clipboardWriteText(str)
})
},
assertApiSupport(source) {
return assertApiSupport(source)
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.lists {
flex: none;
width: 15%;
display: flex;
flex-flow: column nowrap;
}
.title {
font-size: 12px;
line-height: 28px;
padding: 5px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
flex: none;
}
.list {
flex: auto;
min-width: 0;
}
.item {
}
.noitem {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
p {
font-size: 24px;
color: @color-theme_2-font-label;
}
}
each(@themes, {
:global(#container.@{value}) {
.tbody {
td {
&:first-child {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
}
.noitem {
p {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
}
})
</style>

View File

@ -1,5 +1,5 @@
<template lang="pug"> <template lang="pug">
button(:class="[$style.btn, min ? $style.min : null]" :disabled="disabled" @click="$emit('click', $event)") button(:class="[$style.btn, min ? $style.min : null, outline ? $style.outline : null]" :disabled="disabled" @click="$emit('click', $event)")
slot slot
</template> </template>
@ -9,6 +9,10 @@ export default {
min: { min: {
type: Boolean, type: Boolean,
}, },
outline: {
type: Boolean,
default: false,
},
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -36,6 +40,10 @@ export default {
opacity: .4; opacity: .4;
} }
&.outline {
background-color: transparent;
}
&:hover { &:hover {
background-color: @color-btn-hover; background-color: @color-btn-hover;
} }
@ -54,6 +62,9 @@ each(@themes, {
.btn { .btn {
color: ~'@{color-@{value}-btn}'; color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}'; background-color: ~'@{color-@{value}-btn-background}';
&.outline {
background-color: transparent;
}
&:hover { &:hover {
background-color: ~'@{color-@{value}-btn-hover}'; background-color: ~'@{color-@{value}-btn-hover}';
} }

View File

@ -6,7 +6,7 @@ material-modal(:show="show" :bg-close="bgClose" @close="handleClose")
span(:class="$style.name") {{this.musicInfo && `${musicInfo.name}`}} span(:class="$style.name") {{this.musicInfo && `${musicInfo.name}`}}
| &nbsp;{{$t('material.list_add_modal.title_last')}} | &nbsp;{{$t('material.list_add_modal.title_last')}}
div.scroll(:class="$style.btnContent") div.scroll(:class="$style.btnContent")
material-btn(:class="$style.btn" :tips="$t('material.list_add_modal.btn_title', { name: item.name })" :key="item.id" @click="handleClick(index)" v-for="(item, index) in lists") {{item.name}} material-btn(:class="$style.btn" :tips="$t('material.list_add_modal.btn_title', { name: item.name })" :key="item.id" :disabled="item.isExist" @click="handleClick(index)" v-for="(item, index) in lists") {{item.name}}
material-btn(:class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" @click="handleEditing($event)" :tips="$t('view.list.lists_new_list_btn')") material-btn(:class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" @click="handleEditing($event)" :tips="$t('view.list.lists_new_list_btn')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 42 42' space='preserve') svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 42 42' space='preserve')
use(xlink:href='#icon-addTo') use(xlink:href='#icon-addTo')
@ -57,11 +57,12 @@ export default {
computed: { computed: {
...mapGetters('list', ['defaultList', 'loveList', 'userList']), ...mapGetters('list', ['defaultList', 'loveList', 'userList']),
lists() { lists() {
if (!this.musicInfo) return []
return [ return [
this.defaultList, this.defaultList,
this.loveList, this.loveList,
...this.userList, ...this.userList,
].filter(l => l.id != this.excludeListId.includes(l.id)) ].filter(l => !this.excludeListId.includes(l.id)).map(l => ({ ...l, isExist: l.list.some(s => s.songmid == this.musicInfo.songmid) }))
}, },
spaceNum() { spaceNum() {
return this.lists.length < 2 ? 0 : (3 - this.lists.length % 3 - 1) return this.lists.length < 2 ? 0 : (3 - this.lists.length % 3 - 1)

View File

@ -62,7 +62,7 @@ export default {
this.defaultList, this.defaultList,
this.loveList, this.loveList,
...this.userList, ...this.userList,
].filter(l => l.id != this.excludeListId.includes(l.id)) ].filter(l => !this.excludeListId.includes(l.id))
}, },
spaceNum() { spaceNum() {
return this.lists.length < 2 ? 0 : (3 - this.lists.length % 3 - 1) return this.lists.length < 2 ? 0 : (3 - this.lists.length % 3 - 1)

View File

@ -50,7 +50,7 @@ export default {
singer: '', singer: '',
}, },
page: 1, page: 1,
total: 10, total: 0,
maxPage: 1, maxPage: 1,
limit: 20, limit: 20,
isHotLoading: true, isHotLoading: true,

View File

@ -31,8 +31,6 @@ div(:class="$style.songList")
td(:style="{ width: rowWidth.r6 }" style="padding-left: 0; padding-right: 0;") td(:style="{ width: rowWidth.r6 }" style="padding-left: 0; padding-right: 0;")
material-list-buttons(:index="index" :class="$style.btns" material-list-buttons(:index="index" :class="$style.btns"
:remove-btn="false" @btn-click="handleListBtnClick" :remove-btn="false" @btn-click="handleListBtnClick"
:listAdd-btn="assertApiSupport(item.source)"
:play-btn="assertApiSupport(item.source)"
:download-btn="assertApiSupport(item.source)") :download-btn="assertApiSupport(item.source)")
//- button.btn-info(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k'] || item._types.flac" @click.stop='openDownloadModal(index)') //- button.btn-info(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k'] || item._types.flac" @click.stop='openDownloadModal(index)')
//- button.btn-secondary(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k']" @click.stop='testPlay(index)') //- button.btn-secondary(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k']" @click.stop='testPlay(index)')
@ -243,7 +241,7 @@ export default {
this.clickIndex = index this.clickIndex = index
return return
} }
this.emitEvent(this.assertApiSupport(this.source) ? 'testPlay' : 'search', index) this.emitEvent('testPlay', index)
this.clickTime = 0 this.clickTime = 0
this.clickIndex = -1 this.clickIndex = -1
}, },
@ -340,9 +338,9 @@ export default {
}, },
handleListItemRigthClick(event, index) { handleListItemRigthClick(event, index) {
this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.list[index].source].getMusicDetailPageUrl this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.list[index].source].getMusicDetailPageUrl
this.listMenu.itemMenuControl.play = // this.listMenu.itemMenuControl.play =
this.listMenu.itemMenuControl.playLater = // this.listMenu.itemMenuControl.playLater =
this.listMenu.itemMenuControl.download = this.listMenu.itemMenuControl.download =
this.assertApiSupport(this.list[index].source) this.assertApiSupport(this.list[index].source)
let dom_selected = this.$refs.dom_tbody.querySelector('tr.selected') let dom_selected = this.$refs.dom_tbody.querySelector('tr.selected')
if (dom_selected) dom_selected.classList.remove('selected') if (dom_selected) dom_selected.classList.remove('selected')

View File

@ -0,0 +1,240 @@
<template lang="pug">
material-modal(:show="visible" bg-close @close="handleClose")
main(:class="$style.main")
h2 {{$t('material.user_api_modal.title')}}
ul.scroll(v-if="apiList.length" :class="$style.content")
li(:class="[$style.listItem, setting.apiSource == api.id ? $style.active : null]" v-for="(api, index) in apiList" :key="api.id")
div(:class="$style.listLeft")
h3 {{api.name}}
p {{api.description}}
material-btn(:class="$style.listBtn" outline :tips="$t('material.user_api_modal.btn_remove')" @click.stop="handleRemove(index)")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 212.982 212.982' space='preserve' v-once)
use(xlink:href='#icon-delete')
div(v-else :class="$style.content")
div(:class="$style.noitem") {{$t('material.user_api_modal.noitem')}}
div(:class="$style.note")
p(:class="[$style.ruleLink]")
| {{$t('material.user_api_modal.readme')}}
span.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md')" tips="https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md") FAQ.md
p {{$t('material.user_api_modal.note')}}
div(:class="$style.footer")
material-btn(:class="$style.footerBtn" @click="handleImport") {{$t('material.user_api_modal.btn_import')}}
//- material-btn(:class="$style.footerBtn" @click="handleExport") {{$t('material.user_api_modal.btn_export')}}
</template>
<script>
import { mapGetters } from 'vuex'
import { rendererInvoke, NAMES } from '@common/ipc'
import { promises as fsPromises } from 'fs'
import {
selectDir,
openUrl,
} from '../../utils'
import apiSourceInfo from '../../utils/music/api-source-info'
export default {
props: {
visible: {
type: Boolean,
default: false,
},
},
model: {
prop: 'visible',
event: 'toggle',
},
data() {
return {
globalObj: window.globalObj,
}
},
computed: {
...mapGetters(['setting']),
apiList() {
return this.globalObj.userApi.list
},
},
methods: {
handleImport() {
selectDir({
title: this.$t('material.user_api_modal.import_file'),
properties: ['openFile'],
filters: [
{ name: 'LX API File', extensions: ['js'] },
{ name: 'All Files', extensions: ['*'] },
],
}).then(result => {
if (result.canceled) return
return fsPromises.readFile(result.filePaths[0]).then(data => {
return rendererInvoke(NAMES.mainWindow.import_user_api, data.toString()).then(({ apiList }) => {
window.globalObj.userApi.list = apiList
})
})
})
},
handleExport() {
},
async handleRemove(index) {
const api = this.apiList[index]
if (!api) return
if (this.setting.apiSource == api.id) {
let backApi = apiSourceInfo.find(api => !api.disabled)
if (backApi) window.globalObj.apiSource = backApi.id
}
window.globalObj.userApi.list = await rendererInvoke(NAMES.mainWindow.remove_user_api, [api.id])
},
handleClose() {
this.$emit('toggle', false)
},
handleOpenUrl(url) {
openUrl(url)
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.main {
padding: 15px;
max-width: 400px;
min-width: 300px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
min-height: 0;
// max-height: 100%;
// overflow: hidden;
h2 {
font-size: 16px;
color: @color-theme_2-font;
line-height: 1.3;
text-align: center;
}
}
.name {
color: @color-theme;
}
.content {
flex: auto;
min-height: 100px;
max-height: 100%;
margin-top: 15px;
}
.listItem {
display: flex;
flex-flow: row nowrap;
align-items: center;
transition: background-color 0.2s ease;
padding: 10px;
border-radius: @radius-border;
&:hover {
background-color: @color-theme_2-hover;
}
&.active {
background-color: @color-theme_2-active;
}
h3 {
font-size: 15px;
color: @color-theme_2-font;
word-break: break-all;
}
p {
margin-top: 5px;
font-size: 14px;
color: @color-theme_2-font-label;
word-break: break-all;
}
}
.noitem {
height: 100px;
font-size: 18px;
color: @color-theme_2-font-label;
display: flex;
justify-content: center;
align-items: center;
}
.listLeft {
flex: auto;
min-width: 0;
display: flex;
flex-flow: column nowrap;
justify-content: center;
}
.listBtn {
flex: none;
height: 30px;
width: 30px;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
svg {
width: 60%;
}
}
.note {
margin-top: 15px;
font-size: 12px;
line-height: 1.25;
color: @color-theme_2-font;
p {
+ p {
margin-top: 5px;
}
}
}
.footer {
margin-top: 15px;
display: flex;
flex-flow: row nowrap;
}
.footerBtn {
flex: auto;
height: 36px;
line-height: 36px;
padding: 0 10px !important;
width: 150px;
.mixin-ellipsis-1;
+ .footerBtn {
margin-left: 15px;
}
}
.ruleLink {
.mixin-ellipsis-1;
}
each(@themes, {
:global(#container.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.listItem {
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&.active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
h3 {
color: ~'@{color-@{value}-theme_2-font}';
}
p {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
.noitem {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
})
</style>

View File

@ -1,50 +0,0 @@
<template lang="pug">
material-modal(:show="show" :bg-close="bgClose" @close="handleClose")
main.ignore-to-rem(:class="$style.main")
h2 {{$t('material.xm_verify_modal.title')}}
</template>
<script>
export default {
props: {
show: {
type: Boolean,
default: false,
},
bgClose: {
type: Boolean,
default: true,
},
},
methods: {
handleClose() {
this.$emit('close')
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.main {
background: #fff !important;
&:global(.ignore-to-rem) {
padding: 15px;
width: 360px;
height: 330px;
h2 {
font-size: 16px;
}
}
h2 {
font-size: 13px;
color: @color-theme_2-font;
line-height: 1.3;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,10 @@
{
"title": "Custom Source Management",
"readme": "Source writing instructions: ",
"note": "Tip: Although we have isolated the script's running environment as much as possible, importing scripts containing malicious behaviors may still affect your system. Please import them carefully.",
"btn_remove": "Remove",
"btn_import": "Import",
"btn_export": "Export",
"import_file": "Select music API script file",
"noitem": "There is nothing here...😲"
}

View File

@ -11,6 +11,10 @@
"basic_sourcename_real": "Original", "basic_sourcename_real": "Original",
"basic_sourcename_alias": "Aliases", "basic_sourcename_alias": "Aliases",
"basic_sourcename": "Source name", "basic_sourcename": "Source name",
"basic_source_status_success": "Initialization successful",
"basic_source_status_initing": "Initializing",
"basic_source_status_failed": "Initialization failed",
"basic_source_user_api_btn": "Custom Source Management",
"basic_window_size_title": "Set the window size", "basic_window_size_title": "Set the window size",
"basic_window_size": "Window size", "basic_window_size": "Window size",
"basic_window_size_smaller": "Smaller", "basic_window_size_smaller": "Smaller",
@ -30,6 +34,7 @@
"play": "Play", "play": "Play",
"play_save_play_time": "Remember playback progress", "play_save_play_time": "Remember playback progress",
"play_lyric_transition": "Show lyrics translation", "play_lyric_transition": "Show lyrics translation",
"play_lyric_lxlrc": "Use Karaoke-style lyrics playback (if supported)",
"play_quality": "Play 320K quality songs first (if supported)", "play_quality": "Play 320K quality songs first (if supported)",
"play_task_bar": "Show playing progress on the taskbar", "play_task_bar": "Show playing progress on the taskbar",
"play_mediaDevice_title": "Select a media device for audio output", "play_mediaDevice_title": "Select a media device for audio output",
@ -121,10 +126,11 @@
"other_tray_theme": "Tray Icon Style", "other_tray_theme": "Tray Icon Style",
"other_tray_theme_native": "Solid Color", "other_tray_theme_native": "Solid Color",
"other_tray_theme_origin": "Primary Color", "other_tray_theme_origin": "Primary Color",
"other_cache": "Cache size (Not recommended since resources such as pictures after the cache is cleaned need re-downloading. The software will dynamically manage the cache size based on disk space)", "other_resource_cache": "Resource cache management (pictures, audios and other caches, pictures and other resources will need to be downloaded again after cleaning up, it is not recommended to clean up, the software will dynamically manage the cache size according to the disk space)",
"other_cache_label": "Cache size used: ", "other_resource_cache_label": "The software has used cache size: ",
"other_cache_label_title": "Currently used cache size", "other_resource_cache_clear_btn": "Clear resource cache",
"other_cache_clear_btn": "Clear cache", "other_play_list_cache": "List cache management (links to songs that have been cached in my list, alternative sources for playback, after cleaning up, you need to re-acquire them when you play and download songs, and do not clean up if there are no issues related to song playback)",
"other_play_list_cache_clear_btn": "Clear list cache information",
"update": "Update", "update": "Update",
"update_latest_label": "Latest version: ", "update_latest_label": "Latest version: ",

View File

@ -0,0 +1,10 @@
{
"title": "自定义源管理",
"readme": "源编写说明:",
"note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。",
"btn_remove": "移除",
"btn_import": "导入",
"btn_export": "导出",
"import_file": "选择音乐API脚本文件",
"noitem": "这里竟然是空的 😲"
}

View File

@ -11,6 +11,10 @@
"basic_sourcename_real": "原名", "basic_sourcename_real": "原名",
"basic_sourcename_alias": "别名", "basic_sourcename_alias": "别名",
"basic_sourcename": "音源名字", "basic_sourcename": "音源名字",
"basic_source_status_success": "初始化成功",
"basic_source_status_initing": "初始化中",
"basic_source_status_failed": "初始化失败",
"basic_source_user_api_btn": "自定义源管理",
"basic_window_size_title": "设置软件窗口尺寸", "basic_window_size_title": "设置软件窗口尺寸",
"basic_window_size": "窗口尺寸", "basic_window_size": "窗口尺寸",
"basic_window_size_smaller": "较小", "basic_window_size_smaller": "较小",
@ -30,6 +34,7 @@
"play": "播放设置", "play": "播放设置",
"play_save_play_time": "记住播放进度", "play_save_play_time": "记住播放进度",
"play_lyric_transition": "显示歌词翻译", "play_lyric_transition": "显示歌词翻译",
"play_lyric_lxlrc": "使用卡拉OK式歌词播放如果支持",
"play_quality": "优先播放320K品质的歌曲如果支持", "play_quality": "优先播放320K品质的歌曲如果支持",
"play_task_bar": "在任务栏上显示当前歌曲播放进度", "play_task_bar": "在任务栏上显示当前歌曲播放进度",
"play_mediaDevice_title": "选择声音输出的媒体设备", "play_mediaDevice_title": "选择声音输出的媒体设备",
@ -121,10 +126,11 @@
"other_tray_theme": "托盘图标样式", "other_tray_theme": "托盘图标样式",
"other_tray_theme_native": "纯色", "other_tray_theme_native": "纯色",
"other_tray_theme_origin": "原色", "other_tray_theme_origin": "原色",
"other_cache": "缓存大小(清理缓存后图片等资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小)", "other_resource_cache": "资源缓存管理(图片、音频等缓存,清理后图片等资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小)",
"other_cache_label": "软件已使用缓存大小:", "other_resource_cache_label": "软件已使用缓存大小:",
"other_cache_label_title": "当前已用缓存", "other_resource_cache_clear_btn": "清理资源缓存",
"other_cache_clear_btn": "清理缓存", "other_play_list_cache": "列表缓存管理(我的列表中已缓存的歌曲链接、播放代替源,清理后播放、下载歌曲时需要重新获取,没有歌曲播放相关的问题不要清理)",
"other_play_list_cache_clear_btn": "清理列表缓存信息",
"update": "软件更新", "update": "软件更新",
"update_latest_label": "最新版本:", "update_latest_label": "最新版本:",

View File

@ -0,0 +1,10 @@
{
"title": "自定義源管理",
"readme": "源編寫說明:",
"note": "提示:雖然我們已經盡可能地隔離了腳本的運行環境,但導入包含惡意行為的腳本仍可能會影響你的系統,請謹慎導入。",
"btn_remove": "移除",
"btn_import": "導入",
"btn_export": "導出",
"import_file": "選擇音樂API腳本文件",
"noitem": "這裡竟然是空的 😲"
}

View File

@ -11,6 +11,10 @@
"basic_sourcename_real": "原名", "basic_sourcename_real": "原名",
"basic_sourcename_alias": "別名", "basic_sourcename_alias": "別名",
"basic_sourcename": "音源名字", "basic_sourcename": "音源名字",
"basic_source_status_success": "初始化成功",
"basic_source_status_initing": "初始化中",
"basic_source_status_failed": "初始化失敗",
"basic_source_user_api_btn": "自定義源管理",
"basic_window_size_title": "設置軟件窗口尺寸", "basic_window_size_title": "設置軟件窗口尺寸",
"basic_window_size": "窗口尺寸", "basic_window_size": "窗口尺寸",
"basic_window_size_smaller": "較小", "basic_window_size_smaller": "較小",
@ -30,6 +34,7 @@
"play": "播放設置", "play": "播放設置",
"play_save_play_time": "記住播放進度", "play_save_play_time": "記住播放進度",
"play_lyric_transition": "顯示歌詞翻譯", "play_lyric_transition": "顯示歌詞翻譯",
"play_lyric_lxlrc": "使用卡拉OK式歌詞播放如果支持",
"play_quality": "優先播放320K品質的歌曲如果支持", "play_quality": "優先播放320K品質的歌曲如果支持",
"play_task_bar": "在任務欄上顯示當前歌曲播放進度", "play_task_bar": "在任務欄上顯示當前歌曲播放進度",
"play_mediaDevice_title": "選擇聲音輸出的媒體設備", "play_mediaDevice_title": "選擇聲音輸出的媒體設備",
@ -121,10 +126,11 @@
"other_tray_theme": "托盤圖標樣式", "other_tray_theme": "托盤圖標樣式",
"other_tray_theme_native": "純色", "other_tray_theme_native": "純色",
"other_tray_theme_origin": "原色", "other_tray_theme_origin": "原色",
"other_cache": "緩存大小(清理緩存後圖片等資源將需要重新下載,不建議清理,軟件會根據磁盤空間動態管理緩存大小)", "other_resource_cache": "資源緩存管理(圖片、音頻等緩存,清理後圖片等資源將需要重新下載,不建議清理,軟件會根據磁盤空間動態管理緩存大小)",
"other_cache_label": "軟件已使用緩存大小:", "other_resource_cache_label": "軟件已使用緩存大小:",
"other_cache_label_title": "當前已用緩存", "other_resource_cache_clear_btn": "清理資源緩存",
"other_cache_clear_btn": "清理緩存", "other_play_list_cache": "列表緩存管理(我的列表中已緩存的歌曲鏈接、播放代替源,清理後播放、下載歌曲時需要重新獲取,沒有歌曲播放相關的問題不要清理)",
"other_play_list_cache_clear_btn": "清理列表緩存信息",
"update": "軟件更新", "update": "軟件更新",
"update_latest_label": "最新版本:", "update_latest_label": "最新版本:",

View File

@ -1,3 +1,5 @@
import musicSdk from '../../utils/music'
let allList = {} let allList = {}
window.allList = allList window.allList = allList
@ -48,7 +50,12 @@ const getters = {
// actions // actions
const actions = { const actions = {
getOtherSource({ state, commit }, musicInfo) {
return (musicInfo.otherSource && musicInfo.otherSource.length ? Promise.resolve(musicInfo.otherSource) : musicSdk.findMusic(musicInfo)).then(otherSource => {
commit('setOtherSource', { musicInfo, otherSource })
return otherSource
})
},
} }
// mitations // mitations
@ -57,20 +64,6 @@ const mutations = {
if (defaultList != null) Object.assign(state.defaultList, { list: defaultList.list, location: defaultList.location }) 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 (loveList != null) Object.assign(state.loveList, { list: loveList.list, location: loveList.location })
if (userList != null) state.userList = userList if (userList != null) state.userList = userList
if (window.localStorage.getItem('isResetOtherSource') != '1') {
for (const item of defaultList.list) {
if (item.otherSource) item.otherSource = null
}
for (const item of loveList.list) {
if (item.otherSource) item.otherSource = null
}
for (const list of userList) {
for (const item of list.list) {
if (item.otherSource) item.otherSource = null
}
}
window.localStorage.setItem('isResetOtherSource', '1')
}
allListInit(state.defaultList, state.loveList, state.userList) allListInit(state.defaultList, state.loveList, state.userList)
state.isInitedList = true state.isInitedList = true
}, },
@ -205,6 +198,22 @@ const mutations = {
targetList.list.splice(sortNum - 1, 0, ...musicInfos) targetList.list.splice(sortNum - 1, 0, ...musicInfos)
}, },
clearCache() {
const lists = Object.values(allList)
for (const { list } of lists) {
for (const item of list) {
if (item.otherSource) item.otherSource = null
if (item.typeUrl['128k']) delete item.typeUrl['128k']
if (item.typeUrl['320k']) delete item.typeUrl['320k']
if (item.typeUrl.flac) delete item.typeUrl.flac
if (item.typeUrl.wav) delete item.typeUrl.wav
// if (item.lxlrc == '') item.lxlrc = null
}
}
},
setOtherSource(state, { musicInfo, otherSource }) {
musicInfo.otherSource = otherSource
},
} }
export default { export default {

View File

@ -1,6 +1,6 @@
import path from 'path' import path from 'path'
import music from '../../utils/music' import music from '../../utils/music'
import { getRandom, checkPath, assertApiSupport } from '../../utils' import { getRandom, checkPath } from '../../utils'
// state // state
const state = { const state = {
@ -18,8 +18,8 @@ const state = {
} }
let urlRequest let urlRequest
let picRequest // let picRequest
let lrcRequest // let lrcRequest
const filterList = async({ playedList, listInfo, savePath, commit }) => { const filterList = async({ playedList, listInfo, savePath, commit }) => {
// if (this.list.listName === null) return // if (this.list.listName === null) return
@ -44,7 +44,7 @@ const filterList = async({ playedList, listInfo, savePath, commit }) => {
} }
} else { } else {
list = listInfo.list.filter(s => { list = listInfo.list.filter(s => {
if (!assertApiSupport(s.source)) return false // if (!assertApiSupport(s.source)) return false
canPlayList.push(s) canPlayList.push(s)
let index = filteredPlayedList.indexOf(s) let index = filteredPlayedList.indexOf(s)
@ -62,6 +62,42 @@ const filterList = async({ playedList, listInfo, savePath, commit }) => {
return list return list
} }
const getPic = function(musicInfo, retryedSource = [], originMusic) {
// console.log(musicInfo.source)
return music[musicInfo.source].getPic(musicInfo).promise.catch(err => {
if (!retryedSource.includes(musicInfo.source)) retryedSource.push(musicInfo.source)
return this.dispatch('list/getOtherSource', musicInfo).then(otherSource => {
if (!originMusic) originMusic = musicInfo
console.log('find otherSource', otherSource)
if (otherSource.length) {
for (const item of otherSource) {
if (retryedSource.includes(item.source)) continue
console.log('try toggle to: ', item.source, item.name, item.singer, item.interval)
return getPic.call(this, item, retryedSource, originMusic)
}
}
return Promise.reject(err)
})
})
}
const getLyric = function(musicInfo, retryedSource = [], originMusic) {
return music[musicInfo.source].getLyric(musicInfo).promise.catch(err => {
if (!retryedSource.includes(musicInfo.source)) retryedSource.push(musicInfo.source)
return this.dispatch('list/getOtherSource', musicInfo).then(otherSource => {
if (!originMusic) originMusic = musicInfo
console.log('find otherSource', otherSource)
if (otherSource.length) {
for (const item of otherSource) {
if (retryedSource.includes(item.source)) continue
console.log('try toggle to: ', item.source, item.name, item.singer, item.interval)
return getLyric.call(this, item, retryedSource, originMusic)
}
}
return Promise.reject(err)
})
})
}
// getters // getters
const getters = { const getters = {
list: state => state.listInfo.list, list: state => state.listInfo.list,
@ -120,7 +156,11 @@ const actions = {
} }
if (urlRequest && urlRequest.cancelHttp) urlRequest.cancelHttp() if (urlRequest && urlRequest.cancelHttp) urlRequest.cancelHttp()
if (musicInfo.typeUrl[type] && !isRefresh) return Promise.resolve(musicInfo.typeUrl[type]) if (musicInfo.typeUrl[type] && !isRefresh) return Promise.resolve(musicInfo.typeUrl[type])
urlRequest = music[musicInfo.source].getMusicUrl(musicInfo, type) try {
urlRequest = music[musicInfo.source].getMusicUrl(musicInfo, type)
} catch (err) {
return Promise.reject(err)
}
return urlRequest.promise.then(({ url }) => { return urlRequest.promise.then(({ url }) => {
if (originMusic) commit('setUrl', { musicInfo: originMusic, url, type }) if (originMusic) commit('setUrl', { musicInfo: originMusic, url, type })
commit('setUrl', { musicInfo, url, type }) commit('setUrl', { musicInfo, url, type })
@ -132,33 +172,38 @@ const actions = {
}) })
}, },
getPic({ commit, state }, musicInfo) { getPic({ commit, state }, musicInfo) {
if (picRequest && picRequest.cancelHttp) picRequest.cancelHttp() // if (picRequest && picRequest.cancelHttp) picRequest.cancelHttp()
picRequest = music[musicInfo.source].getPic(musicInfo) // picRequest = music[musicInfo.source].getPic(musicInfo)
return picRequest.promise.then(url => { return getPic.call(this, musicInfo).then(url => {
picRequest = null // picRequest = null
commit('getPic', { musicInfo, url }) commit('getPic', { musicInfo, url })
}).catch(err => { }).catch(err => {
picRequest = null // picRequest = null
return Promise.reject(err) return Promise.reject(err)
}) })
}, },
getLrc({ commit, state }, musicInfo) { getLrc({ commit, state }, musicInfo) {
if (lrcRequest && lrcRequest.cancelHttp) lrcRequest.cancelHttp() // if (lrcRequest && lrcRequest.cancelHttp) lrcRequest.cancelHttp()
if (musicInfo.lrc && musicInfo.tlrc != null) { if (musicInfo.lrc && musicInfo.tlrc != null) {
if (musicInfo.lrc.startsWith('\ufeff[id:$00000000]')) { if (musicInfo.lrc.startsWith('\ufeff[id:$00000000]')) {
let str = musicInfo.lrc.replace('\ufeff[id:$00000000]\n', '') let str = musicInfo.lrc.replace('\ufeff[id:$00000000]\n', '')
commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc }) commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })
} else if (musicInfo.lrc.startsWith('[id:$00000000]')) {
let str = musicInfo.lrc.replace('[id:$00000000]\n', '')
commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })
}
if ((musicInfo.lxlrc == null && musicInfo.source != 'kg') || musicInfo.lxlrc != null) {
return Promise.resolve()
} }
return Promise.resolve()
} }
// lrcRequest = music[musicInfo.source].getLyric(musicInfo)
lrcRequest = music[musicInfo.source].getLyric(musicInfo) return getLyric.call(this, musicInfo).then(({ lyric, tlyric, lxlyric }) => {
return lrcRequest.promise.then(({ lyric, tlyric }) => { // lrcRequest = null
lrcRequest = null commit('setLrc', { musicInfo, lyric, tlyric, lxlyric })
commit('setLrc', { musicInfo, lyric, tlyric })
}).catch(err => { }).catch(err => {
lrcRequest = null // lrcRequest = null
return Promise.reject(err) return Promise.reject(err)
}) })
}, },
@ -292,6 +337,7 @@ const mutations = {
setLrc(state, datas) { setLrc(state, datas) {
datas.musicInfo.lrc = datas.lyric datas.musicInfo.lrc = datas.lyric
datas.musicInfo.tlrc = datas.tlyric datas.musicInfo.tlrc = datas.tlyric
datas.musicInfo.lxlrc = datas.lxlyric
}, },
setList(state, { list, index }) { setList(state, { list, index }) {
state.playMusicInfo = { state.playMusicInfo = {

View File

@ -396,3 +396,14 @@ export const getPlayList = () => rendererInvoke(NAMES.mainWindow.get_playlist).c
return rendererInvoke(NAMES.mainWindow.get_playlist, true) return rendererInvoke(NAMES.mainWindow.get_playlist, true)
}) })
// 解析URL参数为对象
export const parseUrlParams = str => {
const params = {}
if (typeof str !== 'string') return params
const paramsArr = str.split('&')
for (const param of paramsArr) {
let [key, value] = param.split('=')
params[key] = value
}
return params
}

View File

@ -0,0 +1,291 @@
const { getNow, TimeoutTools } = require('./utils')
// const fontFormateRxp = /(?=<\d+,\d+>).*?/g
const fontSplitRxp = /(?=<\d+,\d+>).*?/g
const timeRxp = /<(\d+),(\d+)>/
// Create animation
const createAnimation = (dom, duration) => new window.Animation(new window.KeyframeEffect(dom, [
{ backgroundSize: '0 100%' },
{ backgroundSize: '100% 100%' },
], {
duration,
easing: 'linear',
},
), document.timeline)
// https://jsfiddle.net/ceqpnbky/
// https://jsfiddle.net/ceqpnbky/1/
module.exports = class FontPlayer {
constructor({ lyric = '', translationLyric = '', lineClassName = '', fontClassName = '', translationClassName = '', lineModeClassName = '', shadowContent = false, shadowClassName = '' }) {
this.lyric = lyric
this.translationLyric = translationLyric
this.lineClassName = lineClassName
this.fontClassName = fontClassName
this.translationClassName = translationClassName
this.lineModeClassName = lineModeClassName
this.shadowContent = shadowContent
this.shadowClassName = shadowClassName
this.isPlay = false
this.curFontNum = 0
this.maxFontNum = 0
this._performanceTime = 0
this._performanceOffsetTime = 0
this.fontContent = null
this.timeoutTools = new TimeoutTools()
this.waitPlayTimeout = new TimeoutTools()
this._init()
}
_init() {
if (this.lyric == null) this.lyric = ''
this.isLineMode = false
this.lineContent = document.createElement('div')
if (this.lineClassName) this.lineContent.classList.add(this.lineClassName)
this.fontContent = document.createElement('div')
this.fontContent.style = 'position:relative;display:inline-block;'
if (this.fontClassName) this.fontContent.classList.add(this.fontClassName)
if (this.shadowContent) {
this.fontShadowContent = document.createElement('div')
this.fontShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
this.fontShadowContent.className = this.shadowClassName
this.fontContent.appendChild(this.fontShadowContent)
}
this.lineContent.appendChild(this.fontContent)
if (this.translationLyric) {
this.translationContent = document.createElement('div')
this.translationContent.style = 'position:relative;display:inline-block;'
this.translationContent.className = this.translationClassName
this.translationContent.textContent = this.translationLyric
this.lineContent.appendChild(document.createElement('br'))
this.lineContent.appendChild(this.translationContent)
if (this.shadowContent) {
this.translationShadowContent = document.createElement('div')
this.translationShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
this.translationShadowContent.className = this.shadowClassName
this.translationShadowContent.textContent = this.translationLyric
this.translationContent.appendChild(this.translationShadowContent)
}
}
this._parseLyric()
}
_parseLyric() {
const fonts = this.lyric.split(fontSplitRxp)
// console.log(fonts)
this.maxFontNum = fonts.length - 1
this.fonts = []
let text
for (const font of fonts) {
text = font.replace(timeRxp, '')
if (RegExp.$2 == '') return this._handleLineParse()
const time = parseInt(RegExp.$2)
const dom = document.createElement('span')
let shadowDom
dom.textContent = text
const animation = createAnimation(dom, time)
this.fontContent.appendChild(dom)
if (this.shadowContent) {
shadowDom = document.createElement('span')
shadowDom.textContent = text
this.fontShadowContent.appendChild(shadowDom)
}
// dom.style = shadowDom.style = this.fontStyle
// dom.className = shadowDom.className = this.fontClassName
this.fonts.push({
text,
startTime: parseInt(RegExp.$1),
time,
dom,
shadowDom,
animation,
})
}
// console.log(this.fonts)
}
_handleLineParse() {
this.isLineMode = true
const dom = document.createElement('span')
let shadowDom
dom.classList.add(this.lineModeClassName)
dom.textContent = this.lyric
if (this.shadowContent) {
shadowDom = document.createElement('span')
shadowDom.textContent = this.lyric
this.fontShadowContent.appendChild(shadowDom)
}
this.fontContent.appendChild(dom)
this.fonts.push({
text: this.lyric,
dom,
shadowDom,
})
}
_currentTime() {
return getNow() - this._performanceTime + this._performanceOffsetTime
}
_findcurFontNum(curTime) {
const length = this.fonts.length
for (let index = 0; index < length; index++) if (curTime <= this.fonts[index].startTime) return index === 0 ? 0 : index - 1
return length - 1
}
_handlePlayMaxFontNum() {
let curFont = this.fonts[this.curFontNum]
// console.log(curFont.text)
const currentTime = this._currentTime()
const driftTime = currentTime - curFont.startTime
if (currentTime > curFont.startTime + curFont.time) {
this._handlePlayFont(curFont, driftTime, true)
this.pause()
} else {
this._handlePlayFont(curFont, driftTime)
this.isPlay = false
}
}
_handlePlayFont(font, currentTime, toFinishe) {
switch (font.animation.playState) {
case 'finished':
break
case 'idle':
font.dom.style.backgroundSize = '100% 100%'
if (!toFinishe) font.animation.play()
break
default:
if (toFinishe) {
font.animation.cancel()
} else {
font.animation.currentTime = currentTime
font.animation.play()
}
break
}
}
_handlePlayLine(isPlayed) {
this.isPlay = false
this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0'
}
_handlePauseFont(font) {
if (font.animation.playState == 'running') font.animation.pause()
}
_refresh() {
this.curFontNum++
// console.log('curFontNum time', this.fonts[this.curFontNum].time)
if (this.curFontNum === this.maxFontNum) return this._handlePlayMaxFontNum()
let curFont = this.fonts[this.curFontNum]
let nextFont = this.fonts[this.curFontNum + 1]
// console.log(curFont, nextFont, this.curFontNum, this.maxFontNum)
const currentTime = this._currentTime()
// console.log(curFont.text)
const driftTime = currentTime - curFont.startTime
// console.log(currentTime, driftTime)
if (driftTime >= 0 || this.curFontNum == 0) {
this.delay = nextFont.startTime - curFont.startTime - driftTime
if (this.delay > 0) {
this._handlePlayFont(curFont, driftTime)
this.timeoutTools.start(() => {
this._refresh()
}, this.delay)
return
}
} else if (this.curFontNum == 0) {
this.curFontNum--
this.waitPlayTimeout.start(() => {
this._refresh()
}, -driftTime)
return
}
this.curFontNum = this._findcurFontNum(currentTime)
for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
this.curFontNum--
this._refresh()
}
play(curTime = 0) {
// console.log('play', curTime)
if (!this.fonts.length) return
this.pause()
if (this.isLineMode) return this._handlePlayLine(true)
this.isPlay = true
this._performanceTime = getNow() - curTime
this._performanceOffsetTime = 0
if (this._performanceTime < 0) {
this._performanceOffsetTime = -this._performanceTime
this._performanceTime = 0
}
this.curFontNum = this._findcurFontNum(curTime)
for (let i = this.curFontNum; i > -1; i--) {
this._handlePlayFont(this.fonts[i], 0, true)
}
for (let i = this.curFontNum, len = this.fonts.length; i < len; i++) {
let font = this.fonts[i]
font.animation.cancel()
font.dom.style.backgroundSize = '0 100%'
}
this.curFontNum--
this._refresh()
}
pause() {
if (!this.isPlay) return
this.isPlay = false
this.timeoutTools.clear()
this.waitPlayTimeout.clear()
this._handlePauseFont(this.fonts[this.curFontNum])
if (this.curFontNum === this.maxLine) return
const curFontNum = this._findcurFontNum(this._currentTime())
if (this.curFontNum === curFontNum) return
for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
}
finish() {
this.pause()
if (this.isLineMode) return this._handlePlayLine(true)
for (const font of this.fonts) {
font.animation.cancel()
font.dom.style.backgroundSize = '100% 100%'
}
this.curFontNum = this.maxFontNum
}
reset() {
this.pause()
if (this.isLineMode) return this._handlePlayLine(false)
for (const font of this.fonts) {
font.animation.cancel()
font.dom.style.backgroundSize = '0 100%'
}
this.curFontNum = 0
}
}

View File

@ -0,0 +1,166 @@
const LinePlayer = require('./line-player')
const FontPlayer = require('./font-player')
const fontTimeExp = /<(\d+),(\d+)>/g
module.exports = class Lyric {
constructor({
lyric = '',
translationLyric = '',
offset = 150,
lineClassName = '',
fontClassName = 'font',
translationClassName = 'translation',
activeLineClassName = 'active',
lineModeClassName = 'line',
shadowClassName = '',
shadowContent = false,
onPlay = function() { },
onSetLyric = function() { },
}) {
this.lyric = lyric
this.translationLyric = translationLyric
this.offset = offset
this.onPlay = onPlay
this.onSetLyric = onSetLyric
this.lineClassName = lineClassName
this.fontClassName = fontClassName
this.translationClassName = translationClassName
this.activeLineClassName = activeLineClassName
this.lineModeClassName = lineModeClassName
this.shadowClassName = shadowClassName
this.shadowContent = shadowContent
this.playingLineNum = -1
this.isLineMode = false
}
_init() {
this.playingLineNum = -1
this.isLineMode = false
if (this.linePlayer) {
this.linePlayer.setLyric(this.lyric, this.translationLyric)
} else {
this.linePlayer = new LinePlayer({
lyric: this.lyric,
translationLyric: this.translationLyric,
offset: this.offset,
onPlay: this._handleLinePlayerOnPlay,
onSetLyric: this._handleLinePlayerOnSetLyric,
})
}
}
_handleLinePlayerOnPlay = (num, text, curTime) => {
if (this.isLineMode) {
if (num < this.playingLineNum + 1) {
for (let i = this.playingLineNum; i > num - 1; i--) {
const font = this._lineFonts[i]
font.reset()
font.lineContent.classList.remove(this.activeLineClassName)
}
} else if (num > this.playingLineNum + 1) {
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
const font = this._lineFonts[i]
font.reset()
font.lineContent.classList.remove(this.activeLineClassName)
}
} else if (this.playingLineNum > -1) {
const font = this._lineFonts[this.playingLineNum]
font.reset()
font.lineContent.classList.remove(this.activeLineClassName)
}
} else {
if (num < this.playingLineNum + 1) {
for (let i = this.playingLineNum; i > num - 1; i--) {
const font = this._lineFonts[i]
font.lineContent.classList.remove(this.activeLineClassName)
font.reset()
}
} else if (num > this.playingLineNum + 1) {
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
const font = this._lineFonts[i]
font.lineContent.classList.remove(this.activeLineClassName)
font.finish()
}
} else if (this.playingLineNum > -1) {
const font = this._lineFonts[this.playingLineNum]
font.lineContent.classList.remove(this.activeLineClassName)
}
}
this.playingLineNum = num
const font = this._lineFonts[num]
font.lineContent.classList.add(this.activeLineClassName)
font.play(curTime - this._lines[num].time)
this.onPlay(num, this._lines[num].text)
}
_handleLinePlayerOnSetLyric = lyricLines => {
// console.log(lyricLines)
// this._lines = lyricsLines
this.isLineMode = lyricLines.length && !/^<\d+,\d+>/.test(lyricLines[0].text)
this._lineFonts = []
if (this.isLineMode) {
this._lines = lyricLines.map(line => {
const fontPlayer = new FontPlayer({
lyric: line.text,
translationLyric: line.translation,
lineClassName: this.lineClassName,
fontClassName: this.fontClassName,
translationClassName: this.translationClassName,
lineModeClassName: this.lineModeClassName,
shadowClassName: this.shadowClassName,
shadowContent: this.shadowContent,
})
this._lineFonts.push(fontPlayer)
return {
text: line.text,
time: line.time,
dom_line: fontPlayer.lineContent,
}
})
} else {
this._lines = lyricLines.map(line => {
const fontPlayer = new FontPlayer({
lyric: line.text,
translationLyric: line.translation,
lineClassName: this.lineClassName,
fontClassName: this.fontClassName,
translationClassName: this.translationClassName,
shadowClassName: this.shadowClassName,
shadowContent: this.shadowContent,
})
this._lineFonts.push(fontPlayer)
return {
text: line.text.replace(fontTimeExp, ''),
time: line.time,
dom_line: fontPlayer.lineContent,
}
})
}
this.onSetLyric(this._lines)
}
play(curTime) {
if (!this.linePlayer) return
this.linePlayer.play(curTime)
}
pause() {
if (!this.linePlayer) return
this.linePlayer.pause()
if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum].pause()
}
setLyric(lyric, translationLyric) {
this.lyric = lyric
this.translationLyric = translationLyric
this._init()
}
}

View File

@ -0,0 +1,177 @@
const { getNow, TimeoutTools } = require('./utils')
const timeExp = /^\[([\d:.]*)\]{1}/g
const tagRegMap = {
title: 'ti',
artist: 'ar',
album: 'al',
offset: 'offset',
by: 'by',
}
const timeoutTools = new TimeoutTools()
module.exports = class LinePlayer {
constructor({ lyric = '', translationLyric = '', offset = 0, onPlay = function() { }, onSetLyric = function() { } } = {}) {
this.lyric = lyric
this.translationLyric = translationLyric
this.tags = {}
this.lines = null
this.translationLines = null
this.onPlay = onPlay
this.onSetLyric = onSetLyric
this.isPlay = false
this.curLineNum = 0
this.maxLine = 0
this.offset = offset
this.isOffseted = false
this._performanceTime = 0
this._performanceOffsetTime = 0
this._init()
}
_init() {
if (this.lyric == null) this.lyric = ''
if (this.translationLyric == null) this.translationLyric = ''
this._initTag()
this._initLines()
this.onSetLyric(this.lines)
}
_initTag() {
for (let tag in tagRegMap) {
const matches = this.lyric.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i'))
this.tags[tag] = (matches && matches[1]) || ''
}
}
_initLines() {
this.lines = []
this.translationLines = []
const lines = this.lyric.split('\n')
const linesMap = {}
// const translationLines = this.translationLyric.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
let result = timeExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
if (text) {
const timeStr = RegExp.$1
const timeArr = timeStr.split(':')
if (timeArr.length < 3) timeArr.unshift(0)
if (timeArr[2].indexOf('.') > -1) {
timeArr.push(...timeArr[2].split('.'))
timeArr.splice(2, 1)
}
linesMap[timeStr] = {
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
text,
}
}
}
}
const translationLines = this.translationLyric.split('\n')
for (let i = 0; i < translationLines.length; i++) {
const line = translationLines[i].trim()
let result = timeExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
if (text) {
const timeStr = RegExp.$1
const targetLine = linesMap[timeStr]
if (targetLine) targetLine.translation = text
}
}
}
this.lines = Object.values(linesMap)
this.lines.sort((a, b) => {
return a.time - b.time
})
this.maxLine = this.lines.length - 1
}
_currentTime() {
return getNow() - this._performanceTime + this._performanceOffsetTime
}
_findCurLineNum(curTime) {
const length = this.lines.length
for (let index = 0; index < length; index++) if (curTime <= this.lines[index].time) return index === 0 ? 0 : index - 1
return length - 1
}
_handleMaxLine() {
this.onPlay(this.curLineNum, this.lines[this.curLineNum].text, this._currentTime())
this.pause()
}
_refresh() {
this.curLineNum++
// console.log('curLineNum time', this.lines[this.curLineNum].time)
let curLine = this.lines[this.curLineNum]
let nextLine = this.lines[this.curLineNum + 1]
const currentTime = this._currentTime()
const driftTime = currentTime - curLine.time
if (driftTime >= 0 || this.curLineNum === 0) {
if (this.curLineNum === this.maxLine) return this._handleMaxLine()
this.delay = nextLine.time - curLine.time - driftTime
if (this.delay > 0) {
if (!this.isOffseted && this.delay >= this.offset) {
this._performanceOffsetTime += this.offset
this.delay -= this.offset
this.isOffseted = true
}
timeoutTools.start(() => {
this._refresh()
}, this.delay)
this.onPlay(this.curLineNum, curLine.text, currentTime)
return
}
}
this.curLineNum = this._findCurLineNum(currentTime) - 1
this._refresh()
}
play(curTime = 0) {
if (!this.lines.length) return
this.pause()
this.isPlay = true
this._performanceOffsetTime = 0
this._performanceTime = getNow() - curTime
if (this._performanceTime < 0) {
this._performanceOffsetTime = -this._performanceTime
this._performanceTime = 0
}
this.curLineNum = this._findCurLineNum(curTime) - 1
this._refresh()
}
pause() {
if (!this.isPlay) return
this.isPlay = false
this.isOffseted = false
timeoutTools.clear()
if (this.curLineNum === this.maxLine) return
const currentTime = this._currentTime()
const curLineNum = this._findCurLineNum(currentTime)
if (this.curLineNum !== curLineNum) {
this.curLineNum = curLineNum
this.onPlay(curLineNum, this.lines[curLineNum].text, currentTime)
}
}
setLyric(lyric, translationLyric) {
// console.log(translationLyric)
if (this.isPlay) this.pause()
this.lyric = lyric
this.translationLyric = translationLyric
this._init()
}
}

View File

@ -0,0 +1,50 @@
const getNow = exports.getNow = typeof performance == 'object' && window.performance.now ? window.performance.now.bind(window.performance) : Date.now.bind(Date)
exports.TimeoutTools = class TimeoutTools {
constructor() {
this.invokeTime = 0
this.animationFrameId = null
this.timeoutId = null
this.callback = null
this.thresholdTime = 200
}
run() {
this.animationFrameId = window.requestAnimationFrame(() => {
this.animationFrameId = null
let diff = this.invokeTime - getNow()
// console.log('diff', diff)
if (diff > 0) {
if (diff < this.thresholdTime) return this.run()
return this.timeoutId = setTimeout(() => {
this.timeoutId = null
this.run()
}, diff - this.thresholdTime)
}
// console.log('diff', diff)
this.callback(diff)
})
}
start(callback = () => {}, timeout = 0) {
// console.log(timeout)
this.callback = callback
this.invokeTime = getNow() + timeout
this.run()
}
clear() {
if (this.animationFrameId) {
window.cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
if (this.timeoutId) {
window.clearTimeout(this.timeoutId)
this.timeoutId = null
}
}
}

View File

@ -11,7 +11,6 @@ module.exports = [
tx: ['128k'], tx: ['128k'],
wy: ['128k'], wy: ['128k'],
mg: ['128k'], mg: ['128k'],
xm: ['128k'],
// bd: ['128k'], // bd: ['128k'],
}, },
}, },

View File

@ -13,6 +13,7 @@ for (const api of apiSourceInfo) {
const getAPI = source => apiList[`${window.globalObj.apiSource}_api_${source}`] const getAPI = source => apiList[`${window.globalObj.apiSource}_api_${source}`]
const apis = source => { const apis = source => {
if (/^user_api/.test(window.globalObj.apiSource)) return window.globalObj.userApi.apis[source]
let api = getAPI(source) let api = getAPI(source)
if (api) return api if (api) return api
throw new Error('Api is not found') throw new Error('Api is not found')

View File

@ -50,10 +50,12 @@ const sources = {
export default { export default {
...sources, ...sources,
init() { init() {
const tasks = []
for (let source of sources.sources) { for (let source of sources.sources) {
let sm = sources[source.id] let sm = sources[source.id]
sm && sm.init && sm.init() sm && sm.init && tasks.push(sm.init())
} }
return Promise.all(tasks)
}, },
supportQuality, supportQuality,
@ -74,26 +76,31 @@ export default {
return tempResult return tempResult
} }
const trimStr = str => typeof str == 'string' ? str.trim() : str const trimStr = str => typeof str == 'string' ? str.trim() : str
const sortedSinger = sortSingle(musicInfo.singer) const sortedSinger = String(sortSingle(musicInfo.singer)).toLowerCase()
const musicName = trimStr(musicInfo.name) const musicName = trimStr(musicInfo.name)
const lowerCaseName = String(musicName).toLowerCase()
const lowerCaseAlbumName = String(musicInfo.albumName).toLowerCase()
for (const source of sources.sources) { for (const source of sources.sources) {
if (!sources[source.id].musicSearch || source.id === musicInfo.source || source.id === 'xm') continue if (!sources[source.id].musicSearch || source.id === musicInfo.source || source.id === 'xm') continue
tasks.push(sources[source.id].musicSearch.search(`${musicName} ${musicInfo.singer || ''}`.trim(), 1, { limit: 10 }).then(res => { tasks.push(sources[source.id].musicSearch.search(`${musicName} ${musicInfo.singer || ''}`.trim(), 1, { limit: 10 }).then(res => {
for (const item of res.list) { for (const item of res.list) {
item.sortedSinger = sortSingle(item.singer) item.sortedSinger = String(sortSingle(item.singer)).toLowerCase()
item.name = trimStr(item.name) item.name = trimStr(item.name)
item.lowerCaseName = String(item.name).toLowerCase()
item.lowerCaseAlbumName = String(item.albumName).toLowerCase()
// console.log(lowerCaseName, item.lowerCaseName)
if ( if (
( (
item.sortedSinger === sortedSinger && item.sortedSinger === sortedSinger &&
(item.name === musicName || item.interval === musicInfo.interval) (item.lowerCaseName === lowerCaseName || item.interval === musicInfo.interval)
) || ) ||
( (
item.interval === musicInfo.interval && item.name === musicName && item.interval === musicInfo.interval && item.lowerCaseName === lowerCaseName &&
(item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger)) (item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
) || ) ||
( (
item.name === musicName && item.albumName === musicInfo.albumName && item.lowerCaseName === lowerCaseName && item.lowerCaseAlbumName === lowerCaseAlbumName &&
item.interval === musicInfo.interval item.interval === musicInfo.interval
) )
) { ) {
@ -106,12 +113,13 @@ export default {
const result = (await Promise.all(tasks)).filter(s => s) const result = (await Promise.all(tasks)).filter(s => s)
const newResult = [] const newResult = []
if (result.length) { if (result.length) {
newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.name === musicName && item.interval === musicInfo.interval)) newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.lowerCaseName === lowerCaseName && item.interval === musicInfo.interval))
newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.interval === musicInfo.interval)) newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.interval === musicInfo.interval))
newResult.push(...sortMusic(result, item => item.name === musicName && item.sortedSinger === sortedSinger && item.albumName === musicInfo.albumName)) newResult.push(...sortMusic(result, item => item.lowerCaseName === lowerCaseName && item.sortedSinger === sortedSinger && item.lowerCaseAlbumName === lowerCaseAlbumName))
newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.name === musicName)) newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.lowerCaseName === lowerCaseName))
for (const item of result) { for (const item of result) {
delete item.sortedSinger delete item.sortedSinger
delete item.lowerCaseName
} }
newResult.push(...result) newResult.push(...result)
} }

View File

@ -26,7 +26,7 @@ const kg = {
return pic.getPic(songInfo) return pic.getPic(songInfo)
}, },
getMusicDetailPageUrl(songInfo) { getMusicDetailPageUrl(songInfo) {
return `https://www.kugou.com/song/#hash=${songInfo.hash}` return `https://www.kugou.com/song/#hash=${songInfo.hash}&album_id=${songInfo.albumId}`
}, },
// getPic(songInfo) { // getPic(songInfo) {
// return apis('kg').getPic(songInfo) // return apis('kg').getPic(songInfo)

View File

@ -2,10 +2,13 @@ import { httpFetch } from '../../request'
import { decodeLyric } from './util' import { decodeLyric } from './util'
import { decodeName } from '../..' import { decodeName } from '../..'
const headExp = /^.*\[id:\$\w+\]\n/
const parseLyric = str => { const parseLyric = str => {
str = str.replace(/(?:<\d+,\d+,\d+>|\r)/g, '') str = str.replace(/\r/g, '')
if (str.startsWith('\ufeff[id:$00000000]')) str = str.replace('\ufeff[id:$00000000]\n', '') if (headExp.test(str)) str = str.replace(headExp, '')
let trans = str.match(/\[language:([\w=\\/+]+)\]/) let trans = str.match(/\[language:([\w=\\/+]+)\]/)
let lyric
let tlyric let tlyric
if (trans) { if (trans) {
str = str.replace(/\[language:[\w=\\/+]+\]\n/, '') str = str.replace(/\[language:[\w=\\/+]+\]\n/, '')
@ -18,7 +21,7 @@ const parseLyric = str => {
} }
} }
let i = 0 let i = 0
let lyric = str.replace(/\[((\d+),\d+)\].*/g, str => { let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
let result = str.match(/\[((\d+),\d+)\].*/) let result = str.match(/\[((\d+),\d+)\].*/)
let time = parseInt(result[2]) let time = parseInt(result[2])
let ms = time % 1000 let ms = time % 1000
@ -31,11 +34,14 @@ const parseLyric = str => {
return str.replace(result[1], time) return str.replace(result[1], time)
}) })
tlyric = tlyric ? tlyric.join('\n') : '' tlyric = tlyric ? tlyric.join('\n') : ''
lyric = decodeName(lyric) lxlyric = lxlyric.replace(/<(\d+,\d+),\d+>/g, '<$1>')
lxlyric = decodeName(lxlyric)
lyric = lxlyric.replace(/<\d+,\d+>/g, '')
tlyric = decodeName(tlyric) tlyric = decodeName(tlyric)
return { return {
lyric, lyric,
tlyric, tlyric,
lxlyric,
} }
} }
@ -119,7 +125,7 @@ export default {
let requestObj = this.searchLyric(songInfo.name, songInfo.hash, songInfo._interval || this.getIntv(songInfo.interval)) let requestObj = this.searchLyric(songInfo.name, songInfo.hash, songInfo._interval || this.getIntv(songInfo.interval))
requestObj.promise = requestObj.promise.then(result => { requestObj.promise = requestObj.promise.then(result => {
if (!result) return { lyric: '', tlyric: '' } if (!result) return { lyric: null, tlyric: null, lxlyric: null }
let requestObj2 = this.getLyricDownload(result.id, result.accessKey) let requestObj2 = this.getLyricDownload(result.id, result.accessKey)

View File

@ -16,56 +16,67 @@ export default {
searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`) searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`)
return searchRequest.promise.then(({ body }) => body) return searchRequest.promise.then(({ body }) => body)
}, },
filterData(rawData) {
const types = []
const _types = {}
if (rawData.filesize !== 0) {
let size = sizeFormate(rawData.filesize)
types.push({ type: '128k', size, hash: rawData.hash })
_types['128k'] = {
size,
hash: rawData.hash,
}
}
if (rawData['320filesize'] !== 0) {
let size = sizeFormate(rawData['320filesize'])
types.push({ type: '320k', size, hash: rawData['320hash'] })
_types['320k'] = {
size,
hash: rawData['320hash'],
}
}
if (rawData.sqfilesize !== 0) {
let size = sizeFormate(rawData.sqfilesize)
types.push({ type: 'flac', size, hash: rawData.sqhash })
_types.flac = {
size,
hash: rawData.sqhash,
}
}
return {
singer: decodeName(rawData.singername),
name: decodeName(rawData.songname),
albumName: decodeName(rawData.album_name),
albumId: rawData.album_id,
songmid: rawData.hash,
source: 'kg',
interval: formatPlayTime(rawData.duration),
_interval: rawData.duration,
img: null,
lrc: null,
otherSource: null,
hash: rawData.hash,
types,
_types,
audioId: rawData.audio_id + '_' + rawData.hash,
typeUrl: {},
}
},
handleResult(rawData) { handleResult(rawData) {
// console.log(rawData) // console.log(rawData)
let ids = new Set() let ids = new Set()
const list = [] const list = []
rawData.forEach(item => { rawData.forEach(item => {
if (ids.has(item.audio_id)) return const key = item.audio_id + item.hash
ids.add(item.audio_id) if (ids.has(key)) return
const types = [] ids.add(key)
const _types = {} list.push(this.filterData(item))
if (item.filesize !== 0) { for (const childItem of item.group) {
let size = sizeFormate(item.filesize) const key = item.audio_id + item.hash
types.push({ type: '128k', size, hash: item.hash }) if (ids.has(key)) return
_types['128k'] = { ids.add(key)
size, list.push(this.filterData(childItem))
hash: item.hash,
}
} }
if (item['320filesize'] !== 0) {
let size = sizeFormate(item['320filesize'])
types.push({ type: '320k', size, hash: item['320hash'] })
_types['320k'] = {
size,
hash: item['320hash'],
}
}
if (item.sqfilesize !== 0) {
let size = sizeFormate(item.sqfilesize)
types.push({ type: 'flac', size, hash: item.sqhash })
_types.flac = {
size,
hash: item.sqhash,
}
}
list.push({
singer: decodeName(item.singername),
name: decodeName(item.songname),
albumName: decodeName(item.album_name),
albumId: item.album_id,
songmid: item.audio_id,
source: 'kg',
interval: formatPlayTime(item.duration),
_interval: item.duration,
img: null,
lrc: null,
otherSource: null,
hash: item.hash,
types,
_types,
typeUrl: {},
})
}) })
return list return list
}, },

View File

@ -37,7 +37,9 @@ export default {
requestObj.promise = requestObj.promise.then(({ body }) => { requestObj.promise = requestObj.promise.then(({ body }) => {
if (body.error_code !== 0) return Promise.reject('图片获取失败') if (body.error_code !== 0) return Promise.reject('图片获取失败')
let info = body.data[0].info let info = body.data[0].info
return info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image const img = info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image
if (!img) return Promise.reject('Pic get failed')
return img
}) })
return requestObj return requestObj
}, },

View File

@ -106,7 +106,7 @@ const kw = {
}, },
init() { init() {
getToken() return getToken()
}, },
} }

View File

@ -48,10 +48,12 @@ export default {
return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/user/queryMusicListSongs.do?musicListId=${id}&pageNo=${page}&pageSize=${this.limit_song}` return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/user/queryMusicListSongs.do?musicListId=${id}&pageNo=${page}&pageSize=${this.limit_song}`
}, },
defaultHeaders: { defaultHeaders: {
language: 'Chinese', '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',
ua: 'Android_migu', Referer: 'https://m.music.migu.cn/',
mode: 'android', // language: 'Chinese',
version: '6.8.5', // ua: 'Android_migu',
// mode: 'android',
// version: '6.8.5',
}, },
/** /**
@ -155,6 +157,7 @@ export default {
if (this._requestObj_list) this._requestObj_list.cancelHttp() if (this._requestObj_list) this._requestObj_list.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num')) if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_list = httpFetch(this.getSongListUrl(sortId, tagId, page), { this._requestObj_list = httpFetch(this.getSongListUrl(sortId, tagId, page), {
headers: this.defaultHeaders,
// headers: { // headers: {
// sign: 'c3b7ae985e2206e97f1b2de8f88691e2', // sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
// timestamp: 1578225871982, // timestamp: 1578225871982,
@ -186,6 +189,7 @@ export default {
// }) // })
// }) // })
return this._requestObj_list.promise.then(({ body }) => { return this._requestObj_list.promise.then(({ body }) => {
// console.log(body)
if (body.retCode !== '100000' || body.retMsg.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) if (body.retCode !== '100000' || body.retMsg.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)
return { return {
list: this.filterList(body.retMsg.playlist), list: this.filterList(body.retMsg.playlist),

View File

@ -20,7 +20,9 @@ const wy = {
return getLyric(songInfo.songmid) return getLyric(songInfo.songmid)
}, },
getPic(songInfo) { getPic(songInfo) {
return getMusicInfo(songInfo.songmid).then(info => info.al.picUrl) const requestObj = getMusicInfo(songInfo.songmid)
requestObj.promise = requestObj.promise.then(info => info.al.picUrl)
return requestObj
}, },
getMusicDetailPageUrl(songInfo) { getMusicDetailPageUrl(songInfo) {
return `https://music.163.com/#/song?id=${songInfo.songmid}` return `https://music.163.com/#/song?id=${songInfo.songmid}`

View File

@ -0,0 +1,57 @@
// import { apis } from '../api-source'
// import leaderboard from './leaderboard'
// import songList from './songList'
// import musicSearch from './musicSearch'
// import pic from './pic'
// import lyric from './lyric'
// import hotSearch from './hotSearch'
// import comment from './comment'
// import musicInfo from './musicInfo'
// import { closeVerifyModal } from './util'
const xm = {
// songList,
// musicSearch,
// leaderboard,
// hotSearch,
// closeVerifyModal,
comment: {
getComment() {
return Promise.reject(new Error('fail'))
},
getHotComment() {
return Promise.reject(new Error('fail'))
},
},
getMusicUrl(songInfo, type) {
return {
promise: Promise.reject(new Error('fail')),
}
// return apis('xm').getMusicUrl(songInfo, type)
},
getLyric(songInfo) {
return {
promise: Promise.reject(new Error('fail')),
}
// return lyric.getLyric(songInfo)
},
getPic(songInfo) {
return {
promise: Promise.reject(new Error('fail')),
}
// return pic.getPic(songInfo)
},
// getMusicDetailPageUrl(songInfo) {
// if (songInfo.songStringId) return `https://www.xiami.com/song/${songInfo.songStringId}`
// musicInfo.getMusicInfo(songInfo).then(({ data }) => {
// songInfo.songStringId = data.songStringId
// })
// return `https://www.xiami.com/song/${songInfo.songmid}`
// },
// init() {
// getToken()
// },
}
export default xm

View File

@ -1,20 +0,0 @@
import { httpFetch } from '../../request'
import { requestMsg } from '../../message'
import { headers, timeout } from '../options'
const api_test = {
getMusicUrl(songInfo, type) {
const requestObj = httpFetch(`http://ts.tempmusic.tk/url/xm/${songInfo.songmid}/${type}`, {
method: 'get',
timeout,
headers,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail))
})
return requestObj
},
}
export default api_test

View File

@ -1,47 +0,0 @@
import { xmRequest } from './util'
import { dateFormat2 } from '../../'
export default {
_requestObj: null,
_requestObj2: null,
async getComment({ songmid }, page = 1, limit = 20) {
if (this._requestObj) this._requestObj.cancelHttp()
const _requestObj = xmRequest('/api/comment/getCommentList', { objectId: songmid, objectType: 'song', pagingVO: { page, pageSize: limit } })
const { body, statusCode } = await _requestObj.promise
// console.log(body)
if (statusCode != 200 || body.code !== 'SUCCESS') throw new Error('获取评论失败')
return { source: 'xm', comments: this.filterComment(body.result.data.commentList), total: body.result.data.pagingVO.count, page, limit, maxPage: Math.ceil(body.result.data.pagingVO.count / limit) || 1 }
},
async getHotComment({ songmid }, page = 1, limit = 100) {
if (this._requestObj2) this._requestObj2.cancelHttp()
if (!songmid) throw new Error('获取失败')
const _requestObj2 = xmRequest('/api/comment/getHotCommentList', { objectId: songmid, objectType: 'song', pagingVO: { page, pageSize: limit } })
const { body, statusCode } = await _requestObj2.promise
// console.log(body)
if (statusCode != 200 || body.code !== 'SUCCESS') throw new Error('获取热门评论失败')
return { source: 'xm', comments: this.filterComment(body.result.data.hotList) }
},
filterComment(rawList) {
return rawList.map(item => ({
id: item.commentId,
text: item.message.split('\n').filter(t => !!t),
time: item.gmtCreate,
timeStr: dateFormat2(item.gmtCreate),
userName: item.nickName,
avatar: item.avatar,
userId: item.userId,
likedCount: item.likes,
reply: item.replyData ? item.replyData.map(c => ({
id: c.commentId,
text: c.message.split('\n').filter(t => !!t),
time: c.gmtCreate,
timeStr: dateFormat2(c.gmtCreate),
userName: c.nickName,
avatar: c.avatar,
userId: c.userId,
likedCount: c.likes,
})) : [],
}))
},
}

View File

@ -1,20 +0,0 @@
// import { xmRequest } from './util'
export default {
_requestObj: null,
async getList(retryNum = 0) {
// if (this._requestObj) this._requestObj.cancelHttp()
// if (retryNum > 2) return Promise.reject(new Error('try max num'))
// const _requestObj = xmRequest('/api/search/getHotSearchWords')
// const { body, statusCode } = await _requestObj.promise
// // console.log(body)
// if (statusCode != 200 || body.code !== 'SUCCESS') return this.getList(++retryNum)
// // console.log(body, statusCode)
// return { source: 'xm', list: this.filterList(body.result.data.hotWords) }
return { source: 'xm', list: [] }
},
filterList(rawList) {
return rawList.map(item => item.word)
},
}

View File

@ -1,42 +0,0 @@
import { apis } from '../api-source'
import leaderboard from './leaderboard'
import songList from './songList'
import musicSearch from './musicSearch'
// import pic from './pic'
import lyric from './lyric'
import hotSearch from './hotSearch'
import comment from './comment'
import musicInfo from './musicInfo'
import { closeVerifyModal } from './util'
const xm = {
songList,
musicSearch,
leaderboard,
hotSearch,
closeVerifyModal,
comment,
getMusicUrl(songInfo, type) {
return apis('xm').getMusicUrl(songInfo, type)
},
getLyric(songInfo) {
return lyric.getLyric(songInfo)
},
getPic(songInfo) {
return Promise.reject(new Error('fail'))
// return pic.getPic(songInfo)
},
getMusicDetailPageUrl(songInfo) {
if (songInfo.songStringId) return `https://www.xiami.com/song/${songInfo.songStringId}`
musicInfo.getMusicInfo(songInfo).then(({ data }) => {
songInfo.songStringId = data.songStringId
})
return `https://www.xiami.com/song/${songInfo.songmid}`
},
// init() {
// getToken()
// },
}
export default xm

View File

@ -1,211 +0,0 @@
import { xmRequest } from './util'
import { formatPlayTime, sizeFormate } from '../../index'
// import jshtmlencode from 'js-htmlencode'
let boardList = [{ id: 'xm__102', name: '新歌榜', bangid: '102' }, { id: 'xm__103', name: '热歌榜', bangid: '103' }, { id: 'xm__104', name: '原创榜', bangid: '104' }, { id: 'xm__306', name: 'K歌榜', bangid: '306' }, { id: 'xm__332', name: '抖音热歌榜', bangid: '332' }, { id: 'xm__305', name: '歌单收录榜', bangid: '305' }, { id: 'xm__327', name: '趴间热歌榜', bangid: '327' }, { id: 'xm__324', name: '影视原声榜', bangid: '324' }, { id: 'xm__204', name: '美国Billboard单曲榜', bangid: '204' }, { id: 'xm__206', name: '韩国MNET音乐排行榜', bangid: '206' }, { id: 'xm__201', name: 'Hito 中文排行榜', bangid: '201' }, { id: 'xm__203', name: '英国UK单曲榜', bangid: '203' }, { id: 'xm__205', name: 'oricon公信单曲榜', bangid: '205' }, { id: 'xm__328', name: '美国iTunes榜', bangid: '328' }, { id: 'xm__329', name: 'Beatport电音榜', bangid: '329' }, { id: 'xm__330', name: '香港商业电台榜', bangid: '330' }]
export default {
limit: 200,
list: [
{
id: 'xmrgb',
name: '热歌榜',
bangid: '103',
},
{
id: 'xmxgb',
name: '新歌榜',
bangid: '102',
},
{
id: 'xmrcb',
name: '原创榜',
bangid: '104',
},
{
id: 'xmdyb',
name: '抖音榜',
bangid: '332',
},
{
id: 'xmkgb',
name: 'K歌榜',
bangid: '306',
},
{
id: 'xmfxb',
name: '分享榜',
bangid: '307',
},
{
id: 'xmrdtlb',
name: '讨论榜',
bangid: '331',
},
{
id: 'xmgdslb',
name: '歌单榜',
bangid: '305',
},
{
id: 'xmpjrgb',
name: '趴间榜',
bangid: '327',
},
{
id: 'xmysysb',
name: '影视榜',
bangid: '324',
},
],
requestBoardsObj: null,
requestObj: null,
getBoardsData() {
if (this.requestBoardsObj) this.requestBoardsObj.cancelHttp()
this.requestBoardsObj = xmRequest('/api/billboard/getBillboards')
return this.requestBoardsObj.promise
},
getData(id) {
if (this.requestObj) this.requestObj.cancelHttp()
this.requestObj = xmRequest('/api/billboard/getBillboardDetail', { billboardId: id })
return this.requestObj.promise
},
getSinger(singers) {
let arr = []
singers.forEach(singer => {
arr.push(singer.artistName)
})
return arr.join('、')
},
filterData(rawList) {
// console.log(rawList)
let ids = new Set()
const list = []
rawList.forEach(songData => {
if (!songData) return
if (ids.has(songData.songId)) return
ids.add(songData.songId)
const types = []
const _types = {}
let size = null
for (const item of songData.purviewRoleVOs) {
if (!item.filesize) continue
size = sizeFormate(item.filesize)
switch (item.quality) {
case 's':
types.push({ type: 'wav', size })
_types.wav = {
size,
}
break
case 'h':
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
break
case 'l':
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
break
}
}
types.reverse()
list.push({
singer: this.getSinger(songData.singerVOs),
name: songData.songName,
albumName: songData.albumName,
albumId: songData.albumId,
source: 'xm',
interval: formatPlayTime(parseInt(songData.length / 1000)),
songmid: songData.songId,
img: songData.albumLogo || songData.albumLogoS,
songStringId: songData.songStringId,
lrc: null,
lrcUrl: songData.lyricInfo && songData.lyricInfo.lyricFile,
otherSource: null,
types,
_types,
typeUrl: {},
})
})
return list
},
filterBoardsData(rawList) {
// console.log(rawList)
let list = []
if (rawList.xiamiBillboards) {
for (const board of rawList.xiamiBillboards) {
if (board.itemType != 1) continue
list.push({
id: 'xm__' + board.billboardId,
name: board.name,
bangid: String(board.billboardId),
})
}
}
if (rawList.spBillboards) {
for (const board of rawList.spBillboards) {
if (board.itemType != 1) continue
list.push({
id: 'xm__' + board.billboardId,
name: board.name,
bangid: String(board.billboardId),
})
}
}
if (rawList.globalBillboards) {
for (const board of rawList.globalBillboards) {
if (board.itemType != 1) continue
list.push({
id: 'xm__' + board.billboardId,
name: board.name,
bangid: String(board.billboardId),
})
}
}
return list
},
async getBoards(retryNum = 0) {
// if (++retryNum > 3) return Promise.reject(new Error('try max num'))
// let response
// try {
// response = await this.getBoardsData()
// } catch (error) {
// return this.getBoards(retryNum)
// }
// if (response.statusCode !== 200 || response.body.code !== 'SUCCESS') return this.getBoards(retryNum)
// const list = this.filterBoardsData(response.body.result.data)
// this.list = list
// return {
// list,
// source: 'xm',
// }
this.list = boardList
return {
list: boardList,
source: 'xm',
}
},
getList(bangid, page, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
return this.getData(bangid).then(({ statusCode, body }) => {
if (statusCode !== 200 || body.code !== 'SUCCESS') return this.getList(bangid, page, retryNum)
// console.log(body)
const list = this.filterData(body.result.data.billboard.songs)
return {
total: parseInt(body.result.data.billboard.attributeMap.item_size),
list,
limit: this.limit,
page,
source: 'xm',
}
})
},
}

View File

@ -1,107 +0,0 @@
import { httpGet, httpFetch } from '../../request'
import { xmRequest } from './util'
const parseLyric = str => {
str = str.replace(/(?:<\d+>|\r)/g, '')
let tlyric = []
let lyric = str.replace(/\[[\d:.]+\].*?\n\[x-trans\].*/g, s => {
// console.log(s)
let [lrc, tlrc] = s.split('\n')
tlrc = tlrc.replace('[x-trans]', lrc.replace(/^(\[[\d:.]+\]).*$/, '$1'))
tlyric.push(tlrc)
return lrc
})
tlyric = tlyric.join('\n')
return {
lyric,
tlyric,
}
}
export default {
failTime: 0,
expireTime: 60 * 1000 * 1000,
getLyricFile_1(url, retryNum = 0) {
if (retryNum > 5) return Promise.reject('歌词获取失败')
let requestObj = httpFetch(url)
requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {
if (statusCode !== 200) {
let tryRequestObj = this.getLyric(url, ++retryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise
}
return url.endsWith('.xtrc') ? parseLyric(body) : {
lyric: body,
tlyric: '',
}
})
return requestObj
},
getLyricFile_2(url, retryNum = 0) {
if (retryNum > 5) return Promise.reject('歌词获取失败')
return new Promise((resolve, reject) => {
httpGet(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
referer: 'https://www.xiami.com',
},
}, function(err, resp, body) {
if (err || resp.statusCode !== 200) return this.getLyricFile(url, ++retryNum).then(resolve).catch(reject)
return resolve(url.endsWith('.xtrc') ? parseLyric(body) : {
lyric: body,
tlyric: '',
})
})
})
},
getLyricUrl_1(songInfo, retryNum = 0) {
if (retryNum > 2) return Promise.reject('歌词获取失败')
let requestObj = xmRequest('/api/lyric/getSongLyrics', { songId: songInfo.songmid })
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode !== 200) {
let tryRequestObj = this.getLyricUrl_1(songInfo, ++retryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise
}
if (body.code !== 'SUCCESS') {
this.failTime = Date.now()
let tryRequestObj = this.getLyricUrl_2(songInfo)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise
}
if (!body.result.data.lyrics.length) return Promise.reject(new Error('未找到歌词'))
let lrc = body.result.data.lyrics.find(lyric => /\.(trc|lrc)$/.test(lyric.lyricUrl))
return lrc
? lrc.lyricUrl.endsWith('.trc')
? parseLyric(lrc.content)
: { lyric: lrc.content, tlyric: '' }
: Promise.reject(new Error('未找到歌词'))
})
return requestObj
},
getLyricUrl_2(songInfo, retryNum = 0) {
if (retryNum > 2) return Promise.reject('歌词获取失败')
// https://github.com/listen1/listen1_chrome_extension/blob/2587e627d23a85e490628acc0b3c9b534bc8323d/js/provider/xiami.js#L149
let requestObj = httpFetch(`https://emumo.xiami.com/song/playlist/id/${songInfo.songmid}/object_name/default/object_id/0/cat/json`, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
referer: 'https://www.xiami.com',
},
})
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode !== 200 || !body.status) {
let tryRequestObj = this.getLyricUrl_2(songInfo, ++retryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise
}
let url = body.data.trackList[0].lyric_url
if (!url) return Promise.reject(new Error('未找到歌词'))
return this.getLyricFile_2(/^http:/.test(url) ? url : ('http:' + url))
})
return requestObj
},
getLyric(songInfo) {
if (songInfo.lrcUrl && /\.(xtrc|lrc)$/.test(songInfo.lrcUrl)) return this.getLyricFile_1(songInfo.lrcUrl)
return Date.now() - this.failTime > this.expireTime ? this.getLyricUrl_1(songInfo) : this.getLyricUrl_2(songInfo)
},
}

View File

@ -1,14 +0,0 @@
import { xmRequest } from './util'
export default {
_requestObj: null,
async getMusicInfo({ songmid }, page = 1, limit = 20) {
if (this._requestObj) this._requestObj.cancelHttp()
const _requestObj = xmRequest('/api/song/initialize', { songId: songmid })
const { body, statusCode } = await _requestObj.promise
// console.log(body)
if (statusCode != 200 || body.code !== 'SUCCESS') throw new Error('获取歌曲信息失败')
return { source: 'xm', data: body.result.data.songDetail }
},
}

View File

@ -1,115 +0,0 @@
// import '../../polyfill/array.find'
// import jshtmlencode from 'js-htmlencode'
import { xmRequest } from './util'
import { formatPlayTime, sizeFormate } from '../../index'
// import { debug } from '../../utils/env'
// import { formatSinger } from './util'
// "cdcb72dc3eba41cb5bc4267f09183119_xmMain_/api/list/collect_{"pagingVO":{"page":1,"pageSize":60},"dataType":"system"}"
let searchRequest
export default {
limit: 30,
total: 0,
page: 0,
allPage: 1,
musicSearch(str, page, limit) {
if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp()
searchRequest = xmRequest('/api/search/searchSongs', {
key: str,
pagingVO: {
page: page,
pageSize: limit,
},
})
return searchRequest.promise.then(({ body }) => body)
},
getSinger(singers) {
let arr = []
singers.forEach(singer => {
arr.push(singer.artistName)
})
return arr.join('、')
},
handleResult(rawData) {
// console.log(rawData)
let ids = new Set()
const list = []
rawData.forEach(songData => {
if (!songData) return
if (ids.has(songData.songId)) return
ids.add(songData.songId)
const types = []
const _types = {}
let size = null
for (const item of songData.purviewRoleVOs) {
if (!item.filesize) continue
size = sizeFormate(item.filesize)
switch (item.quality) {
case 's':
types.push({ type: 'wav', size })
_types.wav = {
size,
}
break
case 'h':
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
break
case 'l':
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
break
}
}
types.reverse()
list.push({
singer: this.getSinger(songData.singerVOs),
name: songData.songName,
albumName: songData.albumName,
albumId: songData.albumId,
source: 'xm',
interval: formatPlayTime(parseInt(songData.length / 1000)),
songmid: songData.songId,
img: songData.albumLogo || songData.albumLogoS,
songStringId: songData.songStringId,
lrc: null,
lrcUrl: songData.lyricInfo && songData.lyricInfo.lyricFile,
otherSource: null,
types,
_types,
typeUrl: {},
})
})
return list
},
search(str, page = 1, { limit } = {}, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
if (limit == null) limit = this.limit
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
return this.musicSearch(str, page, limit).then(result => {
if (!result) return this.search(str, page, { limit }, retryNum)
if (result.code !== 'SUCCESS') return this.search(str, page, { limit }, retryNum)
// const songResultData = result.data || { songs: [], total: 0 }
let list = this.handleResult(result.result.data.songs)
if (list == null) return this.search(str, page, { limit }, retryNum)
this.total = parseInt(result.result.data.pagingVO.count)
this.page = page
this.allPage = Math.ceil(this.total / limit)
return Promise.resolve({
list,
allPage: this.allPage,
limit,
total: this.total,
source: 'xm',
})
}).catch(err => err.message.includes('canceled verify') ? Promise.reject(err) : this.search(str, page, { limit }, retryNum))
},
}

View File

@ -1,221 +0,0 @@
import { xmRequest } from './util'
import { sizeFormate, formatPlayTime } from '../../index'
export default {
_requestObj_tags: null,
_requestObj_list: null,
_requestObj_listDetail: null,
limit_list: 36,
limit_song: 100000,
successCode: 'SUCCESS',
sortList: [
{
name: '推荐',
id: 'system',
},
{
name: '精选',
id: 'recommend',
},
{
name: '最热',
id: 'hot',
},
{
name: '最新',
id: 'new',
},
],
regExps: {
// https://www.xiami.com/collect/1138092824?action=play
listDetailLink: /^.+\/collect\/(\d+)(?:\s\(.*|\?.*|&.*$|#.*$|$)/,
},
tagsUrl: '/api/collect/getRecommendTags',
songListUrl: '/api/list/collect',
songListDetailUrl: '/api/collect/initialize',
getSongListData(sortId, tagId, page) {
if (tagId == null) {
return { pagingVO: { page, pageSize: this.limit_list }, dataType: sortId }
}
switch (sortId) {
case 'system':
case 'recommend':
sortId = 'hot'
}
return { pagingVO: { page, pageSize: this.limit_list }, dataType: sortId, key: tagId }
},
/**
* 格式化播放数量
* @param {*} num
*/
formatPlayCount(num) {
if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿'
if (num > 10000) return parseInt(num / 1000) / 10 + '万'
return num
},
getSinger(singers) {
let arr = []
singers.forEach(singer => {
arr.push(singer.artistName)
})
return arr.join('、')
},
getListDetail(id, page, tryNum = 0) { // 获取歌曲列表内的音乐
if (this._requestObj_listDetail) this._requestObj_listDetail.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')
this._requestObj_listDetail = xmRequest('/api/collect/getCollectStaticUrl', { listId: id })
return this._requestObj_listDetail.promise.then(({ body }) => {
if (body.code !== this.successCode) return this.getListDetail(id, page, ++tryNum)
this._requestObj_listDetail = xmRequest(body.result.data.data.data.url)
return this._requestObj_listDetail.promise.then(({ body }) => {
if (!body.status) return this.getListDetail(id, page, ++tryNum)
// console.log(JSON.stringify(body))
return {
list: this.filterListDetail(body.resultObj.songs),
page,
limit: this.limit_song,
total: body.resultObj.songCount,
source: 'xm',
info: {
name: body.resultObj.collectName,
img: body.resultObj.collectLogo,
desc: body.resultObj.description,
author: body.resultObj.userName,
play_count: this.formatPlayCount(body.resultObj.playCount),
},
}
})
})
},
filterListDetail(rawList) {
// console.log(rawList)
let ids = new Set()
const list = []
rawList.forEach(songData => {
if (!songData) return
if (ids.has(songData.songId)) return
ids.add(songData.songId)
const types = []
const _types = {}
let size = null
for (const item of songData.purviewRoleVOs) {
if (!item.filesize) continue
size = sizeFormate(item.filesize)
switch (item.quality) {
case 's':
types.push({ type: 'wav', size })
_types.wav = {
size,
}
break
case 'h':
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
break
case 'l':
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
break
}
}
types.reverse()
list.push({
singer: this.getSinger(songData.singerVOs),
name: songData.songName,
albumName: songData.albumName,
albumId: songData.albumId,
source: 'xm',
interval: formatPlayTime(parseInt(songData.length / 1000)),
songmid: songData.songId,
songStringId: songData.songStringId,
img: songData.albumLogo || songData.albumLogoS,
lrc: null,
lrcUrl: songData.lyricInfo && songData.lyricInfo.lyricFile,
otherSource: null,
types,
_types,
typeUrl: {},
})
})
return list
},
// 获取列表数据
getList(sortId, tagId, page, tryNum = 0) {
if (this._requestObj_list) this._requestObj_list.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_list = xmRequest(this.songListUrl, this.getSongListData(sortId, tagId, page))
return this._requestObj_list.promise.then(({ body }) => {
if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)
return {
list: this.filterList(body.result.data.collects),
total: body.result.data.pagingVO.count,
page,
limit: body.result.data.pagingVO.pageSize,
source: 'xm',
}
})
},
filterList(rawData) {
return rawData.map(item => ({
play_count: this.formatPlayCount(item.playCount),
id: item.listId,
author: item.userName,
name: item.collectName,
time: null,
img: item.collectLogo,
grade: null,
desc: null,
source: 'xm',
}))
},
// 获取标签
getTag(tryNum = 0) {
if (this._requestObj_tags) this._requestObj_tags.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_tags = xmRequest(this.tagsUrl, { recommend: 1 })
return this._requestObj_tags.promise.then(({ body }) => {
if (body.code !== this.successCode) return this.getTag(++tryNum)
return this.filterTagInfo(body.result.data.recommendTags)
})
},
filterTagInfo(rawList) {
return {
hotTag: rawList[0].items.map(item => ({
id: item.name,
name: item.name,
source: 'xm',
})),
tags: rawList.slice(1).map(item => ({
name: item.title,
list: item.items.map(tag => ({
parent_id: item.title,
parent_name: item.title,
id: tag.name,
name: tag.name,
source: 'xm',
})),
})),
source: 'xm',
}
},
getTags() {
return this.getTag()
},
}
// getList
// getTags
// getListDetail

View File

@ -1,122 +0,0 @@
import { httpGet, httpFetch } from '../../request'
import { toMD5 } from '../../index'
// import crateIsg from './isg'
import { rendererInvoke, NAMES } from '../../../../common/ipc'
if (!window.xm_token) {
let data = window.localStorage.getItem('xm_token')
window.xm_token = data ? JSON.parse(data) : {
cookies: {},
cookie: null,
token: null,
isGetingToken: false,
}
window.xm_token.isGetingToken = false
}
export const formatSinger = rawData => rawData.replace(/&/g, '、')
const matchToken = headers => {
let cookies = {}
let token
for (const item of headers['set-cookie']) {
const [key, value] = item.substring(0, item.indexOf(';')).split('=')
cookies[key] = value
if (key == 'xm_sg_tk') token = value.substring(0, value.indexOf('_'))
}
// console.log(cookies)
return { token, cookies }
}
const wait = time => new Promise(resolve => setTimeout(() => resolve(), time))
const createToken = (token, path, params) => toMD5(`${token}_xmMain_${path}_${params}`)
const handleSaveToken = ({ token, cookies }) => {
Object.assign(window.xm_token.cookies, cookies)
// window.xm_token.cookies.isg = crateIsg()
window.xm_token.cookie = Object.keys(window.xm_token.cookies).map(k => `${k}=${window.xm_token.cookies[k]};`).join(' ')
if (token) window.xm_token.token = token
window.localStorage.setItem('xm_token', JSON.stringify(window.xm_token))
}
export const getToken = (path, params) => new Promise((resolve, reject) => {
if (window.xm_token.isGetingToken) return wait(1000).then(() => getToken(path, params).then(data => resolve(data)))
if (window.xm_token.token) return resolve({ token: createToken(window.xm_token.token, path, params), cookie: window.xm_token.cookie })
window.xm_token.isGetingToken = true
httpGet('https://www.xiami.com/', (err, resp) => {
window.xm_token.isGetingToken = false
if (err) return reject(err)
if (resp.statusCode != 200) return reject(new Error('获取失败'))
handleSaveToken(matchToken(resp.headers))
resolve({ token: createToken(window.xm_token.token, path, params), cookie: window.xm_token.cookie })
})
})
const baseUrl = 'https://www.xiami.com'
export const xmRequest = (path, params = '') => {
let query = params
if (params != '') {
params = JSON.stringify(params)
query = '&_q=' + encodeURIComponent(params)
}
let requestObj = {
isInited: false,
isCancelled: false,
cancelHttp() {
if (!this.isInited) this.isCancelled = true
this.requestObj.cancelHttp()
},
}
requestObj.promise = getToken(path, params).then(data => {
// console.log(data)
if (requestObj.isCancelled) return Promise.reject('取消请求')
let url = path
if (!/^http/.test(path)) url = baseUrl + path
let s = `_s=${data.token}${query}`
url += (url.includes('?') ? '&' : '?') + s
requestObj.requestObj = httpFetch(url, {
headers: {
Referer: 'https://www.xiami.com/',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
cookie: data.cookie,
},
})
return requestObj.requestObj.promise.then(resp => {
// console.log(resp.body)
if (resp.statusCode != 200) {
// console.log(resp.headers)
window.xm_token.token = null
return Promise.reject(new Error('获取失败'))
}
if (resp.body.code !== 'SUCCESS' && resp.body.rgv587_flag == 'sm') {
window.globalObj.xm.isShowVerify = true
return wait(300).then(() => {
return rendererInvoke(NAMES.mainWindow.handle_xm_verify_open, /^https:/.test(resp.body.url) ? resp.body.url : 'https:' + resp.body.url).then(x5sec => {
handleSaveToken({ cookies: { x5sec } })
// console.log(x5sec)
window.globalObj.xm.isShowVerify = false
return Promise.reject(new Error('获取成功'))
}).catch(err => {
window.globalObj.xm.isShowVerify = false
return Promise.reject(err)
})
})
}
if (resp.headers['set-cookie']) handleSaveToken(matchToken(resp.headers))
return Promise.resolve(resp)
})
})
return requestObj
}
export const closeVerifyModal = async() => {
if (!window.globalObj.xm.isShowVerify) return
await rendererInvoke(NAMES.mainWindow.handle_xm_verify_close)
window.globalObj.xm.isShowVerify = false
}

View File

@ -386,12 +386,12 @@ export default {
this.listMenu.itemMenuControl.file = true this.listMenu.itemMenuControl.file = true
this.listMenu.itemMenuControl.start = this.listMenu.itemMenuControl.start =
this.listMenu.itemMenuControl.pause = false this.listMenu.itemMenuControl.pause = false
// } else if (item.status === this.downloadStatus.ERROR || item.status === this.downloadStatus.PAUSE) { } else if (item.status === this.downloadStatus.ERROR || item.status === this.downloadStatus.PAUSE) {
// this.listMenu.itemMenuControl.play = this.listMenu.itemMenuControl.play =
// this.listMenu.itemMenuControl.playLater = this.listMenu.itemMenuControl.playLater =
// this.listMenu.itemMenuControl.pause = this.listMenu.itemMenuControl.pause =
// this.listMenu.itemMenuControl.file = false this.listMenu.itemMenuControl.file = false
// this.listMenu.itemMenuControl.start = true this.listMenu.itemMenuControl.start = true
} else { } else {
this.listMenu.itemMenuControl.play = this.listMenu.itemMenuControl.play =
this.listMenu.itemMenuControl.playLater = this.listMenu.itemMenuControl.playLater =

View File

@ -54,7 +54,7 @@
td(style="width: 9%;") td(style="width: 9%;")
span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}} span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}}
td(style="width: 15%; padding-left: 0; padding-right: 0;") td(style="width: 15%; padding-left: 0; padding-right: 0;")
material-list-buttons(:index="index" @btn-click="handleListBtnClick") material-list-buttons(:index="index" @btn-click="handleListBtnClick" :download-btn="assertApiSupport(item.source)")
//- button.btn-info(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k'] || item._types.flac" @click.stop='openDownloadModal(index)') //- button.btn-info(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k'] || item._types.flac" @click.stop='openDownloadModal(index)')
//- button.btn-secondary(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k']" @click.stop='testPlay(index)') //- button.btn-secondary(type='button' v-if="item._types['128k'] || item._types['192k'] || item._types['320k']" @click.stop='testPlay(index)')
//- button.btn-secondary(type='button' @click.stop='handleRemove(index)') //- button.btn-secondary(type='button' @click.stop='handleRemove(index)')
@ -569,7 +569,7 @@ export default {
} }
}, },
testPlay(index) { testPlay(index) {
if (!this.assertApiSupport(this.list[index].source)) return // if (!this.assertApiSupport(this.list[index].source)) return
this.setPlayList({ list: this.listData, index }) this.setPlayList({ list: this.listData, index })
}, },
handleRemove(index) { handleRemove(index) {
@ -579,7 +579,7 @@ export default {
switch (info.action) { switch (info.action) {
case 'download': { case 'download': {
const minfo = this.list[info.index] const minfo = this.list[info.index]
if (!this.assertApiSupport(minfo.source)) return // if (!this.assertApiSupport(minfo.source)) return
this.musicInfo = minfo this.musicInfo = minfo
this.$nextTick(() => { this.$nextTick(() => {
this.isShowDownload = true this.isShowDownload = true
@ -732,9 +732,9 @@ export default {
}, },
handleListItemRigthClick(event, index) { handleListItemRigthClick(event, index) {
this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.list[index].source].getMusicDetailPageUrl this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.list[index].source].getMusicDetailPageUrl
this.listMenu.itemMenuControl.play = // this.listMenu.itemMenuControl.play =
this.listMenu.itemMenuControl.playLater = // this.listMenu.itemMenuControl.playLater =
this.listMenu.itemMenuControl.download = this.listMenu.itemMenuControl.download =
this.assertApiSupport(this.list[index].source) this.assertApiSupport(this.list[index].source)
let dom_selected = this.$refs.dom_tbody.querySelector('tr.selected') let dom_selected = this.$refs.dom_tbody.querySelector('tr.selected')
if (dom_selected) dom_selected.classList.remove('selected') if (dom_selected) dom_selected.classList.remove('selected')

View File

@ -274,7 +274,7 @@ export default {
this.clickIndex = index this.clickIndex = index
return return
} }
this.testPlay(index) if (this.assertApiSupport(this.listInfo.list[index].source)) this.testPlay(index)
this.clickTime = 0 this.clickTime = 0
this.clickIndex = -1 this.clickIndex = -1
}, },
@ -289,14 +289,6 @@ export default {
case 'play': case 'play':
this.testPlay(info.index) this.testPlay(info.index)
break break
case 'playLater':
if (this.selectedData.length) {
this.setTempPlayList(this.selectedData.map(s => ({ listId: '__temp__', musicInfo: s })))
this.resetSelect()
} else {
this.setTempPlayList([{ listId: '__temp__', musicInfo: this.list[info.index] }])
}
break
case 'listAdd': case 'listAdd':
this.musicInfo = this.listInfo.list[info.index] this.musicInfo = this.listInfo.list[info.index]
this.$nextTick(() => { this.$nextTick(() => {
@ -480,6 +472,14 @@ export default {
} }
this.testPlay(index) this.testPlay(index)
break break
case 'playLater':
if (this.selectedData.length) {
this.setTempPlayList(this.selectedData.map(s => ({ listId: '__temp__', musicInfo: s })))
this.resetSelect()
} else {
this.setTempPlayList([{ listId: '__temp__', musicInfo: this.listInfo.list[index] }])
}
break
case 'addTo': case 'addTo':
if (this.selectedData.length) { if (this.selectedData.length) {
this.$nextTick(() => { this.$nextTick(() => {

View File

@ -1,5 +1,5 @@
<template lang="pug"> <template lang="pug">
//- div(:class="$style.main") div(:class="$style.main")
//- div.scroll(:class="$style.toc") //- div.scroll(:class="$style.toc")
//- ul(:class="$style.tocList") //- ul(:class="$style.tocList")
//- li(:class="$style.tocListItem" v-for="h2 in toc.list" :key="h2.id") //- li(:class="$style.tocListItem" v-for="h2 in toc.list" :key="h2.id")
@ -9,57 +9,59 @@
//- li(:class="$style.tocSubListItem" v-for="h3 in h2.children" :key="h3.id") //- li(:class="$style.tocSubListItem" v-for="h3 in h2.children" :key="h3.id")
//- h3(:class="[$style.tocH3, toc.activeId == h3.id ? $style.active : null]" :tips="h3.title") //- h3(:class="[$style.tocH3, toc.activeId == h3.id ? $style.active : null]" :tips="h3.title")
//- a(:href="'#' + h3.id" @click="toc.activeId = h3.id") {{h3.title}} //- a(:href="'#' + h3.id" @click="toc.activeId = h3.id") {{h3.title}}
div.scroll(:class="$style.setting" ref="dom_setting") div.scroll(:class="$style.setting" ref="dom_setting")
dl(ref="dom_setting_list") dl(ref="dom_setting_list")
dt#basic {{$t('view.setting.basic')}} dt#basic {{$t('view.setting.basic')}}
dd dd
h3#basic_theme {{$t('view.setting.basic_theme')}} h3#basic_theme {{$t('view.setting.basic_theme')}}
div div
ul(:class="$style.theme") ul(:class="$style.theme")
li(v-for="theme in themes.list" :key="theme.id" :tips="$t('store.state.theme_' + theme.class)" @click="current_setting.themeId = theme.id" :class="[theme.class, themes.active == theme.id ? $style.active : '']") li(v-for="theme in themes.list" :key="theme.id" :tips="$t('store.state.theme_' + theme.class)" @click="current_setting.themeId = theme.id" :class="[theme.class, themes.active == theme.id ? $style.active : '']")
span span
label {{$t('store.state.theme_' + theme.class)}} label {{$t('store.state.theme_' + theme.class)}}
dd dd
div(:class="[$style.gapTop, $style.top]") div(:class="[$style.gapTop, $style.top]")
material-checkbox(id="setting_show_animate" v-model="current_setting.isShowAnimation" :label="$t('view.setting.basic_show_animation')") material-checkbox(id="setting_show_animate" v-model="current_setting.isShowAnimation" :label="$t('view.setting.basic_show_animation')")
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_animate" v-model="current_setting.randomAnimate" :label="$t('view.setting.basic_animation')") material-checkbox(id="setting_animate" v-model="current_setting.randomAnimate" :label="$t('view.setting.basic_animation')")
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_to_tray" v-model="current_setting.tray.isShow" @change="handleTrayShowChange" :label="$t('view.setting.basic_to_tray')") material-checkbox(id="setting_to_tray" v-model="current_setting.tray.isShow" @change="handleTrayShowChange" :label="$t('view.setting.basic_to_tray')")
dd(:tips="$t('view.setting.basic_source_title')") dd(:tips="$t('view.setting.basic_source_title')")
h3#basic_source {{$t('view.setting.basic_source')}} h3#basic_source {{$t('view.setting.basic_source')}}
div div
div(v-for="item in apiSources" :key="item.id" :class="$style.gapTop") div(v-for="item in apiSources" :key="item.id" :class="$style.gapTop")
material-checkbox(:id="`setting_api_source_${item.id}`" name="setting_api_source" @change="handleAPISourceChange(item.id)" material-checkbox(:id="`setting_api_source_${item.id}`" name="setting_api_source" @change="handleAPISourceChange(item.id)"
need v-model="current_setting.apiSource" :disabled="item.disabled" :value="item.id" :label="item.label") need v-model="current_setting.apiSource" :disabled="item.disabled" :value="item.id" :label="item.label")
p(:class="$style.gapTop")
material-btn(:class="$style.btn" min @click="isShowUserApiModal = true") {{$t('view.setting.basic_source_user_api_btn')}}
dd(:tips="$t('view.setting.basic_window_size_title')") dd(:tips="$t('view.setting.basic_window_size_title')")
h3#basic_window_size {{$t('view.setting.basic_window_size')}} h3#basic_window_size {{$t('view.setting.basic_window_size')}}
div div
material-checkbox(v-for="(item, index) in windowSizeList" :id="`setting_window_size_${item.id}`" name="setting_window_size" @change="handleWindowSizeChange" :class="$style.gapLeft" material-checkbox(v-for="(item, index) in windowSizeList" :id="`setting_window_size_${item.id}`" name="setting_window_size" @change="handleWindowSizeChange" :class="$style.gapLeft"
need v-model="current_setting.windowSizeId" :value="item.id" :label="$t('view.setting.basic_window_size_' + item.name)" :key="item.id") need v-model="current_setting.windowSizeId" :value="item.id" :label="$t('view.setting.basic_window_size_' + item.name)" :key="item.id")
dd(:tips="$t('view.setting.basic_lang_title')") dd(:tips="$t('view.setting.basic_lang_title')")
h3#basic_lang {{$t('view.setting.basic_lang')}} h3#basic_lang {{$t('view.setting.basic_lang')}}
div div
material-checkbox(v-for="item in languageList" :key="item.locale" :id="`setting_lang_${item.locale}`" name="setting_lang" material-checkbox(v-for="item in languageList" :key="item.locale" :id="`setting_lang_${item.locale}`" name="setting_lang"
@change="handleLangChange(item.locale)" :class="$style.gapLeft" @change="handleLangChange(item.locale)" :class="$style.gapLeft"
need v-model="current_setting.langId" :value="item.locale" :label="item.name") need v-model="current_setting.langId" :value="item.locale" :label="item.name")
dd(:tips="$t('view.setting.basic_sourcename_title')") dd(:tips="$t('view.setting.basic_sourcename_title')")
h3#basic_sourcename {{$t('view.setting.basic_sourcename')}} h3#basic_sourcename {{$t('view.setting.basic_sourcename')}}
div div
material-checkbox(v-for="item in sourceNameTypes" :key="item.id" :class="$style.gapLeft" :id="`setting_abasic_sourcename_${item.id}`" material-checkbox(v-for="item in sourceNameTypes" :key="item.id" :class="$style.gapLeft" :id="`setting_abasic_sourcename_${item.id}`"
name="setting_basic_sourcename" need v-model="current_setting.sourceNameType" :value="item.id" :label="item.label") name="setting_basic_sourcename" need v-model="current_setting.sourceNameType" :value="item.id" :label="item.label")
dd dd
h3#basic_control_btn_position {{$t('view.setting.basic_control_btn_position')}} h3#basic_control_btn_position {{$t('view.setting.basic_control_btn_position')}}
div div
material-checkbox(v-for="item in controlBtnPositionList" :key="item.id" :class="$style.gapLeft" :id="`setting_basic_control_btn_position_${item.id}`" material-checkbox(v-for="item in controlBtnPositionList" :key="item.id" :class="$style.gapLeft" :id="`setting_basic_control_btn_position_${item.id}`"
name="setting_basic_control_btn_position" need v-model="current_setting.controlBtnPosition" :value="item.id" :label="item.name") name="setting_basic_control_btn_position" need v-model="current_setting.controlBtnPosition" :value="item.id" :label="item.name")
dt#play {{$t('view.setting.play')}} dt#play {{$t('view.setting.play')}}
dd dd
@ -67,6 +69,8 @@ div.scroll(:class="$style.setting" ref="dom_setting")
material-checkbox(id="setting_player_save_play_time" v-model="current_setting.player.isSavePlayTime" :label="$t('view.setting.play_save_play_time')") material-checkbox(id="setting_player_save_play_time" v-model="current_setting.player.isSavePlayTime" :label="$t('view.setting.play_save_play_time')")
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_player_lyric_transition" v-model="current_setting.player.isShowLyricTransition" :label="$t('view.setting.play_lyric_transition')") material-checkbox(id="setting_player_lyric_transition" v-model="current_setting.player.isShowLyricTransition" :label="$t('view.setting.play_lyric_transition')")
div(:class="$style.gapTop")
material-checkbox(id="setting_player_lyric_play_lxlrc" v-model="current_setting.player.isPlayLxlrc" :label="$t('view.setting.play_lyric_lxlrc')")
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_player_highQuality" v-model="current_setting.player.highQuality" :label="$t('view.setting.play_quality')") material-checkbox(id="setting_player_highQuality" v-model="current_setting.player.highQuality" :label="$t('view.setting.play_quality')")
div(:class="$style.gapTop") div(:class="$style.gapTop")
@ -88,173 +92,179 @@ div.scroll(:class="$style.setting" ref="dom_setting")
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_desktop_lyric_lockScreen" v-model="current_setting.desktopLyric.isLockScreen" :label="$t('view.setting.desktop_lyric_lock_screen')") material-checkbox(id="setting_desktop_lyric_lockScreen" v-model="current_setting.desktopLyric.isLockScreen" :label="$t('view.setting.desktop_lyric_lock_screen')")
dt#search {{$t('view.setting.search')}} dt#search {{$t('view.setting.search')}}
dd dd
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_search_showHot_enable" v-model="current_setting.search.isShowHotSearch" :label="$t('view.setting.search_hot')") material-checkbox(id="setting_search_showHot_enable" v-model="current_setting.search.isShowHotSearch" :label="$t('view.setting.search_hot')")
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_search_showHistory_enable" v-model="current_setting.search.isShowHistorySearch" :label="$t('view.setting.search_history')") material-checkbox(id="setting_search_showHistory_enable" v-model="current_setting.search.isShowHistorySearch" :label="$t('view.setting.search_history')")
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_search_focusSearchBox_enable" v-model="current_setting.search.isFocusSearchBox" :label="$t('view.setting.search_focus_search_box')") material-checkbox(id="setting_search_focusSearchBox_enable" v-model="current_setting.search.isFocusSearchBox" :label="$t('view.setting.search_focus_search_box')")
dt#list {{$t('view.setting.list')}} dt#list {{$t('view.setting.list')}}
dd dd
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_list_showSource_enable" v-model="current_setting.list.isShowSource" :label="$t('view.setting.list_source')") material-checkbox(id="setting_list_showSource_enable" v-model="current_setting.list.isShowSource" :label="$t('view.setting.list_source')")
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_list_scroll_enable" v-model="current_setting.list.isSaveScrollLocation" :label="$t('view.setting.list_scroll')") material-checkbox(id="setting_list_scroll_enable" v-model="current_setting.list.isSaveScrollLocation" :label="$t('view.setting.list_scroll')")
//- dd(:tips="") //- dd(:tips="")
h3 专辑栏 h3 专辑栏
div div
material-checkbox(id="setting_list_showalbum" v-model="current_setting.list.isShowAlbumName" label="是否显示专辑栏") material-checkbox(id="setting_list_showalbum" v-model="current_setting.list.isShowAlbumName" label="是否显示专辑栏")
dt#download {{$t('view.setting.download')}} dt#download {{$t('view.setting.download')}}
dd dd
material-checkbox(id="setting_download_enable" v-model="current_setting.download.enable" :label="$t('view.setting.download_enable')") material-checkbox(id="setting_download_enable" v-model="current_setting.download.enable" :label="$t('view.setting.download_enable')")
dd(:tips="$t('view.setting.download_path_title')") dd(:tips="$t('view.setting.download_path_title')")
h3#download_path {{$t('view.setting.download_path')}} h3#download_path {{$t('view.setting.download_path')}}
div div
p p
| {{$t('view.setting.download_path_label')}} | {{$t('view.setting.download_path_label')}}
span.auto-hidden.hover(:tips="$t('view.setting.download_path_open_label')" :class="$style.savePath" @click="handleOpenDir(current_setting.download.savePath)") {{current_setting.download.savePath}} span.auto-hidden.hover(:tips="$t('view.setting.download_path_open_label')" :class="$style.savePath" @click="handleOpenDir(current_setting.download.savePath)") {{current_setting.download.savePath}}
p p
material-btn(:class="$style.btn" min @click="handleChangeSavePath") {{$t('view.setting.download_path_change_btn')}} material-btn(:class="$style.btn" min @click="handleChangeSavePath") {{$t('view.setting.download_path_change_btn')}}
dd(:tips="$t('view.setting.download_name_title')") dd(:tips="$t('view.setting.download_name_title')")
h3#download_name {{$t('view.setting.download_name')}} h3#download_name {{$t('view.setting.download_name')}}
div div
material-checkbox(:id="`setting_download_musicName_${item.value}`" :class="$style.gapLeft" name="setting_download_musicName" :value="item.value" :key="item.value" need material-checkbox(:id="`setting_download_musicName_${item.value}`" :class="$style.gapLeft" name="setting_download_musicName" :value="item.value" :key="item.value" need
v-model="current_setting.download.fileName" v-for="item in musicNames" :label="item.name") v-model="current_setting.download.fileName" v-for="item in musicNames" :label="item.name")
dd dd
h3#download_data_embed {{$t('view.setting.download_data_embed')}} h3#download_data_embed {{$t('view.setting.download_data_embed')}}
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_download_isEmbedPic" v-model="current_setting.download.isEmbedPic" :label="$t('view.setting.download_embed_pic')") material-checkbox(id="setting_download_isEmbedPic" v-model="current_setting.download.isEmbedPic" :label="$t('view.setting.download_embed_pic')")
div(:class="$style.gapTop") div(:class="$style.gapTop")
material-checkbox(id="setting_download_isEmbedLyric" v-model="current_setting.download.isEmbedLyric" :label="$t('view.setting.download_embed_lyric')") material-checkbox(id="setting_download_isEmbedLyric" v-model="current_setting.download.isEmbedLyric" :label="$t('view.setting.download_embed_lyric')")
dd(:tips="$t('view.setting.download_lyric_title')") dd(:tips="$t('view.setting.download_lyric_title')")
h3#download_lyric {{$t('view.setting.download_lyric')}} h3#download_lyric {{$t('view.setting.download_lyric')}}
div div
material-checkbox(id="setting_download_isDownloadLrc" v-model="current_setting.download.isDownloadLrc" :label="$t('view.setting.is_enable')") material-checkbox(id="setting_download_isDownloadLrc" v-model="current_setting.download.isDownloadLrc" :label="$t('view.setting.is_enable')")
dt#hot_key {{$t('view.setting.hot_key')}} dt#hot_key {{$t('view.setting.hot_key')}}
dd dd
h3#hot_key_local_title {{$t('view.setting.hot_key_local_title')}} h3#hot_key_local_title {{$t('view.setting.hot_key_local_title')}}
div div
material-checkbox(id="setting_download_hotKeyLocal" v-model="current_hot_key.local.enable" :label="$t('view.setting.is_enable')" @change="handleHotKeySaveConfig") material-checkbox(id="setting_download_hotKeyLocal" v-model="current_hot_key.local.enable" :label="$t('view.setting.is_enable')" @change="handleHotKeySaveConfig")
div(:class="$style.hotKeyContainer" :style="{ opacity: current_hot_key.local.enable ? 1 : .6 }") div(:class="$style.hotKeyContainer" :style="{ opacity: current_hot_key.local.enable ? 1 : .6 }")
div(:class="$style.hotKeyItem" v-for="item in hotKeys.local") div(:class="$style.hotKeyItem" v-for="item in hotKeys.local")
h4(:class="$style.hotKeyItemTitle") {{$t('view.setting.hot_key_' + item.name)}} h4(:class="$style.hotKeyItemTitle") {{$t('view.setting.hot_key_' + item.name)}}
material-input.key-bind(:class="$style.hotKeyItemInput" readonly @keyup.prevent :placeholder="$t('view.setting.hot_key_unset_input')" material-input.key-bind(:class="$style.hotKeyItemInput" readonly @keyup.prevent :placeholder="$t('view.setting.hot_key_unset_input')"
:value="hotKeyConfig.local[item.name] && formatHotKeyName(hotKeyConfig.local[item.name].key)" :value="hotKeyConfig.local[item.name] && formatHotKeyName(hotKeyConfig.local[item.name].key)"
@focus="handleHotKeyFocus($event, item, 'local')" @focus="handleHotKeyFocus($event, item, 'local')"
@blur="handleHotKeyBlur($event, item, 'local')") @blur="handleHotKeyBlur($event, item, 'local')")
h3#hot_key_global_title {{$t('view.setting.hot_key_global_title')}} h3#hot_key_global_title {{$t('view.setting.hot_key_global_title')}}
div div
material-checkbox(id="setting_download_hotKeyGlobal" v-model="current_hot_key.global.enable" :label="$t('view.setting.is_enable')" @change="handleEnableHotKey") material-checkbox(id="setting_download_hotKeyGlobal" v-model="current_hot_key.global.enable" :label="$t('view.setting.is_enable')" @change="handleEnableHotKey")
div(:class="$style.hotKeyContainer" :style="{ opacity: current_hot_key.global.enable ? 1 : .6 }") div(:class="$style.hotKeyContainer" :style="{ opacity: current_hot_key.global.enable ? 1 : .6 }")
div(:class="$style.hotKeyItem" v-for="item in hotKeys.global") div(:class="$style.hotKeyItem" v-for="item in hotKeys.global")
h4(:class="$style.hotKeyItemTitle") {{$t('view.setting.hot_key_' + item.name)}} h4(:class="$style.hotKeyItemTitle") {{$t('view.setting.hot_key_' + item.name)}}
material-input.key-bind(:class="[$style.hotKeyItemInput, hotKeyConfig.global[item.name] && hotKeyStatus[hotKeyConfig.global[item.name].key] && hotKeyStatus[hotKeyConfig.global[item.name].key].status === false ? $style.hotKeyFailed : null]" material-input.key-bind(:class="[$style.hotKeyItemInput, hotKeyConfig.global[item.name] && hotKeyStatus[hotKeyConfig.global[item.name].key] && hotKeyStatus[hotKeyConfig.global[item.name].key].status === false ? $style.hotKeyFailed : null]"
:value="hotKeyConfig.global[item.name] && formatHotKeyName(hotKeyConfig.global[item.name].key)" @input.prevent readonly :placeholder="$t('view.setting.hot_key_unset_input')" :value="hotKeyConfig.global[item.name] && formatHotKeyName(hotKeyConfig.global[item.name].key)" @input.prevent readonly :placeholder="$t('view.setting.hot_key_unset_input')"
@focus="handleHotKeyFocus($event, item, 'global')" @focus="handleHotKeyFocus($event, item, 'global')"
@blur="handleHotKeyBlur($event, item, 'global')") @blur="handleHotKeyBlur($event, item, 'global')")
dt#network {{$t('view.setting.network')}}
dd
h3#network_proxy_title {{$t('view.setting.network_proxy_title')}}
div
p
material-checkbox(id="setting_network_proxy_enable" v-model="current_setting.network.proxy.enable" @change="handleProxyChange('enable')" :label="$t('view.setting.is_enable')")
p
material-input(:class="$style.gapLeft" v-model="current_setting.network.proxy.host" @change="handleProxyChange('host')" :placeholder="$t('view.setting.network_proxy_host')")
material-input(:class="$style.gapLeft" v-model="current_setting.network.proxy.port" @change="handleProxyChange('port')" :placeholder="$t('view.setting.network_proxy_port')")
p
material-input(:class="$style.gapLeft" v-model="current_setting.network.proxy.username" @change="handleProxyChange('username')" :placeholder="$t('view.setting.network_proxy_username')")
material-input(:class="$style.gapLeft" v-model="current_setting.network.proxy.password" @change="handleProxyChange('password')" type="password" :placeholder="$t('view.setting.network_proxy_password')")
dt#odc {{$t('view.setting.odc')}}
dd
div(:class="$style.gapTop")
material-checkbox(id="setting_odc_isAutoClearSearchInput" v-model="current_setting.odc.isAutoClearSearchInput" :label="$t('view.setting.odc_clear_search_input')")
div(:class="$style.gapTop")
material-checkbox(id="setting_odc_isAutoClearSearchList" v-model="current_setting.odc.isAutoClearSearchList" :label="$t('view.setting.odc_clear_search_list')")
dt#backup {{$t('view.setting.backup')}}
dd
h3#backup_part {{$t('view.setting.backup_part')}}
div
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleImportPlayList") {{$t('view.setting.backup_part_import_list')}}
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleExportPlayList") {{$t('view.setting.backup_part_export_list')}}
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleImportSetting") {{$t('view.setting.backup_part_import_setting')}}
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleExportSetting") {{$t('view.setting.backup_part_export_setting')}}
dd
h3#backup_all {{$t('view.setting.backup_all')}}
div
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleImportAllData") {{$t('view.setting.backup_all_import')}}
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleExportAllData") {{$t('view.setting.backup_all_export')}}
dt#other {{$t('view.setting.other')}}
dd
h3#other_tray_theme {{$t('view.setting.other_tray_theme')}}
div
material-checkbox(:id="'setting_tray_theme_' + item.id" v-model="current_setting.tray.themeId" name="setting_tray_theme" need :class="$style.gapLeft"
:label="$t('view.setting.other_tray_theme_' + item.name)" :key="item.id" :value="item.id" v-for="item in trayThemeList")
dd
h3#other_resource_cache {{$t('view.setting.other_resource_cache')}}
div
p
| {{$t('view.setting.other_resource_cache_label')}}
span.auto-hidden {{cacheSize}}
p
material-btn(:class="$style.btn" min :disabled="isDisabledResourceCacheClear" @click="clearResourceCache") {{$t('view.setting.other_resource_cache_clear_btn')}}
dd
h3#other_play_list_cache {{$t('view.setting.other_play_list_cache')}}
div
material-btn(:class="$style.btn" min :disabled="isDisabledListCacheClear" @click="clearListCache") {{$t('view.setting.other_play_list_cache_clear_btn')}}
dt#update {{$t('view.setting.update')}}
dd
p.small
| {{$t('view.setting.update_latest_label')}}{{version.newVersion ? version.newVersion.version : $t('view.setting.update_unknown')}}
p.small {{$t('view.setting.update_current_label')}}{{version.version}}
p.small(v-if="this.version.downloadProgress" style="line-height: 1.5;")
| {{$t('view.setting.update_downloading')}}
br
| {{$t('view.setting.update_progress')}}{{downloadProgress}}
p(v-if="version.newVersion")
span(v-if="version.isLatestVer") {{$t('view.setting.update_latest')}}
material-btn(v-else :class="[$style.btn, $style.gapLeft]" min @click="showUpdateModal") {{$t('view.setting.update_open_version_modal_btn')}}
p.small(v-else) {{$t('view.setting.update_checking')}}
dt#about {{$t('view.setting.about')}}
dd
p.small
| 本软件完全免费代码已开源开源地址
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop#readme')") https://github.com/lyswhut/lx-music-desktop
p.small
| 最新版网盘下载地址网盘内有WindowsMAC版
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://www.lanzoux.com/b0bf2cfa/')") 网盘地址
| &nbsp;&nbsp;密码
span.hover(:tips="$t('view.setting.click_copy')" @click="clipboardWriteText('glqw')") glqw
p.small
| 软件的常见问题可转至
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md')") 常见问题
p.small
strong 仔细 仔细 仔细
| 地阅读常见问题后
p.small
| 仍有问题可加企鹅群&nbsp;
span.hover(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://jq.qq.com/?_wv=1027&k=51ECeq2')") 830125506
| &nbsp;反馈
strong (为免满人无事勿加入群先看群公告)
| 或到 GitHub 提交&nbsp;
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/issues')") issue
dt#network {{$t('view.setting.network')}}
dd
h3#network_proxy_title {{$t('view.setting.network_proxy_title')}}
div
p
material-checkbox(id="setting_network_proxy_enable" v-model="current_setting.network.proxy.enable" @change="handleProxyChange('enable')" :label="$t('view.setting.is_enable')")
p
material-input(:class="$style.gapLeft" v-model="current_setting.network.proxy.host" @change="handleProxyChange('host')" :placeholder="$t('view.setting.network_proxy_host')")
material-input(:class="$style.gapLeft" v-model="current_setting.network.proxy.port" @change="handleProxyChange('port')" :placeholder="$t('view.setting.network_proxy_port')")
p
material-input(:class="$style.gapLeft" v-model="current_setting.network.proxy.username" @change="handleProxyChange('username')" :placeholder="$t('view.setting.network_proxy_username')")
material-input(:class="$style.gapLeft" v-model="current_setting.network.proxy.password" @change="handleProxyChange('password')" type="password" :placeholder="$t('view.setting.network_proxy_password')")
dt#odc {{$t('view.setting.odc')}}
dd
div(:class="$style.gapTop")
material-checkbox(id="setting_odc_isAutoClearSearchInput" v-model="current_setting.odc.isAutoClearSearchInput" :label="$t('view.setting.odc_clear_search_input')")
div(:class="$style.gapTop")
material-checkbox(id="setting_odc_isAutoClearSearchList" v-model="current_setting.odc.isAutoClearSearchList" :label="$t('view.setting.odc_clear_search_list')")
dt#backup {{$t('view.setting.backup')}}
dd
h3#backup_part {{$t('view.setting.backup_part')}}
div
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleImportPlayList") {{$t('view.setting.backup_part_import_list')}}
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleExportPlayList") {{$t('view.setting.backup_part_export_list')}}
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleImportSetting") {{$t('view.setting.backup_part_import_setting')}}
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleExportSetting") {{$t('view.setting.backup_part_export_setting')}}
dd
h3#backup_all {{$t('view.setting.backup_all')}}
div
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleImportAllData") {{$t('view.setting.backup_all_import')}}
material-btn(:class="[$style.btn, $style.gapLeft]" min @click="handleExportAllData") {{$t('view.setting.backup_all_export')}}
dt#other {{$t('view.setting.other')}}
dd
h3#other_tray_theme {{$t('view.setting.other_tray_theme')}}
div
material-checkbox(:id="'setting_tray_theme_' + item.id" v-model="current_setting.tray.themeId" name="setting_tray_theme" need :class="$style.gapLeft"
:label="$t('view.setting.other_tray_theme_' + item.name)" :key="item.id" :value="item.id" v-for="item in trayThemeList")
dd
h3#other_cache {{$t('view.setting.other_cache')}}
div
p
| {{$t('view.setting.other_cache_label')}}
span.auto-hidden(:tips="$t('view.setting.other_cache_label_title')") {{cacheSize}}
p
material-btn(:class="$style.btn" min @click="clearCache") {{$t('view.setting.other_cache_clear_btn')}}
dt#update {{$t('view.setting.update')}}
dd
p.small
| {{$t('view.setting.update_latest_label')}}{{version.newVersion ? version.newVersion.version : $t('view.setting.update_unknown')}}
p.small {{$t('view.setting.update_current_label')}}{{version.version}}
p.small(v-if="this.version.downloadProgress" style="line-height: 1.5;")
| {{$t('view.setting.update_downloading')}}
br br
| {{$t('view.setting.update_progress')}}{{downloadProgress}} p.small 感谢以前捐赠过的人现在软件不再接受捐赠建议把你们的爱心用来支持正版音乐
p(v-if="version.newVersion") p.small 由于软件开发的初衷仅是为了对新技术的学习与研究因此软件直至停止维护都将会一直保持纯净
span(v-if="version.isLatestVer") {{$t('view.setting.update_latest')}}
material-btn(v-else :class="[$style.btn, $style.gapLeft]" min @click="showUpdateModal") {{$t('view.setting.update_open_version_modal_btn')}}
p.small(v-else) {{$t('view.setting.update_checking')}}
dt#about {{$t('view.setting.about')}}
dd
p.small
| 本软件完全免费代码已开源开源地址
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop#readme')") https://github.com/lyswhut/lx-music-desktop
p.small
| 最新版网盘下载地址网盘内有WindowsMAC版
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://www.lanzoux.com/b0bf2cfa/')") 网盘地址
| &nbsp;&nbsp;密码
span.hover(:tips="$t('view.setting.click_copy')" @click="clipboardWriteText('glqw')") glqw
p.small
| 软件的常见问题可转至
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md')") 常见问题
p.small
strong 仔细 仔细 仔细
| 地阅读常见问题后
p.small
| 仍有问题可加企鹅群&nbsp;
span.hover(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://jq.qq.com/?_wv=1027&k=51ECeq2')") 830125506
| &nbsp;反馈
strong (为免满人无事勿加入群先看群公告)
| 或到 GitHub 提交&nbsp;
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/issues')") issue
br p.small
p.small 感谢以前捐赠过的人现在软件不再接受捐赠建议把你们的爱心用来支持正版音乐 | 你已签署本软件的&nbsp;
p.small 由于软件开发的初衷仅是为了对新技术的学习与研究因此软件直至停止维护都将会一直保持纯净 material-btn(min @click="handleShowPact") 许可协议
| 协议的在线版本在&nbsp;
strong.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop#%E9%A1%B9%E7%9B%AE%E5%8D%8F%E8%AE%AE')") 这里
| &nbsp;
br
p.small p
| 你已签署本软件的&nbsp; small By
material-btn(min @click="handleShowPact") 许可协议 | 落雪无痕
| 协议的在线版本在&nbsp; material-user-api-modal(v-model="isShowUserApiModal")
strong.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop#%E9%A1%B9%E7%9B%AE%E5%8D%8F%E8%AE%AE')") 这里
| &nbsp;
br
p
small By
| 落雪无痕
</template> </template>
<script> <script>
@ -296,32 +306,41 @@ export default {
? `${this.version.downloadProgress.percent.toFixed(2)}% - ${sizeFormate(this.version.downloadProgress.transferred)}/${sizeFormate(this.version.downloadProgress.total)} - ${sizeFormate(this.version.downloadProgress.bytesPerSecond)}/s` ? `${this.version.downloadProgress.percent.toFixed(2)}% - ${sizeFormate(this.version.downloadProgress.transferred)}/${sizeFormate(this.version.downloadProgress.total)} - ${sizeFormate(this.version.downloadProgress.bytesPerSecond)}/s`
: this.$t('view.setting.update_init') : this.$t('view.setting.update_init')
}, },
togglePlayMethods() { // togglePlayMethods() {
return [ // return [
{ // {
name: this.$t('view.setting.play_toggle_list_loop'), // name: this.$t('view.setting.play_toggle_list_loop'),
value: 'listLoop', // value: 'listLoop',
}, // },
{ // {
name: this.$t('view.setting.play_toggle_random'), // name: this.$t('view.setting.play_toggle_random'),
value: 'random', // value: 'random',
}, // },
{ // {
name: this.$t('view.setting.play_toggle_list'), // name: this.$t('view.setting.play_toggle_list'),
value: 'list', // value: 'list',
}, // },
{ // {
name: this.$t('view.setting.play_toggle_single_loop'), // name: this.$t('view.setting.play_toggle_single_loop'),
value: 'singleLoop', // value: 'singleLoop',
}, // },
] // ]
}, // },
apiSources() { apiSources() {
return apiSourceInfo.map(api => ({ return [
id: api.id, ...apiSourceInfo.map(api => ({
label: this.$t('view.setting.basic_source_' + api.id) || api.name, id: api.id,
disabled: api.disabled, label: this.$t('view.setting.basic_source_' + api.id) || api.name,
})) disabled: api.disabled,
})),
...window.globalObj.userApi.list.map(api => ({
id: api.id,
label: `${api.name}${api.description ? `${api.description}` : ''}${api.id == this.setting.apiSource ? `[${this.getApiStatus()}]` : ''}`,
status: api.status,
message: api.message,
disabled: false,
})),
]
}, },
sourceNameTypes() { sourceNameTypes() {
return [ return [
@ -566,10 +585,13 @@ export default {
}, },
isEditHotKey: false, isEditHotKey: false,
isShowUserApiModal: false,
toc: { toc: {
list: [], list: [],
activeId: '', activeId: '',
}, },
isDisabledResourceCacheClear: false,
isDisabledListCacheClear: false,
} }
}, },
watch: { watch: {
@ -590,6 +612,9 @@ export default {
'setting.player.isMute'(n) { 'setting.player.isMute'(n) {
this.current_setting.player.isMute = n this.current_setting.player.isMute = n
}, },
'setting.apiSource'(n) {
this.current_setting.apiSource = n
},
'setting.desktopLyric.enable'(n) { 'setting.desktopLyric.enable'(n) {
this.current_setting.desktopLyric.enable = n this.current_setting.desktopLyric.enable = n
}, },
@ -599,6 +624,9 @@ export default {
'setting.player.togglePlayMethod'(n) { 'setting.player.togglePlayMethod'(n) {
this.current_setting.player.togglePlayMethod = n this.current_setting.player.togglePlayMethod = n
}, },
// 'setting.player.isPlayLxlrc'(n) {
// this.current_setting.player.isPlayLxlrc = n
// },
'current_setting.player.isShowTaskProgess'(n) { 'current_setting.player.isShowTaskProgess'(n) {
if (n) return if (n) return
this.$nextTick(() => { this.$nextTick(() => {
@ -624,7 +652,10 @@ export default {
}, },
methods: { methods: {
...mapMutations(['setSetting', 'setSettingVersion', 'setVersionModalVisible']), ...mapMutations(['setSetting', 'setSettingVersion', 'setVersionModalVisible']),
...mapMutations('list', ['setList']), ...mapMutations('list', {
setList: 'setList',
clearMyListCache: 'clearCache',
}),
...mapMutations(['setMediaDeviceId']), ...mapMutations(['setMediaDeviceId']),
init() { init() {
this.current_setting = JSON.parse(JSON.stringify(this.setting)) this.current_setting = JSON.parse(JSON.stringify(this.setting))
@ -859,11 +890,18 @@ export default {
this.cacheSize = sizeFormate(size) this.cacheSize = sizeFormate(size)
}) })
}, },
clearCache() { clearResourceCache() {
this.isDisabledResourceCacheClear = true
clearCache().then(() => { clearCache().then(() => {
this.getCacheSize() this.getCacheSize()
this.isDisabledResourceCacheClear = false
}) })
}, },
clearListCache() {
this.isDisabledListCacheClear = true
this.clearMyListCache()
this.isDisabledListCacheClear = false
},
handleWindowSizeChange(index) { handleWindowSizeChange(index) {
let info = index == null ? this.windowSizeList[2] : this.windowSizeList[index] let info = index == null ? this.windowSizeList[2] : this.windowSizeList[index]
setWindowSize(info.width, info.height) setWindowSize(info.width, info.height)
@ -1070,6 +1108,14 @@ export default {
}) })
}) })
}, },
getApiStatus() {
let status
if (window.globalObj.userApi.status) status = this.$t('view.setting.basic_source_status_success')
else if (window.globalObj.userApi.message == 'initing') status = this.$t('view.setting.basic_source_status_initing')
else status = `${this.$t('view.setting.basic_source_status_failed')} - ${window.globalObj.userApi.message}`
return status
},
}, },
} }
</script> </script>
@ -1077,12 +1123,12 @@ export default {
<style lang="less" module> <style lang="less" module>
@import '../assets/styles/layout.less'; @import '../assets/styles/layout.less';
// .main { .main {
// display: flex; display: flex;
// flex-flow: row nowrap; flex-flow: row nowrap;
// height: 100%; height: 100%;
// border-top: 1px solid rgba(0, 0, 0, 0.12); // border-top: 1px solid rgba(0, 0, 0, 0.12);
// } }
// .toc { // .toc {
// flex: 0 0 15%; // flex: 0 0 15%;

View File

@ -23,7 +23,7 @@
material-select(:class="$style.select" :list="sourceInfo.sources" item-key="id" item-name="name" v-model="source") material-select(:class="$style.select" :list="sourceInfo.sources" item-key="id" item-name="name" v-model="source")
div(:class="$style.songListContent") div(:class="$style.songListContent")
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut") transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
div(:class="$style.songListContent" v-show="listData.list.length") div(:class="$style.songListContent")
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut") transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
div.scroll(:class="$style.songList" v-if="sortId !== 'importSongList'" ref="dom_scrollContent") div.scroll(:class="$style.songList" v-if="sortId !== 'importSongList'" ref="dom_scrollContent")
ul ul
@ -49,7 +49,7 @@
li {{$t('view.song_list.tip_2')}} li {{$t('view.song_list.tip_2')}}
li {{$t('view.song_list.tip_3')}} li {{$t('view.song_list.tip_3')}}
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut") transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
div(v-show="!listData.list.length" :class="$style.noitem") div(v-show="!listData.list.length && sortId !== 'importSongList'" :class="$style.noitem")
p {{$t('view.song_list.loding_list')}} p {{$t('view.song_list.loding_list')}}
material-download-modal(:show="isShowDownload" :musicInfo="musicInfo" @select="handleAddDownload" @close="isShowDownload = false") material-download-modal(:show="isShowDownload" :musicInfo="musicInfo" @select="handleAddDownload" @close="isShowDownload = false")
material-download-multiple-modal(:show="isShowDownloadMultiple" :list="selectedData" @select="handleAddDownloadMultiple" @close="isShowDownloadMultiple = false") material-download-multiple-modal(:show="isShowDownloadMultiple" :list="selectedData" @select="handleAddDownloadMultiple" @close="isShowDownloadMultiple = false")
@ -239,7 +239,7 @@ export default {
break break
case 'play': case 'play':
if (this.selectedData.length) { if (this.selectedData.length) {
this.listAddMultiple({ id: 'default', list: this.filterList(this.selectedData) }) this.listAddMultiple({ id: 'default', list: [...this.selectedData] })
this.resetSelect() this.resetSelect()
} }
this.testPlay(info.index) this.testPlay(info.index)