Merge branch 'api' into dev
parent
2bc669c828
commit
e0de81230e
162
FAQ.md
162
FAQ.md
|
@ -171,3 +171,165 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看下面的
|
|||
从`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讨论。
|
||||
|
|
|
@ -17,8 +17,8 @@ module.exports = merge(baseConfig, {
|
|||
NODE_ENV: '"development"',
|
||||
},
|
||||
__static: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`,
|
||||
__userApi: `"${path.join(__dirname, '../../src/main/modules/userApi').replace(/\\/g, '\\\\')}"`,
|
||||
}),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new FriendlyErrorsPlugin({
|
||||
onErrors(severity, errors) { // Silent warning from electron-debug
|
||||
if (severity != 'warning') return
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const path = require('path')
|
||||
const { merge } = require('webpack-merge')
|
||||
const webpack = require('webpack')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
|
||||
const baseConfig = require('./webpack.config.base')
|
||||
|
||||
|
@ -20,6 +21,18 @@ module.exports = merge(baseConfig, {
|
|||
__filename: false,
|
||||
},
|
||||
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({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"',
|
||||
|
|
|
@ -11,7 +11,6 @@ module.exports = merge(baseConfig, {
|
|||
devtool: 'eval-source-map',
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new FriendlyErrorsPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
|
|
|
@ -11,7 +11,6 @@ module.exports = merge(baseConfig, {
|
|||
devtool: 'eval-source-map',
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new FriendlyErrorsPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
|
|
|
@ -179,6 +179,9 @@ function startElectron() {
|
|||
function electronLog(data, color) {
|
||||
let log = data.toString()
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
|
@ -162,13 +162,13 @@
|
|||
},
|
||||
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.13.1",
|
||||
"@babel/core": "^7.13.8",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-modules-umd": "^7.13.0",
|
||||
"@babel/plugin-transform-runtime": "^7.13.4",
|
||||
"@babel/plugin-transform-runtime": "^7.13.9",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/preset-env": "^7.13.0",
|
||||
"@babel/preset-env": "^7.13.9",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-minify-webpack-plugin": "^0.3.1",
|
||||
|
@ -177,15 +177,15 @@
|
|||
"chalk": "^4.1.0",
|
||||
"changelog-parser": "^2.8.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"core-js": "^3.9.0",
|
||||
"core-js": "^3.9.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^4.3.0",
|
||||
"del": "^6.0.0",
|
||||
"electron": "^9.4.2",
|
||||
"electron-builder": "^22.9.1",
|
||||
"electron-builder": "^22.10.5",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"eslint": "^7.20.0",
|
||||
"eslint": "^7.21.0",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-formatter-friendly": "^7.0.0",
|
||||
"eslint-loader": "^4.0.2",
|
||||
|
@ -196,7 +196,7 @@
|
|||
"eslint-plugin-standard": "^4.1.0",
|
||||
"file-loader": "^6.2.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-loader": "^7.3.0",
|
||||
"markdown-it": "^12.0.4",
|
||||
|
@ -204,7 +204,7 @@
|
|||
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
||||
"postcss-loader": "^4.2.0",
|
||||
"postcss-pxtorem": "^5.1.1",
|
||||
"pug": "^3.0.0",
|
||||
"pug": "^3.0.2",
|
||||
"pug-loader": "^2.4.0",
|
||||
"pug-plain-loader": "^1.1.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
|
@ -227,16 +227,16 @@
|
|||
"dnscache": "^1.0.2",
|
||||
"electron-log": "^4.3.2",
|
||||
"electron-store": "^6.0.1",
|
||||
"electron-updater": "^4.3.5",
|
||||
"electron-updater": "^4.3.8",
|
||||
"iconv-lite": "^0.6.2",
|
||||
"image-size": "^0.9.3",
|
||||
"image-size": "^0.9.4",
|
||||
"js-htmlencode": "^0.3.0",
|
||||
"lrc-file-parser": "^1.0.7",
|
||||
"needle": "^2.6.0",
|
||||
"node-id3": "^0.2.2",
|
||||
"request": "^2.88.2",
|
||||
"vue": "^2.6.12",
|
||||
"vue-i18n": "^8.22.4",
|
||||
"vue-i18n": "^8.23.0",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
- 新增设置-其他-列表缓存信息清理功能,注:此功能一般情况下不要使用
|
||||
- 新增启动参数`-play`,可以在启动软件时播放指定歌单,使用方法看Readme.md的"启动参数"部分
|
||||
- 新增逐字歌词播放,默认开启,可到设置界面关闭,注:本功能目前仅对酷狗源的歌曲有效
|
||||
- 新增自定义源功能,源编写规则可以去常见问题查看
|
||||
|
||||
### 优化
|
||||
|
||||
|
|
|
@ -2,11 +2,17 @@ const { ipcMain, ipcRenderer } = require('electron')
|
|||
const names = require('./ipcNames')
|
||||
|
||||
|
||||
exports.mainOn = (event, callback) => {
|
||||
ipcMain.on(event, callback)
|
||||
exports.mainOn = (name, callback) => {
|
||||
ipcMain.on(name, callback)
|
||||
}
|
||||
exports.mainOnce = (event, callback) => {
|
||||
ipcMain.once(event, callback)
|
||||
exports.mainOnce = (name, callback) => {
|
||||
ipcMain.once(name, callback)
|
||||
}
|
||||
exports.mainOff = (name, callback) => {
|
||||
ipcMain.removeListener(name, callback)
|
||||
}
|
||||
exports.mainOffAll = name => {
|
||||
ipcMain.removeAllListeners(name)
|
||||
}
|
||||
|
||||
exports.mainHandle = (name, callback) => {
|
||||
|
@ -15,6 +21,9 @@ exports.mainHandle = (name, callback) => {
|
|||
exports.mainHandleOnce = (name, callback) => {
|
||||
ipcMain.handleOnce(name, callback)
|
||||
}
|
||||
exports.mainHandleRemove = name => {
|
||||
ipcMain.removeListener(name)
|
||||
}
|
||||
|
||||
exports.mainSend = (window, name, params) => {
|
||||
window.webContents.send(name, params)
|
||||
|
@ -33,5 +42,11 @@ exports.rendererOn = (name, callback) => {
|
|||
exports.rendererOnce = (name, callback) => {
|
||||
ipcRenderer.once(name, callback)
|
||||
}
|
||||
exports.rendererOff = (name, callback) => {
|
||||
ipcRenderer.removeListener(name, callback)
|
||||
}
|
||||
exports.rendererOffAll = name => {
|
||||
ipcRenderer.removeAllListeners(name)
|
||||
}
|
||||
|
||||
exports.NAMES = names
|
||||
|
|
|
@ -50,6 +50,15 @@ const names = {
|
|||
get_data: 'get_data',
|
||||
save_data: 'save_data',
|
||||
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: {
|
||||
close: 'close',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const log = require('electron-log')
|
||||
const Store = require('electron-store')
|
||||
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 { dialog, app } = require('electron')
|
||||
const path = require('path')
|
||||
|
@ -115,10 +115,10 @@ exports.mergeSetting = (setting, version) => {
|
|||
setting = defaultSettingCopy
|
||||
}
|
||||
|
||||
if (!apiSource.some(api => api.id === setting.apiSource && !api.disabled)) {
|
||||
let api = apiSource.find(api => !api.disabled)
|
||||
if (api) setting.apiSource = api.id
|
||||
}
|
||||
// if (!apiSource.some(api => api.id === setting.apiSource && !api.disabled)) {
|
||||
// let api = apiSource.find(api => !api.disabled)
|
||||
// if (api) setting.apiSource = api.id
|
||||
// }
|
||||
|
||||
return { setting, version: defaultVersion }
|
||||
}
|
||||
|
|
|
@ -6,8 +6,12 @@ const Tray = require('./Tray')
|
|||
const WinLyric = require('./WinLyric')
|
||||
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.mainWindow) global.lx_event.mainWindow = new MainWindow()
|
||||
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.hotKey) global.lx_event.hotKey = new HotKey()
|
||||
|
||||
if (!global.lx_event.userApi) global.lx_event.userApi = new UserApi()
|
||||
|
|
|
@ -2,3 +2,4 @@ require('./appMenu')
|
|||
require('./winLyric')
|
||||
require('./tray')
|
||||
require('./hotKey')
|
||||
require('./userApi')
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
exports.userApis = []
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
status: 'status',
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
// }
|
||||
// },
|
||||
})
|
|
@ -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>
|
|
@ -0,0 +1,12 @@
|
|||
const names = {
|
||||
init: '',
|
||||
request: '',
|
||||
response: '',
|
||||
openDevTools: '',
|
||||
}
|
||||
|
||||
|
||||
for (const key of Object.keys(names)) {
|
||||
names[key] = `userApi_${key}`
|
||||
}
|
||||
module.exports = names
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -19,3 +19,5 @@ require('./playList')
|
|||
require('./data')
|
||||
|
||||
require('./kw_decodeLyric')
|
||||
|
||||
require('./userApi')
|
||||
|
|
|
@ -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)
|
||||
})
|
|
@ -27,6 +27,7 @@ import { isLinux } from '../common/utils'
|
|||
import music from './utils/music'
|
||||
import { throttle, openUrl, compareVer, getPlayList, parseUrlParams } from './utils'
|
||||
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
|
||||
dnscache({
|
||||
|
@ -56,10 +57,16 @@ export default {
|
|||
isDT: false,
|
||||
isLinux,
|
||||
globalObj: {
|
||||
apiSource: 'test',
|
||||
apiSource: null,
|
||||
proxy: {},
|
||||
isShowPact: false,
|
||||
qualityList: {},
|
||||
userApi: {
|
||||
list: [],
|
||||
status: false,
|
||||
message: 'initing',
|
||||
apis: {},
|
||||
},
|
||||
},
|
||||
updateTimeout: null,
|
||||
envParams: {
|
||||
|
@ -137,8 +144,20 @@ export default {
|
|||
searchHistoryList(n) {
|
||||
this.saveSearchHistoryList(n)
|
||||
},
|
||||
'globalObj.apiSource'(n) {
|
||||
this.globalObj.qualityList = music.supportQuality[n]
|
||||
'globalObj.apiSource'(n, o) {
|
||||
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) {
|
||||
this.setSetting(Object.assign({}, this.setting, {
|
||||
apiSource: n,
|
||||
|
@ -244,8 +263,13 @@ export default {
|
|||
|
||||
this.listenEvent()
|
||||
asyncTask.push(this.initData())
|
||||
asyncTask.push(this.initUserApi())
|
||||
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)
|
||||
window.globalObj = this.globalObj
|
||||
|
||||
|
@ -345,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() {
|
||||
(this.version.newVersion && this.version.newVersion.history
|
||||
? Promise.resolve(this.version.newVersion)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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
|
||||
</template>
|
||||
|
||||
|
@ -9,6 +9,10 @@ export default {
|
|||
min: {
|
||||
type: Boolean,
|
||||
},
|
||||
outline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -36,6 +40,10 @@ export default {
|
|||
opacity: .4;
|
||||
}
|
||||
|
||||
&.outline {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @color-btn-hover;
|
||||
}
|
||||
|
@ -54,6 +62,9 @@ each(@themes, {
|
|||
.btn {
|
||||
color: ~'@{color-@{value}-btn}';
|
||||
background-color: ~'@{color-@{value}-btn-background}';
|
||||
&.outline {
|
||||
background-color: transparent;
|
||||
}
|
||||
&:hover {
|
||||
background-color: ~'@{color-@{value}-btn-hover}';
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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...😲"
|
||||
}
|
|
@ -11,6 +11,10 @@
|
|||
"basic_sourcename_real": "Original",
|
||||
"basic_sourcename_alias": "Aliases",
|
||||
"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": "Window size",
|
||||
"basic_window_size_smaller": "Smaller",
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"title": "自定义源管理",
|
||||
"readme": "源编写说明:",
|
||||
"note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。",
|
||||
"btn_remove": "移除",
|
||||
"btn_import": "导入",
|
||||
"btn_export": "导出",
|
||||
"import_file": "选择音乐API脚本文件",
|
||||
"noitem": "这里竟然是空的 😲"
|
||||
}
|
|
@ -11,6 +11,10 @@
|
|||
"basic_sourcename_real": "原名",
|
||||
"basic_sourcename_alias": "别名",
|
||||
"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": "窗口尺寸",
|
||||
"basic_window_size_smaller": "较小",
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"title": "自定義源管理",
|
||||
"readme": "源編寫說明:",
|
||||
"note": "提示:雖然我們已經盡可能地隔離了腳本的運行環境,但導入包含惡意行為的腳本仍可能會影響你的系統,請謹慎導入。",
|
||||
"btn_remove": "移除",
|
||||
"btn_import": "導入",
|
||||
"btn_export": "導出",
|
||||
"import_file": "選擇音樂API腳本文件",
|
||||
"noitem": "這裡竟然是空的 😲"
|
||||
}
|
|
@ -11,6 +11,10 @@
|
|||
"basic_sourcename_real": "原名",
|
||||
"basic_sourcename_alias": "別名",
|
||||
"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": "窗口尺寸",
|
||||
"basic_window_size_smaller": "較小",
|
||||
|
|
|
@ -13,6 +13,7 @@ for (const api of apiSourceInfo) {
|
|||
const getAPI = source => apiList[`${window.globalObj.apiSource}_api_${source}`]
|
||||
|
||||
const apis = source => {
|
||||
if (/^user_api/.test(window.globalObj.apiSource)) return window.globalObj.userApi.apis[source]
|
||||
let api = getAPI(source)
|
||||
if (api) return api
|
||||
throw new Error('Api is not found')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template lang="pug">
|
||||
//- div(:class="$style.main")
|
||||
div(:class="$style.main")
|
||||
//- div.scroll(:class="$style.toc")
|
||||
//- ul(:class="$style.tocList")
|
||||
//- 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")
|
||||
//- 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}}
|
||||
div.scroll(:class="$style.setting" ref="dom_setting")
|
||||
dl(ref="dom_setting_list")
|
||||
dt#basic {{$t('view.setting.basic')}}
|
||||
dd
|
||||
h3#basic_theme {{$t('view.setting.basic_theme')}}
|
||||
div
|
||||
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 : '']")
|
||||
span
|
||||
label {{$t('store.state.theme_' + theme.class)}}
|
||||
div.scroll(:class="$style.setting" ref="dom_setting")
|
||||
dl(ref="dom_setting_list")
|
||||
dt#basic {{$t('view.setting.basic')}}
|
||||
dd
|
||||
h3#basic_theme {{$t('view.setting.basic_theme')}}
|
||||
div
|
||||
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 : '']")
|
||||
span
|
||||
label {{$t('store.state.theme_' + theme.class)}}
|
||||
|
||||
dd
|
||||
div(:class="[$style.gapTop, $style.top]")
|
||||
material-checkbox(id="setting_show_animate" v-model="current_setting.isShowAnimation" :label="$t('view.setting.basic_show_animation')")
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_animate" v-model="current_setting.randomAnimate" :label="$t('view.setting.basic_animation')")
|
||||
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')")
|
||||
dd
|
||||
div(:class="[$style.gapTop, $style.top]")
|
||||
material-checkbox(id="setting_show_animate" v-model="current_setting.isShowAnimation" :label="$t('view.setting.basic_show_animation')")
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_animate" v-model="current_setting.randomAnimate" :label="$t('view.setting.basic_animation')")
|
||||
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')")
|
||||
|
||||
|
||||
dd(:tips="$t('view.setting.basic_source_title')")
|
||||
h3#basic_source {{$t('view.setting.basic_source')}}
|
||||
div
|
||||
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)"
|
||||
need v-model="current_setting.apiSource" :disabled="item.disabled" :value="item.id" :label="item.label")
|
||||
dd(:tips="$t('view.setting.basic_source_title')")
|
||||
h3#basic_source {{$t('view.setting.basic_source')}}
|
||||
div
|
||||
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)"
|
||||
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')")
|
||||
h3#basic_window_size {{$t('view.setting.basic_window_size')}}
|
||||
div
|
||||
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")
|
||||
dd(:tips="$t('view.setting.basic_window_size_title')")
|
||||
h3#basic_window_size {{$t('view.setting.basic_window_size')}}
|
||||
div
|
||||
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")
|
||||
|
||||
dd(:tips="$t('view.setting.basic_lang_title')")
|
||||
h3#basic_lang {{$t('view.setting.basic_lang')}}
|
||||
div
|
||||
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"
|
||||
need v-model="current_setting.langId" :value="item.locale" :label="item.name")
|
||||
dd(:tips="$t('view.setting.basic_lang_title')")
|
||||
h3#basic_lang {{$t('view.setting.basic_lang')}}
|
||||
div
|
||||
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"
|
||||
need v-model="current_setting.langId" :value="item.locale" :label="item.name")
|
||||
|
||||
dd(:tips="$t('view.setting.basic_sourcename_title')")
|
||||
h3#basic_sourcename {{$t('view.setting.basic_sourcename')}}
|
||||
div
|
||||
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")
|
||||
dd(:tips="$t('view.setting.basic_sourcename_title')")
|
||||
h3#basic_sourcename {{$t('view.setting.basic_sourcename')}}
|
||||
div
|
||||
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")
|
||||
|
||||
dd
|
||||
h3#basic_control_btn_position {{$t('view.setting.basic_control_btn_position')}}
|
||||
div
|
||||
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")
|
||||
dd
|
||||
h3#basic_control_btn_position {{$t('view.setting.basic_control_btn_position')}}
|
||||
div
|
||||
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")
|
||||
|
||||
dt#play {{$t('view.setting.play')}}
|
||||
dd
|
||||
|
@ -90,178 +92,179 @@ div.scroll(:class="$style.setting" ref="dom_setting")
|
|||
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')")
|
||||
|
||||
dt#search {{$t('view.setting.search')}}
|
||||
dd
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_search_showHot_enable" v-model="current_setting.search.isShowHotSearch" :label="$t('view.setting.search_hot')")
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_search_showHistory_enable" v-model="current_setting.search.isShowHistorySearch" :label="$t('view.setting.search_history')")
|
||||
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')")
|
||||
dt#search {{$t('view.setting.search')}}
|
||||
dd
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_search_showHot_enable" v-model="current_setting.search.isShowHotSearch" :label="$t('view.setting.search_hot')")
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_search_showHistory_enable" v-model="current_setting.search.isShowHistorySearch" :label="$t('view.setting.search_history')")
|
||||
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')")
|
||||
|
||||
dt#list {{$t('view.setting.list')}}
|
||||
dd
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_list_showSource_enable" v-model="current_setting.list.isShowSource" :label="$t('view.setting.list_source')")
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_list_scroll_enable" v-model="current_setting.list.isSaveScrollLocation" :label="$t('view.setting.list_scroll')")
|
||||
//- dd(:tips="播放列表是否显示专辑栏")
|
||||
h3 专辑栏
|
||||
div
|
||||
material-checkbox(id="setting_list_showalbum" v-model="current_setting.list.isShowAlbumName" label="是否显示专辑栏")
|
||||
dt#download {{$t('view.setting.download')}}
|
||||
dd
|
||||
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')")
|
||||
h3#download_path {{$t('view.setting.download_path')}}
|
||||
div
|
||||
p
|
||||
| {{$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}}
|
||||
p
|
||||
material-btn(:class="$style.btn" min @click="handleChangeSavePath") {{$t('view.setting.download_path_change_btn')}}
|
||||
dd(:tips="$t('view.setting.download_name_title')")
|
||||
h3#download_name {{$t('view.setting.download_name')}}
|
||||
div
|
||||
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")
|
||||
dd
|
||||
h3#download_data_embed {{$t('view.setting.download_data_embed')}}
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_download_isEmbedPic" v-model="current_setting.download.isEmbedPic" :label="$t('view.setting.download_embed_pic')")
|
||||
div(:class="$style.gapTop")
|
||||
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')")
|
||||
h3#download_lyric {{$t('view.setting.download_lyric')}}
|
||||
div
|
||||
material-checkbox(id="setting_download_isDownloadLrc" v-model="current_setting.download.isDownloadLrc" :label="$t('view.setting.is_enable')")
|
||||
dt#list {{$t('view.setting.list')}}
|
||||
dd
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_list_showSource_enable" v-model="current_setting.list.isShowSource" :label="$t('view.setting.list_source')")
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_list_scroll_enable" v-model="current_setting.list.isSaveScrollLocation" :label="$t('view.setting.list_scroll')")
|
||||
//- dd(:tips="播放列表是否显示专辑栏")
|
||||
h3 专辑栏
|
||||
div
|
||||
material-checkbox(id="setting_list_showalbum" v-model="current_setting.list.isShowAlbumName" label="是否显示专辑栏")
|
||||
dt#download {{$t('view.setting.download')}}
|
||||
dd
|
||||
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')")
|
||||
h3#download_path {{$t('view.setting.download_path')}}
|
||||
div
|
||||
p
|
||||
| {{$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}}
|
||||
p
|
||||
material-btn(:class="$style.btn" min @click="handleChangeSavePath") {{$t('view.setting.download_path_change_btn')}}
|
||||
dd(:tips="$t('view.setting.download_name_title')")
|
||||
h3#download_name {{$t('view.setting.download_name')}}
|
||||
div
|
||||
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")
|
||||
dd
|
||||
h3#download_data_embed {{$t('view.setting.download_data_embed')}}
|
||||
div(:class="$style.gapTop")
|
||||
material-checkbox(id="setting_download_isEmbedPic" v-model="current_setting.download.isEmbedPic" :label="$t('view.setting.download_embed_pic')")
|
||||
div(:class="$style.gapTop")
|
||||
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')")
|
||||
h3#download_lyric {{$t('view.setting.download_lyric')}}
|
||||
div
|
||||
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')}}
|
||||
dd
|
||||
h3#hot_key_local_title {{$t('view.setting.hot_key_local_title')}}
|
||||
div
|
||||
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.hotKeyItem" v-for="item in hotKeys.local")
|
||||
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')"
|
||||
:value="hotKeyConfig.local[item.name] && formatHotKeyName(hotKeyConfig.local[item.name].key)"
|
||||
@focus="handleHotKeyFocus($event, item, 'local')"
|
||||
@blur="handleHotKeyBlur($event, item, 'local')")
|
||||
dt#hot_key {{$t('view.setting.hot_key')}}
|
||||
dd
|
||||
h3#hot_key_local_title {{$t('view.setting.hot_key_local_title')}}
|
||||
div
|
||||
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.hotKeyItem" v-for="item in hotKeys.local")
|
||||
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')"
|
||||
:value="hotKeyConfig.local[item.name] && formatHotKeyName(hotKeyConfig.local[item.name].key)"
|
||||
@focus="handleHotKeyFocus($event, item, 'local')"
|
||||
@blur="handleHotKeyBlur($event, item, 'local')")
|
||||
|
||||
h3#hot_key_global_title {{$t('view.setting.hot_key_global_title')}}
|
||||
div
|
||||
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.hotKeyItem" v-for="item in hotKeys.global")
|
||||
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]"
|
||||
: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')"
|
||||
@blur="handleHotKeyBlur($event, item, 'global')")
|
||||
h3#hot_key_global_title {{$t('view.setting.hot_key_global_title')}}
|
||||
div
|
||||
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.hotKeyItem" v-for="item in hotKeys.global")
|
||||
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]"
|
||||
: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')"
|
||||
@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#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
|
||||
| 最新版网盘下载地址(网盘内有Windows、MAC版):
|
||||
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://www.lanzoux.com/b0bf2cfa/')") 网盘地址
|
||||
| 密码:
|
||||
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
|
||||
| 仍有问题可加企鹅群
|
||||
span.hover(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://jq.qq.com/?_wv=1027&k=51ECeq2')") 830125506
|
||||
| 反馈
|
||||
strong (为免满人,无事勿加,入群先看群公告)
|
||||
| ,或到 GitHub 提交
|
||||
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/issues')") issue
|
||||
|
||||
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
|
||||
| 最新版网盘下载地址(网盘内有Windows、MAC版):
|
||||
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://www.lanzoux.com/b0bf2cfa/')") 网盘地址
|
||||
| 密码:
|
||||
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
|
||||
| 仍有问题可加企鹅群
|
||||
span.hover(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://jq.qq.com/?_wv=1027&k=51ECeq2')") 830125506
|
||||
| 反馈
|
||||
strong (为免满人,无事勿加,入群先看群公告)
|
||||
| ,或到 GitHub 提交
|
||||
span.hover.underline(:tips="$t('view.setting.click_open')" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/issues')") issue
|
||||
p.small 感谢以前捐赠过的人❤️,现在软件不再接受捐赠,建议把你们的爱心用来支持正版音乐,
|
||||
p.small 由于软件开发的初衷仅是为了对新技术的学习与研究,因此软件直至停止维护都将会一直保持纯净。
|
||||
|
||||
br
|
||||
p.small 感谢以前捐赠过的人❤️,现在软件不再接受捐赠,建议把你们的爱心用来支持正版音乐,
|
||||
p.small 由于软件开发的初衷仅是为了对新技术的学习与研究,因此软件直至停止维护都将会一直保持纯净。
|
||||
p.small
|
||||
| 你已签署本软件的
|
||||
material-btn(min @click="handleShowPact") 许可协议
|
||||
| ,协议的在线版本在
|
||||
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')") 这里
|
||||
| 。
|
||||
br
|
||||
|
||||
p.small
|
||||
| 你已签署本软件的
|
||||
material-btn(min @click="handleShowPact") 许可协议
|
||||
| ,协议的在线版本在
|
||||
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')") 这里
|
||||
| 。
|
||||
br
|
||||
|
||||
p
|
||||
small By:
|
||||
| 落雪无痕
|
||||
p
|
||||
small By:
|
||||
| 落雪无痕
|
||||
material-user-api-modal(v-model="isShowUserApiModal")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -324,11 +327,20 @@ export default {
|
|||
// ]
|
||||
// },
|
||||
apiSources() {
|
||||
return apiSourceInfo.map(api => ({
|
||||
id: api.id,
|
||||
label: this.$t('view.setting.basic_source_' + api.id) || api.name,
|
||||
disabled: api.disabled,
|
||||
}))
|
||||
return [
|
||||
...apiSourceInfo.map(api => ({
|
||||
id: api.id,
|
||||
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() {
|
||||
return [
|
||||
|
@ -573,6 +585,7 @@ export default {
|
|||
|
||||
},
|
||||
isEditHotKey: false,
|
||||
isShowUserApiModal: false,
|
||||
toc: {
|
||||
list: [],
|
||||
activeId: '',
|
||||
|
@ -599,6 +612,9 @@ export default {
|
|||
'setting.player.isMute'(n) {
|
||||
this.current_setting.player.isMute = n
|
||||
},
|
||||
'setting.apiSource'(n) {
|
||||
this.current_setting.apiSource = n
|
||||
},
|
||||
'setting.desktopLyric.enable'(n) {
|
||||
this.current_setting.desktopLyric.enable = n
|
||||
},
|
||||
|
@ -1092,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>
|
||||
|
@ -1099,12 +1123,12 @@ export default {
|
|||
<style lang="less" module>
|
||||
@import '../assets/styles/layout.less';
|
||||
|
||||
// .main {
|
||||
// display: flex;
|
||||
// flex-flow: row nowrap;
|
||||
// height: 100%;
|
||||
// border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
// }
|
||||
.main {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
height: 100%;
|
||||
// border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
// .toc {
|
||||
// flex: 0 0 15%;
|
||||
|
|
Loading…
Reference in New Issue