Merge branch 'dev'

pull/930/merge
lyswhut 2022-02-26 12:22:00 +08:00
commit 53d4f08ee7
102 changed files with 2372 additions and 1006 deletions

View File

@ -2,7 +2,6 @@ module.exports = {
upgrade: true,
reject: [
'electron',
'electron-builder',
'chalk',
],
// target: 'newest',

View File

@ -6,6 +6,38 @@ Project versioning adheres to [Semantic Versioning](http://semver.org/).
Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
## [1.18.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.17.1...v1.18.0) - 2022-02-26
### 新增
- 新增“双击列表里的歌曲时自动切换到当前列表播放”设置,此功能仅对歌单、排行榜有效,默认关闭
- 新增打开收藏的在线列表的对应平台详情页功能,可以在我的列表-列表右键菜单中使用
- 新增定时暂停播放功能,由于此功能大多数人可能不常用,所以将其放在设置-基本设置中
- 新增任务栏缩略图工具栏控制按钮此功能仅在Windows平台可用按钮分别为收藏/取消收藏(将歌曲添加到“我的收藏”列表)、上一曲、播放/暂停、下一曲
- 新增设置-基本设置-软件字体设置此设置可用于设置主界面的字体已知的问题Windows 7 下可能会出现字体列表为空的情况,这是当前系统的 Powershell 版本小于5.1导致的,请自行尝试看常见解决)
- 新增Scheme URL对音乐搜索的调用支持详情看常见问题-Scheme URL支持
- 新增Scheme URL以url传参的方式调用详情看常见问题-Scheme URL支持
- 自定义源新增更新弹窗方法,同时自定义源管理新增是否允许源显示更新弹窗设置(出于防止滥用考虑),当源作者想要通知用户源已更新时,可以调用此方法弹窗告诉用户,调用说明看常见问题-自定义源部分
### 优化
- 过滤tx源某些不支持播放的歌曲解决播放此类内容会导致意外的问题
- 把歌曲的热门评论与最新评论拆分成两个列表显示
### 修复
- 修复排行榜名字右击菜单的播放功能在播放非激活的列表时的列表获取问题
- 修复修改列表名时无法使用`Ctrl`键的问题
- 修复wy源某些歌曲获取歌词翻译的问题处理
- 修复下载功能的歌词换源时会进入死循环的问题
- 修复某些歌曲无法下载的问题
- 修复windows平台下软件目录存在`portable`文件夹时,仍会创建`C:\Users\<user>\AppData\Roaming\lx-music-desktop\Dictionaries\en-US-9-0.bdic`文件的问题现在不会再创建文件但仍会创建空目录Electron的问题目前暂无解决方法
- 修复播放器的停止逻辑问题
### 其他
- 更新electron到v13.6.9
## [1.17.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.17.0...v1.17.1) - 2022-01-28
### 优化

33
FAQ.md
View File

@ -6,6 +6,15 @@
洛雪音乐的最初定位不是作为播放器开发的,它主要用于**查找歌曲**,软件的播放功能仅用于试听,不建议用作为常用播放器使用。
## LX Music中的音乐播放列表机制
1. 默认情况下,播放搜索列表、歌单列表、排行榜列表的歌曲时会自动将该歌曲添加到“我的列表”的试听列表后再播放,手动将歌曲添加到试听列表,再去试听列表找到这首歌点播放是等价的
2. 如果你想要播放多首歌曲,需要使用多选功能(若不知道如何多选请看常见问题)多选后,将歌曲这些歌曲添加到“我的列表”播放,或使用稍后播放功能播放
3. 第2条适用于搜索列表、歌单列表、排行榜列表、我的列表中的歌曲
4. 对于歌单详情列表除了可以使用第2条的方式播放外你可以点击详情页上面的播放按钮临时播放当前歌单或点击收藏将当前歌单收藏到“我的列表”后再去播放
5. 对于排行榜详情列表除了可以使用第2条的方式播放外你可以在右击排行榜名字后弹出的菜单中播放或收藏整个排行榜这与第四条的歌单中的播放、与收藏按钮功能一致
6. v1.18.0及之后新增了“双击列表里的歌曲时自动切换到当前列表播放”设置,默认关闭,此功能仅对歌单、排行榜有效
## 歌曲无法试听与下载
### 所有歌曲都提示 `请求异常😮,可以多试几次,若还是不行就换一首吧。。。`
@ -101,6 +110,9 @@
需要注意的是:这将会覆盖本地的目标列表,歌曲将被替换成最新的在线列表。
## 调整我的列表的列表顺序
按住Ctrl键Mac上对应Command键的时候将进入“拖动模式”此时可以拖动列表的位置来调整顺序。
## 同步功能的使用(实验性,首次使用前建议先备份一次列表)
@ -305,16 +317,34 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看上面的
- URL统一以`lxmusic://`开头
- 此技术目前只支持 Windows、Mac系统
- URL传参以经过URL编码的JSON数据传参`lxmusic://music/play?data=xxxx`,其中`xxxx`为经过URL编码后的JSON数据
- 若无特别说明,源的可用值为:`kw/kg/tx/wy/mg`
- 若无特别说明,音质的可用值为:`128k/320k/flac/flac32bit`
目前支持两种传参方式:
- 通过`data`传参以经过URL编码的JSON数据传参`lxmusic://music/play?data=xxxx`,其中`xxxx`为经过URL编码后的JSON数据支持复杂的参数调用
- 通过`URL`传参适用于简单传参的调用不需要转成JSON格式`lxmusic://music/search/xxxx`但仍然需要对数据进行URL编码只适应于简单参数调用v1.18.0新增)
### `data`方式传参
以经过URL编码的JSON数据传参`lxmusic://music/play?data=xxxx`,其中`xxxx`为经过URL编码后的JSON数据JSON数据内容取决于下表的参数部分
| 描述 | URL | 参数
| --- | --- | ---
| 打开歌单 | `songlist/open` | `source<String>`(源,必须)<br>`id<String/Number>`歌单ID可选<br>`url<String>`歌单URL可选其中ID与URL必需传一个
| 播放歌单 | `songlist/play` | `source<String>`(源,必须)<br>`id<String/Number>`歌单ID可选<br>`url<String>`歌单URL可选其中`id`与`url`必需传一个<br>`index<Number>`播放第几首歌可选从0开始
| 搜索歌曲 | `music/search` | `keywords<String/Number>`(要搜索的内容,必须)<br>`source<String>`(源,可选)
| 播放歌曲 | `music/play` | `name<String>`(歌曲名,必传)<br>`singer<String>`(艺术家名,必传)<br>`source<String>`(源,必传)<br>`songmid<String/Number>`歌曲ID必传<br>`img<String>`(歌曲图片链接,选传)<br>`albumId<String/Number>`歌曲专辑ID选传<br>`interval<String>`(格式化后的歌曲时长,选传,例:`03:55`<br>`albumName<String>`(歌曲专辑名称,选传)<br>`types<Object>`(歌曲可用音质数组,必传,<br>数组格式:`[{"type": "<音质>", size: "<格式化后的文件大小,选传>", hash: "<kg源必传>"}]`<br>例:`[{"type": "128k", size: "3.56M"}, {"type": "320k", size: null}]`<br><br>以下为平台特定参数:<br>`hash<String>`歌曲hashkg源必传<br>`strMediaMid<String>`歌曲strMediaMidtx源必传<br>`albumMid<String>`歌曲albumMidtx源专用选传<br>`copyrightId<String>`歌曲copyrightIdmg源必传<br>`lrcUrl<String>`歌曲lrcUrlmg源专用选传
### `URL`方式传参
由于URL传参只适用于简单传参场景所以目前只支持以下功能的调用
| 描述 | URL | 参数
| --- | --- | ---
| 搜索歌曲 | `music/search/{source}/{keywords}` | `source`(源,可选)<br>`keywords`(要搜索的内容,必须)<br>例:`music/search/kw/xxx`、`music/search/xxx`
| 打开歌单 | `songlist/open/{source}/{id/url}` | `source`(源,必须)<br>`id/url`歌单ID或歌单URL必须<br>例:`songlist/open/kw/123456`
## 自定义源脚本编写说明
文件请使用UTF-8编码格式编写脚本所用编程语言为JavaScript可以使用ES6+语法,脚本与应用的交互是使用类似事件收发的方式进行,这是一个基本的脚本例子:
@ -427,6 +457,7 @@ send(EVENT_NAMES.inited, {
| --- | ---
| `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、名字等信息
| `updateAlert` | 显示源更新弹窗,发送该事件时的参数:`{log, updateUrl}`<br>`log`:更新日志,必传,字符串类型,内容可以使用`\n`换行最大长度1024超过此长度后将被截取超出的部分<br>`updateUrl`更新地址用于引导用户去该地址更新源选传需为http协议的url地址最大长度1024<br>此事件每次运行脚本只能调用一次源版本v1.2.0新增)<br>例子:`lx.send(lx.EVENT_NAMES.updateAlert, { log: 'hello world', updateUrl: 'https://xxx.com' })`
#### `window.lx.on`

View File

@ -48,7 +48,8 @@
软件变化请查看:[更新日志](https://github.com/lyswhut/lx-music-desktop/blob/master/CHANGELOG.md)<br>
软件下载请转到:[发布页面](https://github.com/lyswhut/lx-music-desktop/releases)<br>
或者到网盘下载网盘内有MAC、windows版`https://www.lanzoui.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)<br>
移动版项目地址:<https://github.com/lyswhut/lx-music-mobile>
#### Scheme URL支持

735
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "lx-music-desktop",
"version": "1.17.1",
"version": "1.18.0",
"description": "一个免费的音乐查找助手",
"main": "./dist/electron/main.js",
"productName": "lx-music-desktop",
@ -173,32 +173,32 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.16.12",
"@babel/eslint-parser": "^7.16.5",
"@babel/core": "^7.17.5",
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-umd": "^7.16.7",
"@babel/plugin-transform-runtime": "^7.16.10",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.16.11",
"babel-loader": "^8.2.3",
"babel-preset-minify": "^0.5.1",
"browserslist": "^4.19.1",
"browserslist": "^4.19.3",
"cfonts": "^2.10.0",
"chalk": "^4.1.2",
"changelog-parser": "^2.8.0",
"copy-webpack-plugin": "^10.2.1",
"core-js": "^3.20.3",
"copy-webpack-plugin": "^10.2.4",
"core-js": "^3.21.1",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"css-loader": "^6.6.0",
"css-minimizer-webpack-plugin": "^3.4.1",
"del": "^6.0.0",
"electron": "^13.6.8",
"electron-builder": "^22.11.7",
"electron": "^13.6.9",
"electron-builder": "^22.14.13",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"electron-to-chromium": "^1.4.56",
"eslint": "^8.7.0",
"electron-to-chromium": "^1.4.73",
"eslint": "^8.10.0",
"eslint-config-standard": "^16.0.3",
"eslint-formatter-friendly": "^7.0.0",
"eslint-plugin-html": "^6.2.0",
@ -206,7 +206,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^8.4.0",
"eslint-plugin-vue": "^8.5.0",
"eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
@ -215,7 +215,7 @@
"markdown-it": "^12.3.2",
"mini-css-extract-plugin": "^2.5.3",
"node-loader": "^2.0.0",
"postcss": "^8.4.5",
"postcss": "^8.4.7",
"postcss-loader": "^6.2.1",
"postcss-pxtorem": "^6.0.0",
"pug": "^3.0.2",
@ -227,22 +227,22 @@
"svg-sprite-loader": "^6.0.11",
"svg-transform-loader": "^2.0.13",
"svgo-loader": "^3.0.0",
"terser-webpack-plugin": "^5.3.0",
"terser-webpack-plugin": "^5.3.1",
"url-loader": "^4.1.1",
"vue-loader": "^17.0.0",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.67.0",
"webpack": "^5.69.1",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.3",
"webpack-dev-server": "^4.7.4",
"webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"bufferutil": "^4.0.6",
"crypto-js": "^4.1.1",
"electron-log": "^4.4.5",
"electron-log": "^4.4.6",
"electron-store": "^8.0.1",
"electron-updater": "^4.6.1",
"electron-updater": "^4.6.5",
"font-list": "github:lyswhut/node-font-list#4edbb1933b49a9bac1eedd63a31da16b487fe57d",
"http-terminator": "^3.0.4",
"iconv-lite": "^0.6.3",
@ -256,8 +256,8 @@
"socket.io": "^4.4.1",
"sortablejs": "^1.14.0",
"utf-8-validate": "^5.0.8",
"vue": "^3.2.29",
"vue-i18n": "^9.2.0-beta.29",
"vue": "^3.2.31",
"vue-i18n": "^9.2.0-beta.30",
"vue-router": "^4.0.12",
"vuex": "^4.0.2"
}

View File

@ -1,12 +1,29 @@
### 新增
- 新增“双击列表里的歌曲时自动切换到当前列表播放”设置,此功能仅对歌单、排行榜有效,默认关闭
- 新增打开收藏的在线列表的对应平台详情页功能,可以在我的列表-列表右键菜单中使用
- 新增定时暂停播放功能,由于此功能大多数人可能不常用,所以将其放在设置-基本设置中
- 新增任务栏缩略图工具栏控制按钮此功能仅在Windows平台可用按钮分别为收藏/取消收藏(将歌曲添加到“我的收藏”列表)、上一曲、播放/暂停、下一曲
- 新增设置-基本设置-软件字体设置此设置可用于设置主界面的字体已知的问题Windows 7 下可能会出现字体列表为空的情况,这是当前系统的 Powershell 版本小于5.1导致的,请自行尝试看常见解决)
- 新增Scheme URL对音乐搜索的调用支持详情看常见问题-Scheme URL支持
- 新增Scheme URL以url传参的方式调用详情看常见问题-Scheme URL支持
- 自定义源新增更新弹窗方法,同时自定义源管理新增是否允许源显示更新弹窗设置(出于防止滥用考虑),当源作者想要通知用户源已更新时,可以调用此方法弹窗告诉用户,调用说明看常见问题-自定义源部分
### 优化
- 优化kw源英文与翻译歌词的匹配
- 过滤tx源某些不支持播放的歌曲解决播放此类内容会导致意外的问题
- 把歌曲的热门评论与最新评论拆分成两个列表显示
### 修复
- 修复快捷键与默认按键行为冲突的问题现在若将某些有默认行为的按键如在列表中上、下箭头、Home、End等键可以使列表滚动设置为快捷键时将禁用其默认行为
- 修复列表的聚焦问题,现在在列表中使用上、下箭头、空格等键滚动列表时不会导致滚动到一定距离后丢失焦点的问题
- 修复排行榜名字右击菜单的播放功能在播放非激活的列表时的列表获取问题
- 修复修改列表名时无法使用`Ctrl`键的问题
- 修复wy源某些歌曲获取歌词翻译的问题处理
- 修复下载功能的歌词换源时会进入死循环的问题
- 修复某些歌曲无法下载的问题
- 修复windows平台下软件目录存在`portable`文件夹时,仍会创建`C:\Users\<user>\AppData\Roaming\lx-music-desktop\Dictionaries\en-US-9-0.bdic`文件的问题现在不会再创建文件但仍会创建空目录Electron的问题目前暂无解决方法
- 修复播放器的停止逻辑问题
### 其他
- 更新electron到v13.6.8
- 更新electron到v13.6.9

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@ const path = require('path')
const os = require('os')
const defaultSetting = {
version: '1.0.48',
version: '1.0.51',
player: {
togglePlayMethod: 'listLoop',
highQuality: false,
@ -16,6 +16,8 @@ const defaultSetting = {
isPlayLxlrc: true,
isSavePlayTime: false,
audioVisualization: false,
waitPlayEndStop: true,
waitPlayEndStopTime: '',
},
desktopLyric: {
enable: false,
@ -36,6 +38,7 @@ const defaultSetting = {
},
},
list: {
isClickPlayList: false,
isShowAlbumName: true,
isShowSource: true,
isSaveScrollLocation: true,
@ -99,6 +102,7 @@ const defaultSetting = {
sourceId: 'kw',
apiSource: 'temp',
sourceNameType: 'alias',
font: '',
isShowAnimation: true,
randomAnimate: true,
ignoreVersion: null,

View File

@ -11,6 +11,9 @@ const names = {
clear_env_params_deeplink: 'clear_env_params_deeplink',
wait: 'wait',
wait_cancel: 'wait_cancel',
interval: 'interval',
interval_callback: 'interval_callback',
interval_cancel: 'interval_cancel',
open_dev_tools: 'open_dev_tools',
set_music_meta: 'set_music_meta',
@ -66,6 +69,8 @@ const names = {
request_user_api_cancel: 'request_user_api_cancel',
get_user_api_status: 'get_user_api_status',
user_api_status: 'user_api_status',
user_api_show_update_alert: 'user_api_show_update_alert',
user_api_set_allow_update_alert: 'user_api_set_allow_update_alert',
get_lyric: 'get_lyric',
save_lyric: 'save_lyric',
@ -81,6 +86,9 @@ const names = {
sync_action_list: 'sync_action_list',
sync_list: 'sync_list',
taskbar_set_thumbar_buttons: 'taskbar_set_thumbar_buttons',
taskbar_set_thumbnail_clip: 'taskbar_set_thumbnail_clip',
taskbar_on_thumbar_button_click: 'taskbar_on_thumbar_button_click',
},
winLyric: {
close: 'close',

View File

@ -118,6 +118,7 @@
"lists__remove_tip_button": "Yes, that's right",
"lists__rename": "Rename",
"lists__sort_list": "Sort songs",
"lists__source_detail": "Playlist Page",
"lists__sync": "Update",
"load_list_file_error_detail": "We have helped you back up the old list file to {path}\nIt is stored in JSON format, you can try to repair and restore it manually\n\nError details: {detail}",
"load_list_file_error_title": "Error loading playlist data",
@ -139,9 +140,18 @@
"my_list": "Your Library",
"no_item": "Nothing's here...",
"not_agree": "Not accept",
"ok": "OK",
"pagination__next": "Next page",
"pagination__page": "Page {num}",
"pagination__prev": "Previous page",
"play_timeout": "Timed pause",
"play_timeout_close": "Close",
"play_timeout_confirm": "Confirm",
"play_timeout_end": "Wait for the song to finish before pausing",
"play_timeout_stop": "Cancel timer",
"play_timeout_tip": "Pause after {time}",
"play_timeout_unit": "minute",
"play_timeout_update": "Update timing",
"player__add_music_to": "Add the current song to...",
"player__buffering": "Buffering...",
"player__desktop_lyric_lock": "Right click to lock lyrics",
@ -200,6 +210,7 @@
"setting__basic_control_btn_position": "Control Button Position",
"setting__basic_control_btn_position_left": "Left",
"setting__basic_control_btn_position_right": "Right",
"setting__basic_font": "Font",
"setting__basic_lang": "Language",
"setting__basic_lang_title": "The language displayed in the software",
"setting__basic_show_animation": "Show switching animation",
@ -284,6 +295,7 @@
"setting__list_add_music_location_type": "Position when adding a song to the list",
"setting__list_add_music_location_type_bottom": "Bottom",
"setting__list_add_music_location_type_top": "Top",
"setting__list_click_action": "Automatically switch to the current list when double-clicking a song in the list (only valid for playlists and rankings)",
"setting__list_scroll": "Remember the position of the scroll bar of the playlist (only valid for my music classification)",
"setting__list_source": "Show song source (only valid for my music category)",
"setting__network": "Network",
@ -320,6 +332,7 @@
"setting__play_quality": "Play 320K quality songs first (if supported)",
"setting__play_save_play_time": "Remember playback progress",
"setting__play_task_bar": "Show playing progress on the taskbar",
"setting__play_timeout": "Timed pause",
"setting__search": "Search",
"setting__search_focus_search_box": "Automatically focus the search box on startup",
"setting__search_history": "Search history",
@ -398,12 +411,16 @@
"theme_purple": "Purple",
"theme_red": "Red",
"theme_yellow": "Yellow",
"user_api__allow_show_update_alert": "Allow update popup to show",
"user_api__btn_export": "Export",
"user_api__btn_import": "Import",
"user_api__btn_remove": "Remove",
"user_api__import_file": "Select music API script file",
"user_api__max_tip": "There can only be a maximum of 20 sources at the same time🤪\nIf you want to continue importing, please remove some old sources to make room",
"user_api__noitem": "There is nothing here...😲",
"user_api__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.",
"user_api__readme": "Source writing instructions: ",
"user_api__title": "Custom Source Management"
"user_api__title": "Custom Source Management",
"user_api__update_alert": "Custom source [{name}] found new version:",
"user_api__update_alert_open_url": "Open update address"
}

View File

@ -118,6 +118,7 @@
"lists__remove_tip_button": "是的 没错",
"lists__rename": "重命名",
"lists__sort_list": "排序歌曲",
"lists__source_detail": "歌单详情页",
"lists__sync": "更新",
"load_list_file_error_detail": "我们已经帮你把旧的列表文件备份到{path}\n它以 JSON 格式存储,你可以尝试手动修复并恢复它\n\n错误详情{detail}",
"load_list_file_error_title": "播放列表数据加载错误建议到GitHub或加群反馈",
@ -139,9 +140,18 @@
"my_list": "我的列表",
"no_item": "列表竟然是空的...",
"not_agree": "不接受",
"ok": "我知道了",
"pagination__next": "下一页",
"pagination__page": "第 {num} 页",
"pagination__prev": "上一页",
"play_timeout": "定时暂停",
"play_timeout_close": "关闭",
"play_timeout_confirm": "确认",
"play_timeout_end": "等待歌曲播放完毕再暂停",
"play_timeout_stop": "取消定时",
"play_timeout_tip": "{time} 后暂停播放",
"play_timeout_unit": "分钟",
"play_timeout_update": "更新定时",
"player__add_music_to": "添加当前歌曲到...",
"player__buffering": "缓冲中...",
"player__desktop_lyric_lock": "右击锁定歌词",
@ -200,6 +210,7 @@
"setting__basic_control_btn_position": "控制按钮位置",
"setting__basic_control_btn_position_left": "左边",
"setting__basic_control_btn_position_right": "右边",
"setting__basic_font": "字体",
"setting__basic_lang": "语言",
"setting__basic_lang_title": "软件显示的语言",
"setting__basic_show_animation": "显示切换动画",
@ -284,6 +295,7 @@
"setting__list_add_music_location_type": "添加歌曲到列表时的位置",
"setting__list_add_music_location_type_bottom": "底部",
"setting__list_add_music_location_type_top": "顶部",
"setting__list_click_action": "双击列表里的歌曲时自动切换到当前列表播放(仅对歌单、排行榜有效)",
"setting__list_scroll": "记住播放列表滚动条位置(仅对我的音乐分类有效)",
"setting__list_source": "显示歌曲源(仅对我的音乐分类有效)",
"setting__network": "网络设置",
@ -320,6 +332,7 @@
"setting__play_quality": "优先播放320K品质的歌曲如果支持",
"setting__play_save_play_time": "记住播放进度",
"setting__play_task_bar": "在任务栏上显示当前歌曲播放进度",
"setting__play_timeout": "定时暂停",
"setting__search": "搜索设置",
"setting__search_focus_search_box": "启动时自动聚焦搜索框",
"setting__search_history": "显示历史搜索记录",
@ -398,12 +411,16 @@
"theme_purple": "重斤球紫",
"theme_red": "热情似火",
"theme_yellow": "信口雌黄",
"user_api__allow_show_update_alert": "允许显示更新弹窗",
"user_api__btn_export": "导出",
"user_api__btn_import": "导入",
"user_api__btn_remove": "移除",
"user_api__import_file": "选择音乐API脚本文件",
"user_api__max_tip": "最多只能同时存在20个源哦🤪\n想要继续导入的话请先移除一些旧的源腾出位置吧",
"user_api__noitem": "这里竟然是空的 😲",
"user_api__note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。",
"user_api__readme": "源编写说明:",
"user_api__title": "自定义源管理"
"user_api__title": "自定义源管理",
"user_api__update_alert": "自定义源 [{name}] 发现新版本:",
"user_api__update_alert_open_url": "打开更新地址"
}

View File

@ -118,6 +118,7 @@
"lists__remove_tip_button": "是的 沒錯",
"lists__rename": "重命名",
"lists__sort_list": "排序歌曲",
"lists__source_detail": "歌單詳情頁",
"lists__sync": "更新",
"load_list_file_error_detail": "我們已經幫你把舊的列表文件備份到{path}\n它以 JSON 格式存儲,你可以嘗試手動修復並恢復它\n\n錯誤詳情{detail}",
"load_list_file_error_title": "播放列表數據加載錯誤",
@ -139,9 +140,18 @@
"my_list": "我的列表",
"no_item": "列表竟然是空的...",
"not_agree": "不接受",
"ok": "我知道了",
"pagination__next": "下一頁",
"pagination__page": "第 {num} 頁",
"pagination__prev": "上一頁",
"play_timeout": "定時暫停",
"play_timeout_close": "關閉",
"play_timeout_confirm": "確認",
"play_timeout_end": "等待歌曲播放完畢再暫停",
"play_timeout_stop": "取消定時",
"play_timeout_tip": "{time} 後暫停播放",
"play_timeout_unit": "分鐘",
"play_timeout_update": "更新定時",
"player__add_music_to": "添加當前歌曲到...",
"player__album": "專輯名:",
"player__buffering": "緩衝中...",
@ -200,6 +210,7 @@
"setting__basic_control_btn_position": "控制按鈕位置",
"setting__basic_control_btn_position_left": "左邊",
"setting__basic_control_btn_position_right": "右邊",
"setting__basic_font": "字體",
"setting__basic_lang": "語言",
"setting__basic_lang_title": "軟件顯示的語言",
"setting__basic_show_animation": "顯示切換動畫",
@ -284,6 +295,7 @@
"setting__list_add_music_location_type": "添加歌曲到列表時的位置",
"setting__list_add_music_location_type_bottom": "底部",
"setting__list_add_music_location_type_top": "頂部",
"setting__list_click_action": "雙擊列表裡的歌曲時自動切換到當前列表播放(僅對歌單、排行榜有效)",
"setting__list_scroll": "記住播放列表滾動條位置(僅對我的音樂分類有效)",
"setting__list_source": "顯示歌曲源(僅對我的音樂分類有效)",
"setting__network": "網絡設置",
@ -320,6 +332,7 @@
"setting__play_quality": "優先播放320K品質的歌曲如果支持",
"setting__play_save_play_time": "記住播放進度",
"setting__play_task_bar": "在任務欄上顯示當前歌曲播放進度",
"setting__play_timeout": "定時暫停",
"setting__search": "搜索設置",
"setting__search_focus_search_box": "啟動時自動聚焦搜索框",
"setting__search_history": "顯示歷史搜索記錄",
@ -398,12 +411,16 @@
"theme_purple": "重斤球紫",
"theme_red": "熱情似火",
"theme_yellow": "信口雌黃",
"user_api__allow_show_update_alert": "允許顯示更新彈窗",
"user_api__btn_export": "導出",
"user_api__btn_import": "導入",
"user_api__btn_remove": "移除",
"user_api__import_file": "選擇音樂API腳本文件",
"user_api__max_tip": "最多只能同時存在20個源哦🤪\n想要繼續導入的話請先移除一些舊的源騰出位置吧",
"user_api__noitem": "這裡竟然是空的 😲",
"user_api__note": "提示:雖然我們已經盡可能地隔離了腳本的運行環境,但導入包含惡意行為的腳本仍可能會影響你的系統,請謹慎導入。",
"user_api__readme": "源編寫說明:",
"user_api__title": "自定義源管理"
"user_api__title": "自定義源管理",
"user_api__update_alert": "自定義源 [{name}] 發現新版本:",
"user_api__update_alert_open_url": "打開更新地址"
}

View File

@ -6,6 +6,7 @@ const Tray = require('./Tray')
const WinLyric = require('./WinLyric')
const HotKey = require('./HotKey')
const { Event: TaskBar } = require('../modules/taskbar')
const { Event: UserApi } = require('../modules/userApi')
const { Event: Sync } = require('../modules/sync')
@ -15,5 +16,6 @@ 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.taskbar) global.lx_event.taskbar = new TaskBar()
if (!global.lx_event.userApi) global.lx_event.userApi = new UserApi()
if (!global.lx_event.sync) global.lx_event.sync = new Sync()

View File

@ -129,6 +129,10 @@ app.on('web-contents-created', (event, contents) => {
event.preventDefault()
}
})
// disable create dictionary
// https://github.com/lyswhut/lx-music-desktop/issues/773
contents.session.setSpellCheckerDictionaryDownloadURL('http://0.0.0.0')
})

View File

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

View File

@ -0,0 +1,11 @@
const { EventEmitter } = require('events')
const TASKBAR_EVENT_NAME = require('./name')
class TaskBar extends EventEmitter {
thumbarButtonClick(type) {
this.emit(TASKBAR_EVENT_NAME.thumbarButtonClick, type)
}
}
module.exports = TaskBar

View File

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

View File

@ -0,0 +1,92 @@
const path = require('path')
const Event = require('./event/event')
const eventNames = require('./event/name')
exports.Event = Event
exports.eventNames = eventNames
exports.setThumbnailClip = (region = { x: 0, y: 0, width: 0, height: 0 }) => {
if (!global.modules.mainWindow) return
return global.modules.mainWindow.setThumbnailClip(region)
}
const getIconPath = name => {
return path.join(global.__static, 'images/taskbar', name + '.png')
}
const buttonsFlags = {
empty: true,
collect: false,
play: false,
next: true,
prev: true,
}
const createButtons = ({ empty = false, collect = false, play = false, next = true, prev = true }) => {
const buttons = [
collect
? {
icon: getIconPath('collected'),
click() {
global.lx_event.taskbar.thumbarButtonClick('unCollect')
},
tooltip: '取消收藏',
flags: ['nobackground'],
}
: {
icon: getIconPath('collect'),
click() {
global.lx_event.taskbar.thumbarButtonClick('collect')
},
tooltip: '收藏',
flags: ['nobackground'],
},
{
icon: getIconPath('prev'),
click() {
global.lx_event.taskbar.thumbarButtonClick('prev')
},
tooltip: '上一曲',
flags: prev ? ['nobackground'] : ['nobackground', 'disabled'],
},
play
? {
icon: getIconPath('pause'),
click() {
global.lx_event.taskbar.thumbarButtonClick('pause')
},
tooltip: '暂停',
flags: ['nobackground'],
}
: {
icon: getIconPath('play'),
click() {
global.lx_event.taskbar.thumbarButtonClick('play')
},
tooltip: '播放',
flags: ['nobackground'],
},
{
icon: getIconPath('next'),
click() {
global.lx_event.taskbar.thumbarButtonClick('next')
},
tooltip: '下一曲',
flags: next ? ['nobackground'] : ['nobackground', 'disabled'],
},
]
if (empty) {
for (const button of buttons) {
button.flags = ['nobackground', 'disabled']
}
}
return buttons
}
exports.setThumbarButtons = ({ empty, collect, play, next, prev } = buttonsFlags) => {
if (!global.modules.mainWindow) return
buttonsFlags.empty = empty
buttonsFlags.collect = collect
buttonsFlags.play = play
buttonsFlags.next = next
buttonsFlags.prev = prev
global.modules.mainWindow.setThumbarButtons(createButtons(buttonsFlags))
}

View File

@ -5,6 +5,10 @@ class UserApi extends EventEmitter {
status(info) {
this.emit(USER_API_EVENT_NAME.status, info)
}
showUpdateAlert(info) {
this.emit(USER_API_EVENT_NAME.showUpdateAlert, info)
}
}
module.exports = UserApi

View File

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

View File

@ -1,8 +1,8 @@
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 { getUserApis, importApi, removeApi, setAllowShowUpdateAlert: saveAllowShowUpdateAlert } = require('./utils')
const { request, cancelRequest, getStatus, loadApi, setAllowShowUpdateAlert } = require('./rendererEvent/rendererEvent')
// const { getApiList, importApi, removeApi, setApi, getStatus, request, eventNames }
let userApiId
@ -41,4 +41,7 @@ exports.setApi = async id => {
await loadApi(id)
}
exports.setAllowShowUpdateAlert = (id, enable) => {
saveAllowShowUpdateAlert(id, enable)
setAllowShowUpdateAlert(id, enable)
}

View File

@ -15,7 +15,6 @@ fs.readFile(path.join(dir, 'renderer/user-api.html'), 'utf8', (err, data) => {
})
const denyEvents = [
'new-window',
'will-navigate',
'will-redirect',
'will-attach-webview',
@ -49,11 +48,13 @@ exports.createWindow = async userApi => {
contextIsolation: true,
// worldSafeExecuteJavaScript: true,
nodeIntegration: false,
nodeIntegrationInWorker: false,
spellcheck: false,
autoplayPolicy: 'document-user-activation-required',
enableWebSQL: false,
disableDialogs: true,
nativeWindowOpen: false,
webgl: false,
images: false,
@ -70,6 +71,9 @@ exports.createWindow = async userApi => {
// eslint-disable-next-line node/no-callback-literal
callback(false)
})
global.modules.userApiWindow.webContents.setWindowOpenHandler(() => {
return { action: 'deny' }
})
winEvent(global.modules.userApiWindow)

View File

@ -3,14 +3,16 @@ 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 })
const sendMessage = (action, data, status, message) => {
ipcRenderer.send(action, { data, status, message })
}
let isInitedApi = false
let isShowedUpdateAlert = false
const EVENT_NAMES = {
request: 'request',
inited: 'inited',
updateAlert: 'updateAlert',
}
const eventNames = Object.values(EVENT_NAMES)
const events = {
@ -35,7 +37,7 @@ const supportActions = {
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')
if (!events.request) return sendMessage(USER_API_RENDERER_EVENT_NAME.response, { requestKey }, false, 'Request event is not defined')
try {
events.request.call(context, { source: data.source, action: data.action, info: data.info }).then(response => {
let sendData = {
@ -53,12 +55,12 @@ const handleRequest = (context, { requestKey, data }) => {
}
break
}
sendMessage(USER_API_RENDERER_EVENT_NAME.response, true, sendData)
sendMessage(USER_API_RENDERER_EVENT_NAME.response, sendData, true)
}).catch(err => {
sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, err.message)
sendMessage(USER_API_RENDERER_EVENT_NAME.response, { requestKey }, false, err.message)
})
} catch (err) {
sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, err.message)
sendMessage(USER_API_RENDERER_EVENT_NAME.response, { requestKey }, false, err.message)
}
}
@ -80,7 +82,7 @@ const handleRequest = (context, { requestKey, data }) => {
*/
const handleInit = (context, info) => {
if (!info) {
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, 'Init failed')
sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, 'Init failed')
// sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
return
}
@ -88,7 +90,7 @@ const handleInit = (context, info) => {
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, null, false, 'Init failed')
// sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
return
}
@ -109,16 +111,28 @@ const handleInit = (context, info) => {
}
} catch (error) {
console.log(error)
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, error.message)
sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, error.message)
return
}
sendMessage(USER_API_RENDERER_EVENT_NAME.init, true, sourceInfo)
sendMessage(USER_API_RENDERER_EVENT_NAME.init, sourceInfo, true)
ipcRenderer.on(USER_API_RENDERER_EVENT_NAME.request, (event, data) => {
handleRequest(context, data)
})
}
const handleShowUpdateAlert = (data, resolve, reject) => {
if (!data || typeof data != 'object') return reject(new Error('parameter format error.'))
if (!data.log || typeof data.log != 'string') return reject(new Error('log is required.'))
if (data.updateUrl && !/^https?:\/\/[^\s$.?#].[^\s]*$/.test(data.updateUrl) && data.updateUrl.length > 1024) delete data.updateUrl
if (data.log.length > 1024) data.log = data.log.substring(0, 1024) + '...'
sendMessage(USER_API_RENDERER_EVENT_NAME.showUpdateAlert, {
log: data.log,
updateUrl: data.updateUrl,
})
resolve()
}
contextBridge.exposeInMainWorld('lx', {
EVENT_NAMES,
request(url, { method = 'get', timeout, headers, body, form, formData }, callback) {
@ -165,12 +179,18 @@ contextBridge.exposeInMainWorld('lx', {
if (!eventNames.includes(eventName)) return reject(new Error('The event is not supported: ' + eventName))
switch (eventName) {
case EVENT_NAMES.inited:
if (isInitedApi) return
if (isInitedApi) return reject(new Error('Script is inited'))
isInitedApi = true
handleInit(this, data)
resolve()
break
case EVENT_NAMES.updateAlert:
if (isShowedUpdateAlert) return reject(new Error('The update alert can only be called once.'))
isShowedUpdateAlert = true
handleShowUpdateAlert(data, resolve, reject)
break
default:
resolve(new Error('Unknown event name: ' + eventName))
reject(new Error('Unknown event name: ' + eventName))
}
})
},
@ -180,7 +200,9 @@ contextBridge.exposeInMainWorld('lx', {
case EVENT_NAMES.request:
events.request = handler
break
default: return Promise.reject(new Error('The event is not supported: ' + eventName))
}
return Promise.resolve()
},
utils: {
crypto: {
@ -208,7 +230,7 @@ contextBridge.exposeInMainWorld('lx', {
},
},
},
version: '1.1.0',
version: '1.2.0',
// removeEvent(eventName, handler) {
// if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
// let handlers

View File

@ -3,6 +3,7 @@ const names = {
request: '',
response: '',
openDevTools: '',
showUpdateAlert: '',
}

View File

@ -3,6 +3,7 @@ const { mainOn, mainSend } = require('@common/ipc')
const USER_API_RENDERER_EVENT_NAME = require('../rendererEvent/name')
const { createWindow } = require('../main')
const { getUserApis } = require('../utils')
const { openDevTools } = require('@main/utils')
let userApi
let status = { status: true }
@ -32,14 +33,22 @@ const handleResponse = (event, { status, data: { requestKey, result }, message }
}
const handleOpenDevTools = () => {
if (global.modules.userApiWindow) {
global.modules.userApiWindow.webContents.openDevTools({
mode: 'undocked',
})
openDevTools(global.modules.userApiWindow.webContents)
}
}
const handleShowUpdateAlert = (event, { data }) => {
if (!userApi.allowShowUpdateAlert) return
global.lx_event.userApi.showUpdateAlert({
name: userApi.name,
description: userApi.description,
log: data.log,
updateUrl: data.updateUrl,
})
}
mainOn(USER_API_RENDERER_EVENT_NAME.init, handleInit)
mainOn(USER_API_RENDERER_EVENT_NAME.response, handleResponse)
mainOn(USER_API_RENDERER_EVENT_NAME.openDevTools, handleOpenDevTools)
mainOn(USER_API_RENDERER_EVENT_NAME.showUpdateAlert, handleShowUpdateAlert)
exports.loadApi = async apiId => {
if (!apiId) return global.lx_event.userApi.status(status = { status: false, message: 'api id is null' })
@ -82,3 +91,8 @@ exports.request = ({ requestKey, data }) => new Promise((resolve, reject) => {
})
exports.getStatus = () => status
exports.setAllowShowUpdateAlert = (id, enable) => {
if (!userApi || userApi.id != id) return
userApi.allowShowUpdateAlert = enable
}

View File

@ -11,6 +11,9 @@ exports.getUserApis = () => {
userApis = defaultUserApis
electronStore_userApi.set('userApis', userApis)
}
for (const api of userApis) {
if (api.allowShowUpdateAlert == null) api.allowShowUpdateAlert = false
}
return userApis
}
@ -27,6 +30,7 @@ exports.importApi = script => {
name,
description,
script,
allowShowUpdateAlert: true,
}
userApis.push(apiInfo)
getStore('userApi').set('userApis', userApis)
@ -42,3 +46,10 @@ exports.removeApi = ids => {
}
getStore('userApi').set('userApis', userApis)
}
exports.setAllowShowUpdateAlert = (id, enable) => {
const targetApi = userApis.find(api => api.id == id)
if (!targetApi) return
targetApi.allowShowUpdateAlert = enable
getStore('userApi').set('userApis', userApis)
}

View File

@ -1,4 +1,4 @@
const { isWin } = require('@common/utils')
// require('./request')
// require('./appName')
require('./progressBar')
@ -23,6 +23,8 @@ require('./systemFonts')
require('./wait')
require('./openDevtools')
if (isWin) require('./taskbar')
// require('./kw_decodeLyric')
require('./userApi')

View File

@ -1,13 +1,12 @@
const { mainOn, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
const { openDevTools } = require('@main/utils')
mainOn(ipcMainWindowNames.open_dev_tools, event => {
if (global.modules.mainWindow) {
if (global.modules.mainWindow.isDevToolsOpened()) {
global.modules.mainWindow.webContents.closeDevTools()
} else {
global.modules.mainWindow.webContents.openDevTools({
mode: 'undocked',
})
openDevTools(global.modules.mainWindow.webContents)
}
}
})

View File

@ -0,0 +1,18 @@
const { mainSend, mainOn, NAMES: { mainWindow: ipcMainWindowNames }, mainHandle } = require('@common/ipc')
const { setThumbnailClip, setThumbarButtons, eventNames } = require('../modules/taskbar')
const { mainWindow: MAIN_WINDOW_EVENT_NAME } = require('@main/events/_name')
const handleThumbarButtonClick = action => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.taskbar_on_thumbar_button_click, action)
}
global.lx_event.taskbar.on(eventNames.thumbarButtonClick, handleThumbarButtonClick)
global.lx_event.mainWindow.on(MAIN_WINDOW_EVENT_NAME.show, setThumbarButtons)
mainHandle(ipcMainWindowNames.taskbar_set_thumbnail_clip, (event, clip) => {
return setThumbnailClip(clip)
})
mainOn(ipcMainWindowNames.taskbar_set_thumbar_buttons, (event, buttons) => {
setThumbarButtons(buttons)
})

View File

@ -1,11 +1,15 @@
const { mainSend, mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('@common/ipc')
const { getApiList, importApi, removeApi, setApi, getStatus, request, cancelRequest, eventNames } = require('../modules/userApi')
const { getApiList, importApi, removeApi, setApi, getStatus, request, cancelRequest, eventNames, setAllowShowUpdateAlert } = require('../modules/userApi')
const handleStatusChange = status => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.user_api_status, status)
}
const handleShowUpdateAlert = info => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.user_api_show_update_alert, info)
}
global.lx_event.userApi.on(eventNames.status, handleStatusChange)
global.lx_event.userApi.on(eventNames.showUpdateAlert, handleShowUpdateAlert)
mainHandle(ipcMainWindowNames.import_user_api, async(event, script) => {
return importApi(script)
@ -27,6 +31,10 @@ mainHandle(ipcMainWindowNames.get_user_api_status, event => {
return getStatus()
})
mainHandle(ipcMainWindowNames.user_api_set_allow_update_alert, (event, { id, enable }) => {
return setAllowShowUpdateAlert(id, enable)
})
mainHandle(ipcMainWindowNames.request_user_api, (event, musicInfo) => {
return request(musicInfo)
})

View File

@ -1,4 +1,4 @@
const { mainOn, mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('@common/ipc')
const { mainOn, mainHandle, mainSend, NAMES: { mainWindow: ipcMainWindowNames } } = require('@common/ipc')
const timeoutMap = new Map()
@ -23,3 +23,24 @@ mainOn(ipcMainWindowNames.wait_cancel, (event, id) => {
clearTimeout(timeout.timeout)
timeout.reject('cancelled')
})
mainOn(ipcMainWindowNames.interval, (event, { time, id }) => {
if (timeoutMap.has(id)) return
const timeout = setInterval(() => {
if (global.modules.mainWindow) mainSend(global.modules.mainWindow, ipcMainWindowNames.interval_callback, id)
}, time)
timeoutMap.set(id, {
timeout,
type: 'interval',
time,
})
})
mainOn(ipcMainWindowNames.interval_cancel, (event, id) => {
if (!timeoutMap.has(id)) return
const timeout = timeoutMap.get(id)
timeoutMap.delete(id)
if (timeout.type != 'interval') return
clearInterval(timeout.timeout)
})

View File

@ -1,4 +1,5 @@
const { isWin } = require('../../common/utils')
const { openDevTools } = require('@main/utils')
const { mainSend, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
global.isQuitting = false
global.isTrafficLightClose = false // 是否点击软件上的关闭按钮关闭
@ -30,6 +31,7 @@ module.exports = mainWindow => {
mainWindow.once('ready-to-show', () => {
mainWindow.show()
global.lx_event.mainWindow.readyToShow()
if (global.envParams.cmdParams.debug) openDevTools(global.modules.mainWindow.webContents)
})
mainWindow.on('show', () => {

View File

@ -41,3 +41,9 @@ exports.initSetting = (isShowErrorAlert = true) => {
global.appSetting = info.setting
global.appSettingVersion = info.version
}
exports.openDevTools = webContents => {
webContents.openDevTools({
mode: 'undocked',
})
}

View File

@ -23,6 +23,9 @@ article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
html {
font-family: "Microsoft YaHei", "Microsoft Jhenghei", "PingFang SC", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Source Han Sans", "Noto Sans CJK Sc", sans-serif;
}
body {
line-height: 1;
}
@ -41,3 +44,6 @@ table {
border-collapse: collapse;
border-spacing: 0;
}
button {
font-family: inherit;
}

View File

@ -20,13 +20,20 @@ import useApp from '@renderer/core/useApp'
export default {
setup() {
const theme = useRefGetter('theme')
const font = useRefGetter('font')
const dom_root = document.getElementById('root')
watch(theme, (val) => {
dom_root.className = val
}, {
immediate: true,
})
watch(font, (val) => {
document.documentElement.style.fontFamily = val
}, {
immediate: true,
})
dom_root.className = theme.value
useApp()

View File

@ -23,6 +23,9 @@ article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
html {
font-family: "Microsoft YaHei", "Microsoft Jhenghei", "PingFang SC", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Source Han Sans", "Noto Sans CJK Sc", sans-serif;
}
body {
line-height: 1;
}
@ -41,3 +44,6 @@ table {
border-collapse: collapse;
border-spacing: 0;
}
button {
font-family: inherit;
}

View File

@ -49,7 +49,7 @@
@color-player-pic-c2: darken(@color-theme_2, 30%);
@color-player-progress: darken(@color-theme_2, 6%);
@color-player-progress-bar1: darken(@color-theme_2, 12%);
@color-player-progress-bar2: lighten(@color-theme, 12%);
@color-player-progress-bar2: fadeout(lighten(@color-theme, 12%), 20%);
@color-player-status-text: lighten(@color-theme_2-font, 32%);
@color-player-detail-lyric: fadeout(@color-theme_2-font, 40%);
@color-player-detail-lyric-active: darken(@color-theme, 2%);
@ -114,7 +114,7 @@
@color-green-player-pic-c2: darken(@color-green-theme_2, 30%);
@color-green-player-progress: darken(@color-green-theme_2, 6%);
@color-green-player-progress-bar1: darken(@color-green-theme_2, 12%);
@color-green-player-progress-bar2: lighten(@color-green-theme, 12%);
@color-green-player-progress-bar2: fadeout(lighten(@color-green-theme, 12%), 20%);
@color-green-player-status-text: lighten(@color-green-theme_2-font, 32%);
@color-green-player-detail-lyric: fadeout(@color-green-theme_2-font, 40%);
@color-green-player-detail-lyric-active: darken(@color-green-theme, 2%);
@ -174,7 +174,7 @@
@color-yellow-player-pic-c2: darken(@color-yellow-theme_2, 30%);
@color-yellow-player-progress: darken(@color-yellow-theme_2, 6%);
@color-yellow-player-progress-bar1: darken(@color-yellow-theme_2, 12%);
@color-yellow-player-progress-bar2: lighten(@color-yellow-theme, 2%);
@color-yellow-player-progress-bar2: fadeout(lighten(@color-yellow-theme, 2%), 20%);
@color-yellow-player-status-text: lighten(@color-yellow-theme_2-font, 32%);
@color-yellow-player-detail-lyric: fadeout(@color-yellow-theme_2-font, 40%);
@color-yellow-player-detail-lyric-active: darken(@color-yellow-theme, 7%);
@ -233,7 +233,7 @@
@color-orange-player-pic-c2: darken(@color-orange-theme_2, 30%);
@color-orange-player-progress: darken(@color-orange-theme_2, 6%);
@color-orange-player-progress-bar1: darken(@color-orange-theme_2, 12%);
@color-orange-player-progress-bar2: lighten(@color-orange-theme, 12%);
@color-orange-player-progress-bar2: fadeout(lighten(@color-orange-theme, 12%), 20%);
@color-orange-player-status-text: lighten(@color-orange-theme_2-font, 32%);
@color-orange-player-detail-lyric: fadeout(@color-orange-theme_2-font, 40%);
@color-orange-player-detail-lyric-active: darken(@color-orange-theme, 7%);
@ -292,7 +292,7 @@
@color-blue-player-pic-c2: darken(@color-blue-theme_2, 30%);
@color-blue-player-progress: darken(@color-blue-theme_2, 6%);
@color-blue-player-progress-bar1: darken(@color-blue-theme_2, 12%);
@color-blue-player-progress-bar2: lighten(@color-blue-theme, 12%);
@color-blue-player-progress-bar2: fadeout(lighten(@color-blue-theme, 12%), 20%);
@color-blue-player-status-text: lighten(@color-blue-theme_2-font, 32%);
@color-blue-player-detail-lyric: fadeout(@color-blue-theme_2-font, 40%);
@color-blue-player-detail-lyric-active: darken(@color-blue-theme, 2%);
@ -351,7 +351,7 @@
@color-red-player-pic-c2: darken(@color-red-theme_2, 30%);
@color-red-player-progress: darken(@color-red-theme_2, 6%);
@color-red-player-progress-bar1: darken(@color-red-theme_2, 12%);
@color-red-player-progress-bar2: lighten(@color-red-theme, 12%);
@color-red-player-progress-bar2: fadeout(lighten(@color-red-theme, 12%), 20%);
@color-red-player-status-text: lighten(@color-red-theme_2-font, 32%);
@color-red-player-detail-lyric: fadeout(@color-red-theme_2-font, 40%);
@color-red-player-detail-lyric-active: lighten(@color-red-theme, 2%);
@ -412,7 +412,7 @@
@color-pink-player-pic-c2: darken(@color-pink-theme_2, 30%);
@color-pink-player-progress: darken(@color-pink-theme_2, 6%);
@color-pink-player-progress-bar1: darken(@color-pink-theme_2, 12%);
@color-pink-player-progress-bar2: lighten(@color-pink-theme, 2%);
@color-pink-player-progress-bar2: fadeout(lighten(@color-pink-theme, 2%), 20%);
@color-pink-player-status-text: lighten(@color-pink-theme_2-font, 32%);
@color-pink-player-detail-lyric: fadeout(@color-pink-theme_2-font, 40%);
@color-pink-player-detail-lyric-active: darken(@color-pink-theme, 2%);
@ -471,7 +471,7 @@
@color-purple-player-pic-c2: darken(@color-purple-theme_2, 30%);
@color-purple-player-progress: darken(@color-purple-theme_2, 6%);
@color-purple-player-progress-bar1: darken(@color-purple-theme_2, 12%);
@color-purple-player-progress-bar2: lighten(@color-purple-theme, 12%);
@color-purple-player-progress-bar2: fadeout(lighten(@color-purple-theme, 12%), 20%);
@color-purple-player-status-text: lighten(@color-purple-theme_2-font, 32%);
@color-purple-player-detail-lyric: fadeout(@color-purple-theme_2-font, 40%);
@color-purple-player-detail-lyric-active: darken(@color-purple-theme, 2%);
@ -530,7 +530,7 @@
@color-grey-player-pic-c2: darken(@color-grey-theme_2, 30%);
@color-grey-player-progress: darken(@color-grey-theme_2, 6%);
@color-grey-player-progress-bar1: darken(@color-grey-theme_2, 12%);
@color-grey-player-progress-bar2: lighten(@color-grey-theme, 12%);
@color-grey-player-progress-bar2: fadeout(lighten(@color-grey-theme, 12%), 20%);
@color-grey-player-status-text: lighten(@color-grey-theme_2-font, 32%);
@color-grey-player-detail-lyric: fadeout(@color-grey-theme_2-font, 55%);
@color-grey-player-detail-lyric-active: darken(@color-grey-theme, 3%);
@ -590,7 +590,7 @@
@color-ming-player-pic-c2: darken(@color-ming-theme_2, 30%);
@color-ming-player-progress: darken(@color-ming-theme_2, 6%);
@color-ming-player-progress-bar1: darken(@color-ming-theme_2, 12%);
@color-ming-player-progress-bar2: lighten(@color-ming-theme, 12%);
@color-ming-player-progress-bar2: fadeout(lighten(@color-ming-theme, 12%), 20%);
@color-ming-player-status-text: lighten(@color-ming-theme_2-font, 32%);
@color-ming-player-detail-lyric: fadeout(@color-ming-theme_2-font, 50%);
@color-ming-player-detail-lyric-active: lighten(@color-ming-theme, 2%);
@ -652,7 +652,7 @@
@color-blue2-player-pic-c2: darken(@color-blue2-theme_2, 30%);
@color-blue2-player-progress: darken(@color-blue2-theme_2, 6%);
@color-blue2-player-progress-bar1: darken(@color-blue2-theme_2, 12%);
@color-blue2-player-progress-bar2: lighten(@color-blue2-theme, 12%);
@color-blue2-player-progress-bar2: fadeout(lighten(@color-blue2-theme, 12%), 20%);
@color-blue2-player-status-text: lighten(@color-blue2-theme_2-font, 32%);
@color-blue2-player-detail-lyric: fadeout(@color-blue2-theme_2-font, 50%);
@color-blue2-player-detail-lyric-active: darken(@color-blue2-theme, 2%);
@ -712,7 +712,7 @@
@color-black-player-pic-c2: lighten(@color-black-theme_2, 30%);
@color-black-player-progress: lighten(@color-black-theme_2, 6%);
@color-black-player-progress-bar1: lighten(@color-black-theme_2, 12%);
@color-black-player-progress-bar2: lighten(@color-black-theme, 12%);
@color-black-player-progress-bar2: fadeout(lighten(@color-black-theme, 12%), 20%);
@color-black-player-status-text: darken(@color-black-theme_2-font, 20%);
@color-black-player-detail-lyric: fadeout(darken(@color-black-theme_2-font, 30%), 10%);
@color-black-player-detail-lyric-active: lighten(@color-black-theme, 50%);
@ -772,7 +772,7 @@
@color-mid_autumn-player-pic-c2: darken(@color-mid_autumn-theme_2, 30%);
@color-mid_autumn-player-progress: darken(@color-mid_autumn-theme_2, 10%);
@color-mid_autumn-player-progress-bar1: darken(@color-mid_autumn-theme_2, 12%);
@color-mid_autumn-player-progress-bar2: lighten(@color-mid_autumn-theme, 12%);
@color-mid_autumn-player-progress-bar2: fadeout(lighten(@color-mid_autumn-theme, 12%), 20%);
@color-mid_autumn-player-status-text: lighten(@color-mid_autumn-theme_2-font, 32%);
@color-mid_autumn-player-detail-lyric: fadeout(@color-mid_autumn-theme_2-font, 40%);
@color-mid_autumn-player-detail-lyric-active: lighten(@color-mid_autumn-theme, 7%);
@ -832,7 +832,7 @@
@color-mid_autumn-player-pic-c2: darken(@color-mid_autumn-theme_2, 30%);
@color-mid_autumn-player-progress: darken(@color-mid_autumn-theme_2, 10%);
@color-mid_autumn-player-progress-bar1: darken(@color-mid_autumn-theme_2, 12%);
@color-mid_autumn-player-progress-bar2: lighten(@color-mid_autumn-theme, 12%);
@color-mid_autumn-player-progress-bar2: fadeout(lighten(@color-mid_autumn-theme, 12%), 20%);
@color-mid_autumn-player-status-text: lighten(@color-mid_autumn-theme_2-font, 32%);
@color-mid_autumn-player-detail-lyric: lighten(@color-mid_autumn-theme, 7%);
@color-mid_autumn-player-detail-lyric-active: lighten(@color-mid_autumn-theme, 7%);
@ -891,7 +891,7 @@
@color-naruto-player-pic-c2: darken(@color-naruto-theme_2, 30%);
@color-naruto-player-progress: darken(@color-naruto-theme_2, 10%);
@color-naruto-player-progress-bar1: darken(@color-naruto-theme_2, 12%);
@color-naruto-player-progress-bar2: lighten(@color-naruto-theme, 12%);
@color-naruto-player-progress-bar2: fadeout(lighten(@color-naruto-theme, 12%), 20%);
@color-naruto-player-status-text: lighten(@color-naruto-theme_2-font, 32%);
@color-naruto-player-detail-lyric: fadeout(@color-naruto-theme_2-font, 36%);
@color-naruto-player-detail-lyric-active: darken(@color-naruto-theme, 7%);
@ -950,7 +950,7 @@
@color-happy_new_year-player-pic-c2: darken(@color-happy_new_year-theme_2, 30%);
@color-happy_new_year-player-progress: darken(@color-happy_new_year-theme_2, 10%);
@color-happy_new_year-player-progress-bar1: darken(@color-happy_new_year-theme_2, 5%);
@color-happy_new_year-player-progress-bar2: lighten(@color-happy_new_year-theme, 5%);
@color-happy_new_year-player-progress-bar2: fadeout(lighten(@color-happy_new_year-theme, 5%), 20%);
@color-happy_new_year-player-status-text: lighten(@color-happy_new_year-theme_2-font, 32%);
@color-happy_new_year-player-detail-lyric: fadeout(@color-happy_new_year-theme_2-font, 36%);
@color-happy_new_year-player-detail-lyric-active: darken(@color-happy_new_year-theme, 7%);

View File

@ -41,6 +41,7 @@ export default {
default: false,
},
},
emits: ['update:modelValue', 'change'],
data() {
return {
bool: false,

View File

@ -24,7 +24,7 @@ export default {
default: false,
},
modelValue: {
type: String,
type: [String, Number],
default: '',
},
type: {

View File

@ -125,14 +125,13 @@ export default {
.progress-bar2 {
background-color: @color-player-progress-bar2;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
opacity: 0.8;
will-change: transform;
}
.progress-bar3 {
background-color: @color-player-progress-bar2;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
opacity: 0.3;
opacity: 0.5;
}
.bar-transition {

View File

@ -10,12 +10,13 @@ div(:class="$style.player")
div(:class="$style.title" @click="handleCopy(title)" :tips="title + $t('copy_tip')") {{title}}
control-btns
div(:class="$style.column2")
common-progress-bar(:progress="progress" :handleTransitionEnd="handleTransitionEnd" :isActiveTransition="isActiveTransition")
common-progress-bar(:progress="progress" :handleTransitionEnd="handleTransitionEnd" :isActiveTransition="isActiveTransition" v-if="!isShowPlayerDetail")
div(:class="$style.column3")
span(:class="$style.statusText") {{statusText}}
span {{nowPlayTimeStr}}
span(style="margin: 0 5px;") /
span {{maxPlayTimeStr}}
template(v-if="!isShowPlayerDetail")
span(:class="$style.statusText") {{statusText}}
span {{nowPlayTimeStr}}
span(style="margin: 0 5px;") /
span {{maxPlayTimeStr}}
div(:class="$style.right")
div(:class="$style.playBtn" @click='playPrev' :tips="$t('player__prev')" style="transform: rotate(180deg);")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 220.847 220.847' space='preserve')
@ -48,7 +49,17 @@ import { player as eventPlayerNames } from '@renderer/event/names'
import ControlBtns from './ControlBtns'
import usePlayProgress from '@renderer/utils/compositions/usePlayProgress'
// import { lyric } from '@renderer/core/share/lyric'
import { statusText, musicInfo, setMusicInfo, setShowPlayerDetail, isPlay, musicInfoItem, playInfo, playMusicInfo } from '@renderer/core/share/player'
import {
statusText,
musicInfo,
setMusicInfo,
isShowPlayerDetail,
setShowPlayerDetail,
isPlay,
musicInfoItem,
playInfo,
playMusicInfo,
} from '@renderer/core/share/player'
export default {
name: 'CorePlayBar',
@ -138,6 +149,7 @@ export default {
playNext,
playPrev,
handleToMusicLocation,
isShowPlayerDetail,
}
},
}

View File

@ -2,11 +2,11 @@
<div :class="['right', $style.right]">
<div :class="['lyric', $style.lyric, { [$style.draging]: isMsDown }]" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric">
<div :class="$style.lyricSpace"></div>
<div :class="[$style.lyricText]" ref="dom_lyric_text"></div>
<div ref="dom_lyric_text"></div>
<div :class="$style.lyricSpace"></div>
</div>
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<div :class="[$style.lyricSelectContent, 'select', 'scroll']" v-if="isShowLrcSelectContent" @contextmenu="handleCopySelectText">
<div :class="[$style.lyricSelectContent, 'select', 'scroll', 'lyricSelectContent']" v-if="isShowLrcSelectContent" @contextmenu="handleCopySelectText">
<div v-for="(info, index) in lyric.lines" :key="index" :class="[$style.lyricSelectline, { [$style.lrcActive]: lyric.line == index }]">
<span>{{info.text}}</span>
<br v-if="info.translation"/>

View File

@ -6,11 +6,10 @@ div(:class="$style.footer")
div(:class="$style.progressContent")
common-progress-bar(:class-name="$style.progress" :progress="progress" :handleTransitionEnd="handleTransitionEnd" :isActiveTransition="isActiveTransition")
div(:class="$style.timeLabel")
span(style="margin-left: 15px") {{status}}
div
span {{nowPlayTimeStr}}
span(style="margin: 0 5px;") /
span {{maxPlayTimeStr}}
span(:class="$style.status" style="margin-left: 15px") {{status}}
span {{nowPlayTimeStr}}
span(style="margin: 0 5px;") /
span {{maxPlayTimeStr}}
div(:class="$style.playControl")
div(:class="$style.playBtn" @click="playPrev" style="transform: rotate(180deg);" :tips="$t('player__prev')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 220.847 220.847' space='preserve')
@ -91,7 +90,7 @@ export default {
.progress-content {
position: relative;
height: 15px;
height: 16px;
padding: 5px 0;
width: 100%;
}
@ -108,11 +107,13 @@ export default {
width: 100%;
height: 18px;
display: flex;
justify-content: space-between;
span {
font-size: 13px;
}
}
.status {
flex: auto;
}
.play-control {
flex: none;

View File

@ -1,5 +1,5 @@
<template lang="pug">
div.comment(:class="$style.comment")
div.comment(:class="$style.comment" ref="dom_container")
div(:class="$style.commentHeader")
h3 {{$t('comment__title', { name: title })}}
div(:class="$style.commentHeaderBtns")
@ -10,21 +10,27 @@ div.comment(:class="$style.comment")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-close')
div.scroll(:class="$style.commentMain" ref="dom_comment")
div(v-if="page == 1")
h2(:class="$style.commentType") {{$t('comment__hot_title')}}
p(:class="$style.commentLabel" style="cursor: pointer;" v-if="isHotLoadError" @click="handleGetHotComment(currentMusicInfo)") {{$t('comment__hot_load_error')}}
p(:class="$style.commentLabel" v-else-if="isHotLoading && !hotComments.length") {{$t('comment__hot_loading')}}
comment-floor(v-if="!isHotLoadError && hotComments.length" :class="[$style.commentFloor, isHotLoading ? $style.loading : null]" :comments="hotComments")
p(:class="$style.commentLabel" v-else-if="!isHotLoadError && !isHotLoading") {{$t('comment__no_content')}}
div
h2(:class="$style.commentType") {{$t('comment__new_title')}} ({{total}})
p(:class="$style.commentLabel" style="cursor: pointer;" v-if="isNewLoadError" @click="handleGetNewComment(currentMusicInfo, nextPage, limit)") {{$t('comment__new_load_error')}}
p(:class="$style.commentLabel" v-else-if="isNewLoading && !newComments.length") {{$t('comment__new_loading')}}
comment-floor(v-if="!isNewLoadError && newComments.length" :class="[$style.commentFloor, isNewLoading ? $style.loading : null]" :comments="newComments")
p(:class="$style.commentLabel" v-else-if="!isNewLoadError && !isNewLoading") {{$t('comment__no_content')}}
div(:class="$style.pagination")
material-pagination(:count="total" :btnLength="5" :limit="limit" :page="page" @btn-click="handleToggleCommentPage")
div(:class="$style.commentMain")
header(:class="$style.tab_header")
button(type="button" @click="handleToggleTab('hot')" :class="[$style.commentType, { [$style.active]: tabActiveId == 'hot' }]") {{$t('comment__hot_title')}} ({{hotComment.total}})
button(type="button" @click="handleToggleTab('new')" :class="[$style.commentType, { [$style.active]: tabActiveId == 'new' }]") {{$t('comment__new_title')}} ({{newComment.total}})
main(:class="$style.tab_main" ref="dom_tabMain")
div(:class="$style.tab_content")
div.scroll(:class="$style.tab_content_scroll" ref="dom_commentHot")
p(:class="$style.commentLabel" style="cursor: pointer;" v-if="hotComment.isLoadError" @click="handleGetHotComment(currentMusicInfo, hotComment.nextPage, hotComment.limit)") {{$t('comment__hot_load_error')}}
p(:class="$style.commentLabel" v-else-if="hotComment.isLoading && !hotComment.list.length") {{$t('comment__hot_loading')}}
comment-floor(v-if="!hotComment.isLoadError && hotComment.list.length" :class="[$style.commentFloor, hotComment.isLoading ? $style.loading : null]" :comments="hotComment.list")
p(:class="$style.commentLabel" v-else-if="!hotComment.isLoadError && !hotComment.isLoading") {{$t('comment__no_content')}}
div(:class="$style.pagination")
material-pagination(:count="hotComment.total" :btnLength="5" :limit="hotComment.limit" :page="hotComment.page" @btn-click="handleToggleHotCommentPage")
div(:class="$style.tab_content")
div.scroll(:class="$style.tab_content_scroll" ref="dom_commentNew")
p(:class="$style.commentLabel" style="cursor: pointer;" v-if="newComment.isLoadError" @click="handleGetNewComment(currentMusicInfo, newComment.nextPage, newComment.limit)") {{$t('comment__new_load_error')}}
p(:class="$style.commentLabel" v-else-if="newComment.isLoading && !newComment.list.length") {{$t('comment__new_loading')}}
comment-floor(v-if="!newComment.isLoadError && newComment.list.length" :class="[$style.commentFloor, newComment.isLoading ? $style.loading : null]" :comments="newComment.list")
p(:class="$style.commentLabel" v-else-if="!newComment.isLoadError && !newComment.isLoading") {{$t('comment__no_content')}}
div(:class="$style.pagination")
material-pagination(:count="newComment.total" :btnLength="5" :limit="newComment.limit" :page="newComment.page" @btn-click="handleToggleCommentPage")
</template>
<script>
@ -52,16 +58,16 @@ export default {
name: '',
singer: '',
},
page: 1,
total: 0,
maxPage: 1,
limit: 20,
isHotLoading: true,
isNewLoading: false,
isHotLoadError: true,
isNewLoadError: false,
nextPage: 1,
newComments: [
tabActiveId: 'hot',
newComment: {
isLoading: false,
isLoadError: false,
page: 1,
total: 0,
maxPage: 1,
nextPage: 1,
limit: 20,
list: [
// {
// text: ['123123hhh'],
// userName: 'dsads',
@ -71,8 +77,17 @@ export default {
// likedCount: 100,
// reply: [],
// },
],
hotComments: [
],
},
hotComment: {
isLoading: true,
isLoadError: true,
page: 1,
total: 0,
maxPage: 1,
nextPage: 1,
limit: 20,
list: [
// {
// text: ['123123hhh'],
// userName: 'dsads',
@ -91,7 +106,8 @@ export default {
// },
// ],
// },
],
],
},
}
},
computed: {
@ -102,12 +118,22 @@ export default {
: '^-^'
},
},
mounted() {
this.setWidth()
window.addEventListener('resize', this.setWidth)
},
beforeUnmount() {
window.removeEventListener('resize', this.setWidth)
},
watch: {
show(n) {
if (n) this.handleShowComment()
},
},
methods: {
setWidth() {
this.$refs.dom_container.style.width = this.$refs.dom_container.clientWidth + 'px'
},
async getComment(musicInfo, page, limit, retryNum = 0) {
let resp
try {
@ -118,65 +144,92 @@ export default {
}
return resp
},
async getHotComment(musicInfo, retryNum = 0) {
async getHotComment(musicInfo, page, limit, retryNum = 0) {
let resp
try {
resp = await music[musicInfo.source].comment.getHotComment(musicInfo)
resp = await music[musicInfo.source].comment.getHotComment(musicInfo, page, limit)
} catch (error) {
if (error.message == '取消请求' || ++retryNum > 2) throw error
resp = await this.getHotComment(musicInfo, retryNum)
resp = await this.getHotComment(musicInfo, page, limit, retryNum)
}
return resp
},
handleGetNewComment(musicInfo, page, limit) {
this.isNewLoadError = false
this.isNewLoading = true
this.newComment.isLoadError = false
this.newComment.isLoading = true
this.getComment(musicInfo, page, limit).then(comment => {
this.isNewLoading = false
this.total = comment.total
this.maxPage = comment.maxPage
this.page = page
this.newComments = comment.comments
this.newComment.isLoading = false
this.newComment.total = comment.total
this.newComment.maxPage = comment.maxPage
this.newComment.page = page
this.newComment.list = comment.comments
this.$nextTick(() => {
scrollTo(this.$refs.dom_comment, 0, 300)
scrollTo(this.$refs.dom_commentNew, 0, 300)
})
}).catch(err => {
console.log(err)
if (err.message == '取消请求') return
this.isNewLoadError = true
this.isNewLoading = false
this.newComment.isLoadError = true
this.newComment.isLoading = false
})
},
handleGetHotComment(musicInfo) {
this.isHotLoadError = false
this.isHotLoading = true
this.getHotComment(musicInfo).then(hotComment => {
this.isHotLoading = false
this.hotComments = hotComment.comments
handleGetHotComment(musicInfo, page, limit) {
this.hotComment.isLoadError = false
this.hotComment.isLoading = true
this.getHotComment(musicInfo, page, limit).then(hotComment => {
this.hotComment.isLoading = false
this.hotComment.total = hotComment.total
this.hotComment.maxPage = hotComment.maxPage
this.hotComment.page = page
this.hotComment.list = hotComment.comments
this.$nextTick(() => {
scrollTo(this.$refs.dom_commentHot, 0, 300)
})
}).catch(err => {
console.log(err)
if (err.message == '取消请求') return
this.isHotLoadError = true
this.isHotLoading = false
this.hotComment.isLoadError = true
this.hotComment.isLoading = false
})
},
handleShowComment() {
if (!this.musicInfo.songmid || !music[this.musicInfo.source].comment) return
// if (this.musicInfo.songmid != this.currentMusicInfo.songmid) {
this.page = 1
this.total = 0
this.maxPage = 1
this.nextPage = 1
this.hotComment.page = 1
this.hotComment.total = 0
this.hotComment.maxPage = 1
this.hotComment.nextPage = 1
this.newComment.page = 1
this.newComment.total = 0
this.newComment.maxPage = 1
this.newComment.nextPage = 1
// }
this.isShowComment = true
this.currentMusicInfo = this.musicInfo
if (this.page == 1) this.handleGetHotComment(this.currentMusicInfo)
this.handleGetNewComment(this.currentMusicInfo, this.page, this.limit)
this.handleGetHotComment(this.currentMusicInfo, this.hotComment.page, this.hotComment.limit)
this.handleGetNewComment(this.currentMusicInfo, this.newComment.page, this.newComment.limit)
},
handleToggleHotCommentPage(page) {
this.hotComment.nextPage = page
this.handleGetHotComment(this.currentMusicInfo, page, this.hotComment.limit)
},
handleToggleCommentPage(page) {
this.nextPage = page
this.handleGetNewComment(this.currentMusicInfo, page, this.limit)
this.newComment.nextPage = page
this.handleGetNewComment(this.currentMusicInfo, page, this.newComment.limit)
},
handleToggleTab(id) {
if (this.tabActiveId == id) return
switch (id) {
case 'hot':
this.$refs.dom_tabMain.scrollLeft = 0
break
case 'new':
this.$refs.dom_tabMain.scrollLeft = this.$refs.dom_tabMain.clientWidth
break
}
this.tabActiveId = id
},
},
}
@ -229,10 +282,39 @@ export default {
}
.commentMain {
flex: auto;
padding-left: 15px;
padding-right: 10px;
background-color: @color-reply-floor;
border-radius: 4px;
display: flex;
flex-direction: column;
}
.tab_header {
display: flex;
flex-flow: row nowrap;
gap: 15px;
padding-left: 15px;
padding-right: 10px;
}
.tab_main {
flex: auto;
display: flex;
flex-flow: row nowrap;
overflow: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
}
.tab_content {
flex-shrink: 0;
width: 100%;
position: relative;
}
.tab_content_scroll {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
padding-left: 15px;
padding-right: 10px;
}
.commentLabel {
padding: 15px;
@ -240,9 +322,20 @@ export default {
font-size: 14px;
}
.commentType {
padding: 10px 0;
padding: 5px;
margin: 5px 0;
font-size: 13px;
color: @color-theme;
background: none;
border: none;
cursor: pointer;
transition: @transition-theme;
transition-property: opacity, color;
&:hover {
opacity: .7;
}
&.active {
color: @color-theme;
}
}
.commentFloor {
opacity: 1;
@ -268,7 +361,9 @@ each(@themes, {
color: ~'@{color-@{value}-theme_2-font-label}';
}
.commentType {
color: ~'@{color-@{value}-theme}';
&.active {
color: ~'@{color-@{value}-theme}';
}
}
}
})

View File

@ -162,25 +162,25 @@ export default {
}
}
.bg {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-size: 110% 110%;
filter: blur(60px);
z-index: -1;
}
.bg2 {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: -1;
background-color: rgba(255, 255, 255, .8);
}
// .bg {
// position: absolute;
// width: 100%;
// height: 100%;
// top: 0;
// left: 0;
// background-size: 110% 110%;
// filter: blur(60px);
// z-index: -1;
// }
// .bg2 {
// position: absolute;
// width: 100%;
// height: 100%;
// top: 0;
// left: 0;
// z-index: -1;
// background-color: rgba(255, 255, 255, .8);
// }
.header {
position: relative;
@ -299,6 +299,9 @@ export default {
.lyric {
font-size: 13px;
}
.lyricSelectContent {
font-size: 14px;
}
}
.comment {
opacity: 1;
@ -346,131 +349,6 @@ export default {
}
}
.right {
flex: 0 0 60%;
// padding: 0 30px;
position: relative;
transition: flex-basis @transition-theme;
&:before {
position: absolute;
z-index: 1;
top: 0;
left: 0;
content: ' ';
height: 100px;
width: 100%;
background-image: linear-gradient(0deg,rgba(255,255,255,0) 0%,@color-theme_2-background_1 95%);
pointer-events: none;
}
&:after {
position: absolute;
bottom: 0;
left: 0;
content: ' ';
height: 100px;
width: 100%;
background-image: linear-gradient(-180deg,rgba(255,255,255,0) 0%,@color-theme_2-background_1 95%);
pointer-events: none;
}
}
.lyric {
text-align: center;
height: 100%;
overflow: hidden;
font-size: 16px;
cursor: grab;
&.draging {
cursor: grabbing;
}
:global {
.lrc-content {
line-height: 1.2;
margin: 16px 0;
overflow-wrap: break-word;
color: @color-player-detail-lyric;
.translation {
transition: @transition-theme !important;
transition-property: font-size, color;
font-size: .9em;
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: 1em;
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;
// }
// .lrc-active {
// color: @color-theme;
// font-size: 1.2em;
// }
}
.lyricSelectContent {
position: absolute;
left: 0;
top: 0;
// text-align: center;
height: 100%;
width: 100%;
font-size: 16px;
background-color: @color-theme_2-background_1;
z-index: 10;
color: @color-player-detail-lyric;
.lyricSelectline {
padding: 8px 0;
overflow-wrap: break-word;
transition: @transition-theme !important;
transition-property: color, font-size;
line-height: 1.3;
}
.lyricSelectlineTransition {
font-size: 14px;
}
.lrc-active {
color: @color-theme;
}
}
.lyric-space {
height: 70%;
}
.comment {
position: absolute;
@ -491,14 +369,6 @@ each(@themes, {
background-color: ~'@{color-@{value}-theme_2-background_1}';
// color: ~'@{color-@{value}-theme_2-font}';
}
.right {
&:before {
background-image: linear-gradient(0deg,rgba(255,255,255,0) 0%,~'@{color-@{value}-theme_2-background_1}' 95%);
}
&:after {
background-image: linear-gradient(-180deg,rgba(255,255,255,0) 0%,~'@{color-@{value}-theme_2-background_1}' 95%);
}
}
.header {
&.controlBtnLeft {
.controBtn {
@ -542,56 +412,6 @@ each(@themes, {
box-shadow: 0 0 4px ~'@{color-@{value}-theme-hover}';
// border-color: ~'@{color-@{value}-theme-hover}';
}
.lyric {
:global {
.lrc-content {
color: ~'@{color-@{value}-player-detail-lyric}';
&.active {
.translation {
color: ~'@{color-@{value}-player-detail-lyric-active}';
}
.line {
color: ~'@{color-@{value}-player-detail-lyric-active}';
}
}
span {
background-color: ~'@{color-@{value}-player-detail-lyric}';
background-image: -webkit-linear-gradient(top, ~'@{color-@{value}-player-detail-lyric-active}', ~'@{color-@{value}-player-detail-lyric-active}');
}
}
}
}
// .lrc-active {
// color: ~'@{color-@{value}-theme}';
// }
.lyricSelectContent {
background-color: ~'@{color-@{value}-theme_2-background_1}';
color: ~'@{color-@{value}-player-detail-lyric}';
.lrc-active {
color: ~'@{color-@{value}-theme}';
}
}
.footerLeftControlBtns {
color: ~'@{color-@{value}-theme_2-font}';
}
.footerLeftControlBtn {
&.active {
color: ~'@{color-@{value}-theme}';
}
}
.progress {
background-color: ~'@{color-@{value}-player-progress}';
}
.progress-bar1 {
background-color: ~'@{color-@{value}-player-progress-bar1}';
}
.progress-bar2 {
background-color: ~'@{color-@{value}-player-progress-bar2}';
}
.play-btn {
color: ~'@{color-@{value}-player-detail-play-btn}';
}
}
})

View File

@ -50,7 +50,7 @@ div(:class="$style.songList")
<script>
import { clipboardWriteText, assertApiSupport } from '@renderer/utils'
import { ref, useCssModule } from '@renderer/utils/vueTools'
import { ref, useCssModule, useRefGetter } from '@renderer/utils/vueTools'
import useList from './useList'
import useMenu from './useMenu'
import usePlay from './usePlay'
@ -95,7 +95,7 @@ export default {
type: String,
},
},
emits: ['show-menu', 'togglePage'],
emits: ['show-menu', 'play-list', 'togglePage'],
setup(props, { emit }) {
const rightClickSelectedIndex = ref(-1)
const dom_listContent = ref(null)
@ -103,18 +103,20 @@ export default {
const styles = useCssModule()
const setting = useRefGetter('setting')
const {
selectedList,
listItemHeight,
handleSelectData,
removeAllSelect,
} = useList({ props, emit })
} = useList({ props })
const {
handlePlayMusic,
handlePlayMusicLater,
doubleClickPlay,
} = usePlay({ selectedList, props, removeAllSelect })
} = usePlay({ selectedList, props, removeAllSelect, setting, emit })
const {
isShowListAdd,

View File

@ -2,7 +2,7 @@ import { useCommit } from '@renderer/utils/vueTools'
import { defaultList } from '@renderer/core/share/list'
import { getList } from '@renderer/core/share/utils'
export default ({ selectedList, props, removeAllSelect }) => {
export default ({ selectedList, props, removeAllSelect, setting, emit }) => {
let clickTime = 0
let clickIndex = -1
@ -47,7 +47,11 @@ export default ({ selectedList, props, removeAllSelect }) => {
clickIndex = index
return
}
handlePlayMusic(index, true)
if (setting.value.list.isClickPlayList) {
emit('play-list', index)
} else {
handlePlayMusic(index, true)
}
clickTime = 0
clickIndex = -1
}

View File

@ -104,7 +104,7 @@ export const getPlayIndex = (listId, musicInfo, isTempPlay) => {
}
const list = getList(listId)
if (list?.length) {
if (list?.length && musicInfo) {
if (musicInfo.key) { // 已下载的歌曲
const currentKey = musicInfo.key
playIndex = list.findIndex(m => m.key == currentKey)

View File

@ -40,7 +40,7 @@ export default () => {
isProd,
isLinux,
})
usePlayer({ setting })
const initPlayer = usePlayer({ setting })
const handleEnvParams = useHandleEnvParams()
const initData = useDataInit({
setting,
@ -60,6 +60,7 @@ export default () => {
// 初始化我的列表、下载列表等数据
initData().then(() => {
initPlayer()
handleEnvParams(envParams) // 处理传入的启动参数
initDeepLink(envParams)
})

View File

@ -1,268 +0,0 @@
import { useCommit, useAction, onBeforeUnmount, useRouter, useI18n, markRaw } from '@renderer/utils/vueTools'
import { base as eventBaseName } from '@renderer/event/names'
import { getEnvParams, clearEnvParamsDeeplink } from '@renderer/utils/tools'
import { decodeName } from '@renderer/utils'
// import { allList, defaultList, loveList, userLists } from '@renderer/core/share/list'
import { isShowPlayerDetail, setShowPlayerDetail, playMusicInfo } from '@renderer/core/share/player'
import usePlaySonglist from './compositions/usePlaySonglist'
import { dialog } from '@renderer/plugins/Dialog'
const sources = ['kw', 'kg', 'tx', 'wy', 'mg']
const sourceVerify = source => {
if (!sources.includes(source)) throw new Error('Source no match')
}
const qualitys = ['128k', '320k', 'flac', 'flac32bit']
const qualityFilter = (source, types) => {
types = types.filter(({ type }) => qualitys.includes(type)).map(({ type, size, hash }) => {
if (size != null && typeof size != 'string') throw new Error(type + ' size type no match')
if (source == 'kg' && typeof hash != 'string') throw new Error(type + ' hash type no match')
return hash == null ? { type, size } : { type, size, hash }
})
if (!types.length) throw new Error('quality no match')
return types
}
const dataVerify = (rules, data) => {
const newData = {}
for (const rule of rules) {
const val = data[rule.key]
if (rule.required && val == null) throw new Error(rule.key + ' missing')
if (val != null) {
if (rule.types && !rule.types.includes(typeof val)) throw new Error(rule.key + ' type no match')
if (rule.max && String(val).length > rule.max) throw new Error(rule.key + ' max length no match')
if (rule.min && String(val).length > rule.min) throw new Error(rule.key + ' min length no match')
}
newData[rule.key] = val
}
return newData
}
export default () => {
// const setList = useCommit('list', 'setList')
// const listAdd = useCommit('list', 'listAdd')
// const listMove = useCommit('list', 'listMove')
// const listAddMultiple = useCommit('list', 'listAddMultiple')
// const listMoveMultiple = useCommit('list', 'listMoveMultiple')
// const listRemove = useCommit('list', 'listRemove')
// const listRemoveMultiple = useCommit('list', 'listRemoveMultiple')
// const listClear = useCommit('list', 'listClear')
// const updateMusicInfo = useCommit('list', 'updateMusicInfo')
// const createUserList = useCommit('list', 'createUserList')
// const removeUserList = useCommit('list', 'removeUserList')
// const setUserListName = useCommit('list', 'setUserListName')
// const setMusicPosition = useCommit('list', 'setMusicPosition')
// // const setSyncListData = useCommit('list', 'setSyncListData')
// const setUserListPosition = useCommit('list', 'setUserListPosition')
const router = useRouter()
const setTempPlayList = useCommit('player', 'setTempPlayList')
const playNext = useAction('player', 'playNext')
const playSongListDetail = usePlaySonglist()
const { t } = useI18n()
let isInited = false
const handleOpenSonglist = params => {
if (params.id) {
router.replace({
path: '/songList',
query: {
source: params.source,
id: params.id,
},
})
} else if (params.url) {
router.replace({
path: '/songList',
query: {
source: params.source,
url: params.url,
},
})
}
}
const handlePlayMusic = _musicInfo => {
const musicInfo = {
..._musicInfo,
singer: decodeName(_musicInfo.singer),
name: decodeName(_musicInfo.name),
albumName: decodeName(_musicInfo.albumName),
otherSource: null,
_types: {},
typeUrl: {},
}
for (const type of musicInfo.types) {
musicInfo._types[type.type] = { size: type.size }
}
markRaw(musicInfo)
const isPlaying = !!playMusicInfo.musicInfo
setTempPlayList([{ listId: '__temp__', musicInfo, isTop: true }])
if (isPlaying) playNext()
}
const handleSonglist = (action, songlistInfo) => {
sourceVerify(songlistInfo.source)
switch (action) {
case 'open':
songlistInfo = dataVerify([
{ key: 'source', types: ['string'] },
{ key: 'id', types: ['string', 'number'], max: 64 },
{ key: 'url', types: ['string'], max: 500 },
], songlistInfo)
if (isShowPlayerDetail.value) setShowPlayerDetail(false)
handleOpenSonglist(songlistInfo)
break
case 'play':
songlistInfo = dataVerify([
{ key: 'source', types: ['string'] },
{ key: 'id', types: ['string', 'number'], max: 64 },
{ key: 'url', types: ['string'], max: 500 },
{ key: 'index', types: ['number'], max: 1000000 },
], songlistInfo)
playSongListDetail(songlistInfo.source, songlistInfo.id ?? songlistInfo.url, songlistInfo.index ?? 0)
break
default: throw new Error('Unknown action: ' + action)
}
}
const handleMusic = (action, musicInfo) => {
switch (musicInfo.source) {
case 'kw':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
], musicInfo)
break
case 'kg':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: '_interval', types: ['number'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
{ key: 'hash', types: ['string'], required: true, max: 64 },
], musicInfo)
break
case 'tx':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
{ key: 'strMediaMid', types: ['string'], required: true, max: 64 },
{ key: 'albumMid', types: ['string'], max: 64 },
], musicInfo)
break
case 'wy':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
], musicInfo)
break
case 'mg':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
{ key: 'copyrightId', types: ['string', 'number'], required: true, max: 64 },
{ key: 'lrcUrl', types: ['string'], max: 1024 },
], musicInfo)
break
default: throw new Error('Unknown action: ' + action)
}
musicInfo.types = qualityFilter(musicInfo.source, musicInfo.types)
switch (action) {
case 'play':
handlePlayMusic(musicInfo)
break
default: throw new Error('Unknown action: ' + action)
}
}
const handleLinkAction = link => {
// console.log(link)
const [url, search] = link.split('?')
const [type, action] = url.replace('lxmusic://', '').split('/')
const params = {}
for (const param of search.split('&')) {
const [key, value] = param.split('=')
params[key] = value
}
if (params.data) params.data = JSON.parse(decodeURIComponent(params.data))
console.log(params.data)
switch (type) {
case 'music':
handleMusic(action, params.data)
break
case 'songlist':
handleSonglist(action, params.data)
break
default: throw new Error('Unknown type: ' + type)
}
}
const handleFocus = () => {
if (!isInited) return
getEnvParams().then(envParams => {
if (!envParams.deeplink) return
clearEnvParamsDeeplink()
try {
handleLinkAction(envParams.deeplink)
} catch (err) {
dialog(`${t('deep_link__handle_error_tip', { message: err.message })}`)
}
})
}
window.eventHub.on(eventBaseName.focus, handleFocus)
onBeforeUnmount(() => {
window.eventHub.off(eventBaseName.focus, handleFocus)
})
return envParams => {
if (envParams.deeplink) {
clearEnvParamsDeeplink()
try {
handleLinkAction(envParams.deeplink)
} catch (err) {
dialog(`${t('deep_link__handle_error_tip', { message: err.message })}`)
}
}
isInited = true
}
}

View File

@ -0,0 +1,74 @@
import { onBeforeUnmount } from '@renderer/utils/vueTools'
import { base as eventBaseName } from '@renderer/event/names'
import { getEnvParams, clearEnvParamsDeeplink } from '@renderer/utils/tools'
import { useDialog } from './utils'
import useMusicAction from './useMusicAction'
import useSonglistAction from './useSonglistAction'
export default () => {
let isInited = false
const showErrorDialog = useDialog()
const handleMusicAction = useMusicAction()
const handleSonglistAction = useSonglistAction()
const handleLinkAction = link => {
// console.log(link)
const [url, search] = link.split('?')
const [type, action, ...paths] = url.replace('lxmusic://', '').split('/')
const params = {}
if (search) {
for (const param of search.split('&')) {
const [key, value] = param.split('=')
params[key] = value
}
if (params.data) params.data = JSON.parse(decodeURIComponent(params.data))
}
params.paths = paths.map(p => decodeURIComponent(p))
console.log(params)
switch (type) {
case 'music':
handleMusicAction(action, params)
break
case 'songlist':
handleSonglistAction(action, params)
break
default: throw new Error('Unknown type: ' + type)
}
}
const handleFocus = () => {
if (!isInited) return
getEnvParams().then(envParams => {
if (!envParams.deeplink) return
clearEnvParamsDeeplink()
try {
handleLinkAction(envParams.deeplink)
} catch (err) {
showErrorDialog(err.message)
}
})
}
window.eventHub.on(eventBaseName.focus, handleFocus)
onBeforeUnmount(() => {
window.eventHub.off(eventBaseName.focus, handleFocus)
})
return envParams => {
if (envParams.deeplink) {
clearEnvParamsDeeplink()
try {
handleLinkAction(envParams.deeplink)
} catch (err) {
showErrorDialog(err.message)
}
}
isInited = true
}
}

View File

@ -0,0 +1,167 @@
import { useCommit, useAction, useRouter, markRaw } from '@renderer/utils/vueTools'
import { decodeName } from '@renderer/utils'
// import { allList, defaultList, loveList, userLists } from '@renderer/core/share/list'
import { playMusicInfo } from '@renderer/core/share/player'
import { dataVerify, qualityFilter, sources } from './utils'
const useSearchMusic = () => {
const router = useRouter()
return ({ paths, data: params }) => {
let text
let source
if (params) {
text = dataVerify([
{ key: 'keywords', types: ['string', 'number'], max: 128, required: true },
], params).keywords
source = params.source
} else {
if (!paths.length) throw new Error('Keyword missing')
if (paths.length > 1) {
text = paths[1]
source = paths[0]
} else {
text = paths[0]
}
if (text.length > 128) text = text.substring(0, 128)
}
const sourceList = [...sources, 'all']
source = sourceList.includes(source) ? source : null
router.replace({
path: '/search',
query: {
text,
source,
},
})
}
}
const usePlayMusic = () => {
const setTempPlayList = useCommit('player', 'setTempPlayList')
const playNext = useAction('player', 'playNext')
const filterInfoByPlayMusic = musicInfo => {
switch (musicInfo.source) {
case 'kw':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
], musicInfo)
break
case 'kg':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: '_interval', types: ['number'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
{ key: 'hash', types: ['string'], required: true, max: 64 },
], musicInfo)
break
case 'tx':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
{ key: 'strMediaMid', types: ['string'], required: true, max: 64 },
{ key: 'albumMid', types: ['string'], max: 64 },
], musicInfo)
break
case 'wy':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
], musicInfo)
break
case 'mg':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
{ key: 'copyrightId', types: ['string', 'number'], required: true, max: 64 },
{ key: 'lrcUrl', types: ['string'], max: 1024 },
], musicInfo)
break
default: throw new Error('Unknown source: ' + musicInfo.source)
}
musicInfo.types = qualityFilter(musicInfo.source, musicInfo.types)
return musicInfo
}
return ({ data: _musicInfo }) => {
_musicInfo = filterInfoByPlayMusic(_musicInfo)
const musicInfo = {
..._musicInfo,
singer: decodeName(_musicInfo.singer),
name: decodeName(_musicInfo.name),
albumName: decodeName(_musicInfo.albumName),
otherSource: null,
_types: {},
typeUrl: {},
}
for (const type of musicInfo.types) {
musicInfo._types[type.type] = { size: type.size }
}
markRaw(musicInfo)
const isPlaying = !!playMusicInfo.musicInfo
setTempPlayList([{ listId: '__temp__', musicInfo, isTop: true }])
if (isPlaying) playNext()
}
}
export default () => {
const handleSearchMusic = useSearchMusic()
const handlePlayMusic = usePlayMusic()
return (action, info) => {
switch (action) {
case 'search':
handleSearchMusic(info)
break
case 'play':
handlePlayMusic(info)
break
default: throw new Error('Unknown action: ' + action)
}
}
}

View File

@ -0,0 +1,106 @@
import { useRouter } from '@renderer/utils/vueTools'
import { isShowPlayerDetail, setShowPlayerDetail } from '@renderer/core/share/player'
import usePlaySonglist from '../compositions/usePlaySonglist'
import { dataVerify, sourceVerify } from './utils'
const useOpenSonglist = () => {
const router = useRouter()
const handleOpenSonglist = params => {
if (params.id) {
router.replace({
path: '/songList',
query: {
source: params.source,
id: params.id,
},
})
} else if (params.url) {
router.replace({
path: '/songList',
query: {
source: params.source,
url: params.url,
},
})
}
}
return ({ paths, data }) => {
let songlistInfo = {
source: null,
id: null,
url: null,
}
if (data) {
songlistInfo = data
} else {
songlistInfo.source = paths[0]
songlistInfo.url = paths[1]
}
sourceVerify(songlistInfo.source)
songlistInfo = dataVerify([
{ key: 'source', types: ['string'] },
{ key: 'id', types: ['string', 'number'], max: 64 },
{ key: 'url', types: ['string'], max: 500 },
], songlistInfo)
if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing')
if (isShowPlayerDetail.value) setShowPlayerDetail(false)
handleOpenSonglist(songlistInfo)
}
}
const usePlaySonglistDetail = () => {
const playSongListDetail = usePlaySonglist()
return ({ paths, data }) => {
let songlistInfo = {
source: null,
id: null,
url: null,
index: null,
}
if (data) {
songlistInfo = data
} else {
songlistInfo.source = paths[0]
songlistInfo.url = paths[1]
songlistInfo.index = paths[2]
if (songlistInfo.index != null) songlistInfo.index = parseInt(songlistInfo.index)
}
sourceVerify(songlistInfo.source)
songlistInfo = dataVerify([
{ key: 'source', types: ['string'] },
{ key: 'id', types: ['string', 'number'], max: 64 },
{ key: 'url', types: ['string'], max: 500 },
{ key: 'index', types: ['number'], max: 1000000 },
], songlistInfo)
if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing')
playSongListDetail(songlistInfo.source, songlistInfo.id ?? songlistInfo.url, songlistInfo.index ?? 0)
}
}
export default () => {
const handleOpenSonglist = useOpenSonglist()
const handlePlaySonglist = usePlaySonglistDetail()
return (action, info) => {
switch (action) {
case 'open':
handleOpenSonglist(info)
break
case 'play':
handlePlaySonglist(info)
break
default: throw new Error('Unknown action: ' + action)
}
}
}

View File

@ -0,0 +1,45 @@
import { useI18n } from '@renderer/utils/vueTools'
import { dialog } from '@renderer/plugins/Dialog'
export const useDialog = () => {
const { t } = useI18n()
const errorDialog = message => {
dialog({
message: `${t('deep_link__handle_error_tip', { message })}`,
confirmButtonText: t('ok'),
})
}
return errorDialog
}
export const sources = ['kw', 'kg', 'tx', 'wy', 'mg']
export const sourceVerify = source => {
if (!sources.includes(source)) throw new Error('Source no match')
}
export const qualitys = ['128k', '320k', 'flac', 'flac32bit']
export const qualityFilter = (source, types) => {
types = types.filter(({ type }) => qualitys.includes(type)).map(({ type, size, hash }) => {
if (size != null && typeof size != 'string') throw new Error(type + ' size type no match')
if (source == 'kg' && typeof hash != 'string') throw new Error(type + ' hash type no match')
return hash == null ? { type, size } : { type, size, hash }
})
if (!types.length) throw new Error('quality no match')
return types
}
export const dataVerify = (rules, data) => {
const newData = {}
for (const rule of rules) {
const val = data[rule.key]
if (rule.required && val == null) throw new Error(rule.key + ' missing')
if (val != null) {
if (rule.types && !rule.types.includes(typeof val)) throw new Error(rule.key + ' type no match')
if (rule.max && String(val).length > rule.max) throw new Error(rule.key + ' max length no match')
if (rule.min && String(val).length > rule.min) throw new Error(rule.key + ' min length no match')
}
newData[rule.key] = val
}
return newData
}

View File

@ -1,18 +1,22 @@
import { onBeforeUnmount } from '@renderer/utils/vueTools'
import { onUserApiStatus, setUserApi, getUserApiList, userApiRequest, userApiRequestCancel } from '@renderer/utils/tools'
import { onBeforeUnmount, useI18n } from '@renderer/utils/vueTools'
import { onUserApiStatus, setUserApi, getUserApiList, userApiRequest, userApiRequestCancel, onShowUserApiUpdateAlert } from '@renderer/utils/tools'
import { openUrl } from '@renderer/utils'
import apiSourceInfo from '@renderer/utils/music/api-source-info'
import music from '@renderer/utils/music'
import { apiSource, qualityList, userApi } from '@renderer/core/share'
import { dialog } from '@renderer/plugins/Dialog'
export default ({ setting }) => {
const { t } = useI18n()
if (/^user_api/.test(setting.value.apiSource)) {
setUserApi(setting.value.apiSource)
} else {
qualityList.value = music.supportQuality[setting.value.apiSource]
}
const rUserApiStatus = onUserApiStatus(({ status, message, apiInfo }) => {
const rUserApiStatus = onUserApiStatus((event, { status, message, apiInfo }) => {
userApi.status = status
userApi.message = message
@ -66,8 +70,32 @@ export default ({ setting }) => {
}
})
const rUserApiShowUpdateAlert = onShowUserApiUpdateAlert((event, { name, log, updateUrl }) => {
if (updateUrl) {
dialog({
message: `${t('user_api__update_alert', { name })}\n${log}`,
selection: true,
showCancel: true,
confirmButtonText: t('user_api__update_alert_open_url'),
cancelButtonText: t('close'),
}).then(confirm => {
if (!confirm) return
setTimeout(() => {
openUrl(updateUrl)
}, 300)
})
} else {
dialog({
message: `${t('user_api__update_alert', { name })}\n${log}`,
selection: true,
confirmButtonText: t('ok'),
})
}
})
onBeforeUnmount(() => {
rUserApiStatus()
rUserApiShowUpdateAlert()
})
return () => {

View File

@ -4,6 +4,8 @@ import {
import useMediaDevice from './useMediaDevice'
import usePlayerEvent from './usePlayerEvent'
import usePlayer from './usePlayer'
import useTaskbar from './useTaskbar'
import { init as initPlayTimeoutStop } from '@renderer/utils/timeoutStop'
export default ({ setting }) => {
createAudio()
@ -11,5 +13,12 @@ export default ({ setting }) => {
usePlayerEvent()
useMediaDevice({ setting }) // 初始化音频驱动输出设置
usePlayer({ setting })
const initTaskbar = useTaskbar()
initPlayTimeoutStop()
return () => {
initTaskbar()
}
}

View File

@ -49,6 +49,7 @@ export default ({
}
const handleLoadstart = () => {
if (global.isPlayedStop) return
startLoadingTimeout()
setAllStatus(t('player__loading'))
}
@ -77,6 +78,7 @@ export default ({
const handleError = errCode => {
if (!musicInfo.songmid) return
clearLoadingTimeout()
if (global.isPlayedStop) return
if (playMusicInfo.listId != 'download' && errCode !== 1 && retryNum < 2) { // 若音频URL无效则尝试刷新2次URL
// console.log(this.retryNum)
retryNum++
@ -96,6 +98,11 @@ export default ({
clearLoadingTimeout()
}
const handlePlayedStop = () => {
clearDelayNextTimeout()
clearLoadingTimeout()
}
window.eventHub.on(eventPlayerNames.player_loadstart, handleLoadstart)
window.eventHub.on(eventPlayerNames.player_loadeddata, handleLoadeddata)
@ -105,6 +112,7 @@ export default ({
window.eventHub.on(eventPlayerNames.player_emptied, handleEmpied)
window.eventHub.on(eventPlayerNames.error, handleError)
window.eventHub.on(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.on(eventPlayerNames.playedStop, handlePlayedStop)
onBeforeUnmount(() => {
window.eventHub.off(eventPlayerNames.player_loadstart, handleLoadstart)
@ -115,5 +123,6 @@ export default ({
window.eventHub.off(eventPlayerNames.player_emptied, handleEmpied)
window.eventHub.off(eventPlayerNames.error, handleError)
window.eventHub.off(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.off(eventPlayerNames.playedStop, handlePlayedStop)
})
}

View File

@ -27,7 +27,6 @@ import {
setMusicInfo,
musicInfoItem,
playMusicInfo,
playInfo,
setPlayList,
setPlayMusicInfo,
playedList,
@ -103,11 +102,13 @@ export default ({ setting }) => {
setAllStatus('Try toggle source...')
},
}).then(url => {
if (global.isPlayedStop) return
if (targetSong !== musicInfoItem.value || isPlay.value || type != getPlayType(setting.value.player.highQuality, musicInfoItem.value)) return
setMusicInfo({ url })
setResource(url)
}).catch(err => {
// console.log('err', err.message)
if (global.isPlayedStop) return
if (targetSong !== musicInfoItem.value || isPlay.value) return
if (err.message == requestMsg.cancelRequest) return
if (!isRetryed) return setUrl(targetSong, isRefresh, true)
@ -193,11 +194,10 @@ export default ({ setting }) => {
const setPlayStatus = () => {
setPlay(true)
setTitle(`${musicInfo.name} - ${musicInfo.singer}`)
}
const setPauseStatus = () => {
setPlay(false)
setTitle()
if (global.isPlayedStop) handlePause()
}
const setStopStatus = () => {
setPlay(false)
@ -226,7 +226,7 @@ export default ({ setting }) => {
return
}
if (playInfo.musicInfo) {
if (playMusicInfo.musicInfo) {
setPlayerStop()
window.eventHub.emit(eventPlayerNames.pause)
setStopStatus()
@ -279,20 +279,27 @@ export default ({ setting }) => {
name: musicInfo.name,
album: musicInfo.albumName,
})
setTitle(`${musicInfo.name} - ${musicInfo.singer}`)
}
const handelStop = () => {
setPlayerStop()
setPlayMusicInfo(playMusicInfo.listId, null)
window.eventHub.emit(eventPlayerNames.stop)
}
const handleEnded = () => {
setAllStatus(t('player__end'))
if (global.isPlayedStop) return
playNext()
}
// 播放、暂停播放切换
const handleTogglePlay = async() => {
const handlePause = () => {
setPlayerPause()
}
const handlePlay = async() => {
if (playMusicInfo.musicInfo == null) return
if (isPlayerEmpty()) {
if (playMusicInfo.listId == 'download') {
@ -313,13 +320,24 @@ export default ({ setting }) => {
}
return
}
setPlayerPlay()
}
// 播放、暂停播放切换
const handleTogglePlay = () => {
if (global.isPlayedStop) global.isPlayedStop = false
if (isPlay.value) {
setPlayerPause()
handlePause()
} else {
setPlayerPlay()
handlePlay()
}
}
const handlePlayedStop = () => {
clearDelayNextTimeout()
clearLoadTimeout()
}
watch(() => setting.value.player.togglePlayMethod, newValue => {
setLoopPlay(newValue === 'singleLoop')
if (playedList.length) clearPlayedList()
@ -339,13 +357,16 @@ export default ({ setting }) => {
window.eventHub.on(eventPlayerNames.stop, setStopStatus)
window.eventHub.on(eventPlayerNames.playMusic, playMusic)
window.eventHub.on(eventPlayerNames.setPlay, handlePlay)
window.eventHub.on(eventPlayerNames.setPause, handlePause)
window.eventHub.on(eventPlayerNames.setStop, handelStop)
window.eventHub.on(eventPlayerNames.setTogglePlay, handleTogglePlay)
window.eventHub.on(eventPlayerNames.setPlayPrev, playPrev)
window.eventHub.on(eventPlayerNames.setPlayNext, playNext)
window.eventHub.on(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.on(eventPlayerNames.setStop, handelStop)
window.eventHub.on(eventPlayerNames.player_ended, handleEnded)
window.eventHub.on(eventPlayerNames.playedStop, handlePlayedStop)
onBeforeUnmount(() => {
@ -360,11 +381,14 @@ export default ({ setting }) => {
window.eventHub.off(eventPlayerNames.playMusic, playMusic)
window.eventHub.off(eventPlayerNames.setTogglePlay, handleTogglePlay)
window.eventHub.off(eventPlayerNames.setPlay, handlePlay)
window.eventHub.off(eventPlayerNames.setPause, handlePause)
window.eventHub.off(eventPlayerNames.setStop, handelStop)
window.eventHub.off(eventPlayerNames.setPlayPrev, playPrev)
window.eventHub.off(eventPlayerNames.setPlayNext, playNext)
window.eventHub.off(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.off(eventPlayerNames.setStop, handelStop)
window.eventHub.off(eventPlayerNames.player_ended, handleEnded)
window.eventHub.off(eventPlayerNames.playedStop, handlePlayedStop)
})
}

View File

@ -0,0 +1,128 @@
import { onBeforeUnmount, useCommit } from '@renderer/utils/vueTools'
import { player as eventPlayerNames, taskbar as eventTaskbarNames } from '@renderer/event/names'
import { onTaskbarThumbarClick, setTaskbarThumbnailClip, setTaskbarThumbarButtons } from '@renderer/utils/tools'
// import store from '@renderer/store'
import { loveList, getList } from '@renderer/core/share/list'
import { playMusicInfo } from '@renderer/core/share/player'
export default () => {
const listAdd = useCommit('list', 'listAdd')
const listRemove = useCommit('list', 'listRemove')
// const setVisibleDesktopLyric = useCommit('setVisibleDesktopLyric')
// const setLockDesktopLyric = useCommit('setLockDesktopLyric')
const buttons = {
empty: true,
collect: false,
play: false,
prev: true,
next: true,
lrc: false,
lockLrc: false,
}
const setButtons = () => {
setTaskbarThumbarButtons(buttons)
}
const updateCollectStatus = () => {
let status = !!playMusicInfo.musicInfo && getList(loveList.id).some(musicInfo => playMusicInfo.musicInfo.songmid == musicInfo.songmid)
if (buttons.collect == status) return false
buttons.collect = status
return true
}
const handlePlay = () => {
if (buttons.empty) buttons.empty = false
buttons.play = true
setButtons()
}
const handlePause = () => {
if (buttons.empty) buttons.empty = false
buttons.play = false
setButtons()
}
const handleStop = async() => {
if (playMusicInfo.musicInfo != null) return
if (buttons.collect) buttons.collect = false
buttons.empty = true
setButtons()
}
const handleSetPlayInfo = () => {
if (!updateCollectStatus()) return
setButtons()
}
const handleSetTaskbarThumbnailClip = (clip) => {
setTaskbarThumbnailClip(clip)
}
// const updateSetting = () => {
// const setting = store.getters.setting
// buttons.lrc = setting.desktopLyric.enable
// buttons.lockLrc = setting.desktopLyric.isLock
// setButtons()
// }
const rTaskbarThumbarClick = onTaskbarThumbarClick((event, action) => {
switch (action) {
case 'play':
window.eventHub.emit(eventPlayerNames.setPlay)
break
case 'pause':
window.eventHub.emit(eventPlayerNames.setPause)
break
case 'prev':
window.eventHub.emit(eventPlayerNames.setPlayPrev)
break
case 'next':
window.eventHub.emit(eventPlayerNames.setPlayNext)
break
case 'collect':
if (!playMusicInfo.musicInfo) return
listAdd({ id: loveList.id, musicInfo: playMusicInfo.musicInfo })
if (updateCollectStatus()) setButtons()
break
case 'unCollect':
if (!playMusicInfo.musicInfo) return
listRemove({ listId: loveList.id, id: playMusicInfo.musicInfo.songmid })
if (updateCollectStatus()) setButtons()
break
// case 'lrc':
// setVisibleDesktopLyric(true)
// updateSetting()
// break
// case 'unLrc':
// setVisibleDesktopLyric(false)
// updateSetting()
// break
// case 'lockLrc':
// setLockDesktopLyric(true)
// updateSetting()
// break
// case 'unlockLrc':
// setLockDesktopLyric(false)
// updateSetting()
// break
}
})
window.eventHub.on(eventPlayerNames.play, handlePlay)
window.eventHub.on(eventPlayerNames.pause, handlePause)
window.eventHub.on(eventPlayerNames.stop, handleStop)
window.eventHub.on(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.on(eventTaskbarNames.setTaskbarThumbnailClip, handleSetTaskbarThumbnailClip)
onBeforeUnmount(() => {
rTaskbarThumbarClick()
window.eventHub.off(eventPlayerNames.play, handlePlay)
window.eventHub.off(eventPlayerNames.pause, handlePause)
window.eventHub.off(eventPlayerNames.stop, handleStop)
window.eventHub.off(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.off(eventTaskbarNames.setTaskbarThumbnailClip, handleSetTaskbarThumbnailClip)
})
return () => {
// const setting = store.getters.setting
// buttons.lrc = setting.desktopLyric.enable
// buttons.lockLrc = setting.desktopLyric.isLock
updateCollectStatus()
if (playMusicInfo.musicInfo != null) buttons.empty = false
setButtons()
}
}

View File

@ -15,7 +15,7 @@ export default ({ playNext }) => {
const { playIndex } = updatePlayIndex()
if (playIndex < 0 && !playMusicInfo.isTempPlay) { // 歌曲被移除
if (getList(playMusicInfo.listId).length) {
if (getList(playMusicInfo.listId).length && !global.isPlayedStop) {
playNext()
} else {
window.eventHub.emit(eventPlayerNames.setStop)

View File

@ -15,6 +15,7 @@ const names = {
},
player: {
setTogglePlay: 'setTogglePlay', // 播放/暂停切换
setPlay: 'setPlay', // 播放
setPause: 'setPause', // 暂停
setStop: 'setStop', // 停止
setPlayPrev: 'setPlayPrev', // 上一曲
@ -26,11 +27,12 @@ const names = {
playMusic: 'playMusic',
setPlayInfo: 'setPlayInfo',
updatePic: 'updatePic',
updateLyric: 'updateLyric',
setPlayInfo: 'setPlayInfo', // 设置播放信息
updatePic: 'updatePic', // 更新图片事件
updateLyric: 'updateLyric', // 更新歌词事件
activeTransition: 'activeTransition', // 激活进度条动画事件
playedStop: 'playedStop', // 定时停止事件
// 播放器事件
play: 'play',
@ -64,6 +66,9 @@ const names = {
send_sync_list: 'send_sync_list',
handle_sync_list: 'handle_sync_list',
},
taskbar: {
setTaskbarThumbnailClip: 'setTaskbarThumbnailClip',
},
}
for (const item of Object.keys(names)) {
@ -78,3 +83,4 @@ export const player = names.player
export const list = names.list
export const download = names.download
export const sync = names.sync
export const taskbar = names.taskbar

View File

@ -1,6 +1,6 @@
<template>
<Modal :show="visible" @close="handleCancel" @after-leave="afterLeave" :closeBtn="false" :teleport="teleport">
<main :class="$style.main">{{message}}</main>
<main class="scroll" :class="[$style.main, { 'select': selection }]">{{message}}</main>
<footer :class="$style.footer">
<Btn :class="$style.btn" v-if="showCancel" @click="handleCancel">{{cancelBtnText}}</Btn>
<Btn :class="$style.btn" @click="handleComfirm">{{confirmBtnText}}</Btn>
@ -30,6 +30,7 @@ export default {
cancelButtonText: '',
confirmButtonText: '',
teleport: '#root',
selection: false,
}
},
computed: {
@ -58,9 +59,9 @@ export default {
.main {
flex: auto;
min-height: 40px;
padding: 15px;
padding: 15px 15px 0;
font-size: 14px;
max-width: 320px;
// max-width: 320px;
min-width: 220px;
line-height: 1.5;
white-space: pre-line;
@ -68,7 +69,7 @@ export default {
.footer {
flex: none;
padding: 0 15px 15px;
padding: 15px;
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;

View File

@ -10,10 +10,11 @@ const defaultOptions = {
showCancel: false,
cancelButtonText: '',
confirmButtonText: '',
selection: false,
}
export const dialog = function(options) {
const { message, showCancel, cancelButtonText, confirmButtonText, teleport } =
const { message, showCancel, cancelButtonText, confirmButtonText, teleport, selection } =
Object.assign({}, defaultOptions, typeof options == 'string' ? { message: options } : options || {})
return new Promise((resolve, reject) => {
let app = createApp(Dialog, {
@ -32,6 +33,7 @@ export const dialog = function(options) {
instance.cancelButtonText = cancelButtonText
instance.confirmButtonText = confirmButtonText
instance.teleport = teleport
instance.selection = selection
// 挂载
document.getElementById('container').appendChild(instance.$el)

View File

@ -6,6 +6,9 @@ export default {
let theme = themes.find(theme => theme.id == state.setting.themeId)
return (theme && theme.className) || ''
},
font(state) {
return state.setting.font
},
themes(state) {
return {
active: state.setting.themeId,

View File

@ -162,6 +162,7 @@ const handleGetLyric = function(musicInfo, retryedSource = [], originMusic) {
reqPromise = Promise.reject(err)
}
return reqPromise.catch(err => {
// console.log(err)
if (!retryedSource.includes(musicInfo.source)) retryedSource.push(musicInfo.source)
return this.dispatch('list/getOtherSource', originMusic).then(otherSource => {
console.log('find otherSource', otherSource)
@ -169,7 +170,7 @@ const handleGetLyric = function(musicInfo, retryedSource = [], originMusic) {
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 handleGetLyric.call(this, item, retryedSource, originMusic)
}
}
return Promise.reject(err)
@ -438,6 +439,14 @@ const actions = {
dispatch('startTask')
return
}
if (err.message?.startsWith('Resume failed')) {
fs.unlink(downloadInfo.metadata.filePath, err => {
if (err) return commit('onError', { downloadInfo, errorMsg: '删除不匹配的文件失败:' + err.message })
dls[downloadInfo.key].start()
commit('setStatusText', { downloadInfo, text: '正在重试' })
})
return
}
if (err.code == 'ENOTFOUND') {
commit('onError', { downloadInfo, errorMsg: '链接失效' })
refreshUrl.call(_this, commit, downloadInfo, rootState.setting.download.isUseOtherSource)
@ -561,6 +570,7 @@ const actions = {
filePath: path.join(rootState.setting.download.savePath, downloadInfo.metadata.fileName),
})
dl.updateSaveInfo(rootState.setting.download.savePath, downloadInfo.metadata.fileName)
if (tryNum[downloadInfo.key]) tryNum[downloadInfo.key] = 0
try {
await dl.start()
} catch (error) {

View File

@ -32,6 +32,7 @@ const state = {
}
const playMusic = () => {
if (global.isPlayedStop) global.isPlayedStop = false
window.eventHub.emit(eventPlayerNames.playMusic)
}

View File

@ -68,7 +68,7 @@ const getters = {
const actions = {
getTags({ state, rootState, commit }) {
let source = rootState.setting.songList.source
return music[source].songList.getTags().then(result => commit('setTags', { tags: result, source }))
return music[source]?.songList.getTags().then(result => commit('setTags', { tags: result, source }))
},
getList({ state, rootState, commit }, page) {
let source = rootState.setting.songList.source
@ -79,7 +79,7 @@ const actions = {
if (state.list.list.length && state.list.key == key) return
if (cache.has(key)) return Promise.resolve(cache.get(key)).then(result => commit('setList', { result, key, page }))
commit('clearList')
return music[source].songList.getList(sortId, tabId, page).then(result => commit('setList', { result, key, page }))
return music[source]?.songList.getList(sortId, tabId, page).then(result => commit('setList', { result, key, page }))
},
getListDetail({ state, commit }, { id, source, page, isRefresh = false }) {
let key = `sdetail__${source}__${id}__${page}`
@ -89,7 +89,7 @@ const actions = {
return (
cache.has(key)
? Promise.resolve(cache.get(key))
: music[source].songList.getListDetail(id, page).then(result => ({ ...result, list: filterList(result.list) }))
: music[source]?.songList.getListDetail(id, page).then(result => ({ ...result, list: filterList(result.list) }))
).then(result => {
commit('setListDetail', { result, key, source, id, page })
return result.list
@ -102,7 +102,7 @@ const actions = {
if (isRefresh && cache.has(key)) cache.delete(key)
return cache.has(key)
? Promise.resolve(cache.get(key))
: music[source].songList.getListDetail(id, page).then(result => {
: music[source]?.songList.getListDetail(id, page).then(result => {
cache.set(key, result)
return result
})

View File

@ -3,6 +3,7 @@ export default {
state.setting.themeId = val
},
setSearchSource(state, { searchSource, tempSearchSource }) {
console.log(searchSource, tempSearchSource)
if (searchSource != null) state.setting.search.searchSource = searchSource
if (tempSearchSource != null) state.setting.search.tempSearchSource = tempSearchSource
},

View File

@ -153,7 +153,7 @@ class Task extends EventEmitter {
__initDownload(response) {
this.progress.total = parseInt(response.headers['content-length'] || 0)
let options = {}
let isResumable = this.options.forceResume || response.headers['accept-ranges'] !== 'none'
let isResumable = this.options.forceResume || response.headers['accept-ranges'] !== 'none' || (typeof response.headers['accept-ranges'] == 'string' && parseInt(response.headers['accept-ranges'].replace(/^bytes=(\d+)/, '$1')) > 0)
if (isResumable) {
options.flags = 'a'
if (this.progress.downloaded) this.progress.total -= 10
@ -227,8 +227,9 @@ class Task extends EventEmitter {
if (this.resumeLastChunk) {
chunk = this.__handleDiffChunk(chunk)
if (!chunk) {
this.__handleError(new Error('Resume failed, response chunk does not match.'))
this.stop()
this.__handleStop().finally(() => {
this.__handleError(new Error('Resume failed, response chunk does not match.'))
})
return
}
}

View File

@ -93,7 +93,7 @@ const easeInOutQuad = (t, b, c, d) => {
t--
return (-c / 2) * (t * (t - 2) - 1) + b
}
const handleScroll = (element, to, duration = 300, fn = () => {}) => {
const handleScrollY = (element, to, duration = 300, fn = () => {}) => {
if (!element) return fn()
const start = element.scrollTop || element.scrollY || 0
let cancel = false
@ -148,10 +148,72 @@ export const scrollTo = (element, to, duration = 300, fn = () => {}, delay = 0)
}
timeout = setTimeout(() => {
timeout = null
scrollCancelFn = handleScroll(element, to, duration, fn, delay)
scrollCancelFn = handleScrollY(element, to, duration, fn, delay)
}, delay)
} else {
cancelFn = handleScroll(element, to, duration, fn, delay)
cancelFn = handleScrollY(element, to, duration, fn, delay)
}
return cancelFn
}
const handleScrollX = (element, to, duration = 300, fn = () => {}) => {
if (!element) return fn()
const start = element.scrollLeft || element.scrollX || 0
let cancel = false
if (to > start) {
let maxScrollLeft = element.scrollWidth - element.clientWidth
if (to > maxScrollLeft) to = maxScrollLeft
} else if (to < start) {
if (to < 0) to = 0
} else return fn()
const change = to - start
const increment = 10
if (!change) return fn()
let currentTime = 0
let val
const animateScroll = () => {
currentTime += increment
val = parseInt(easeInOutQuad(currentTime, start, change, duration))
if (element.scrollTo) {
element.scrollTo(val, 0)
} else {
element.scrollLeft = val
}
if (currentTime < duration) {
if (cancel) return fn()
setTimeout(animateScroll, increment)
} else {
fn()
}
}
animateScroll()
return () => {
cancel = true
}
}
/**
* 设置滚动条位置
* @param {*} element 要设置滚动的容器 dom
* @param {*} to 滚动的目标位置
* @param {*} duration 滚动完成时间 ms
* @param {*} fn 滚动完成后的回调
* @param {*} delay 延迟执行时间
*/
export const scrollXTo = (element, to, duration = 300, fn = () => {}, delay = 0) => {
let cancelFn
let timeout
if (delay) {
let scrollCancelFn
cancelFn = () => {
timeout == null ? scrollCancelFn && scrollCancelFn() : clearTimeout(timeout)
}
timeout = setTimeout(() => {
timeout = null
scrollCancelFn = handleScrollX(element, to, duration, fn, delay)
}, delay)
} else {
cancelFn = handleScrollX(element, to, duration, fn, delay)
}
return cancelFn
}

View File

@ -193,4 +193,8 @@ export default {
}
})
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('kg__', '')
return `https://www.kugou.com/yy/rank/home/1-${id}.html`
},
}

View File

@ -718,6 +718,11 @@ export default {
}
})
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('id_', '')
return `https://www.kugou.com/yy/special/single/${id}.html`
},
}
// getList

View File

@ -191,4 +191,8 @@ export default {
}
})
},
// getDetailPageUrl(id) {
// return `http://www.kuwo.cn/rankList/${id}`
// },
}

View File

@ -307,6 +307,13 @@ export default {
getTags() {
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'kw' }))
},
getDetailPageUrl(id) {
if (/^digest-/.test(id)) {
let result = id.split('__')
id = result[1]
}
return `http://www.kuwo.cn/playlist_detail/${id}`
},
}
// getList

View File

@ -4,8 +4,7 @@ import { sizeFormate } from '../../index'
// const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]
const boardList = [{ id: 'mg__27553319', name: '尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: 'KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '网络榜', bangid: '15140034' }, { id: 'mg__21958042', name: '美国iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: '美国billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]
const boardList = [{ id: 'mg__27553319', name: '尖叫新歌榜', bangid: '27553319', webId: 'jianjiao_newsong' }, { id: 'mg__27186466', name: '尖叫热歌榜', bangid: '27186466', webId: 'jianjiao_hotsong' }, { id: 'mg__27553408', name: '尖叫原创榜', bangid: '27553408', webId: 'jianjiao_original' }, { id: 'mg__23189800', name: '港台榜', bangid: '23189800', webId: 'hktw' }, { id: 'mg__23189399', name: '内地榜', bangid: '23189399', webId: 'mainland' }, { id: 'mg__19190036', name: '欧美榜', bangid: '19190036', webId: 'eur_usa' }, { id: 'mg__23189813', name: '日韩榜', bangid: '23189813', webId: 'jpn_kor' }, { id: 'mg__23190126', name: '彩铃榜', bangid: '23190126', webId: 'coloring' }, { id: 'mg__15140045', name: 'KTV榜', bangid: '15140045', webId: 'ktv' }, { id: 'mg__15140034', name: '网络榜', bangid: '15140034', webId: 'network' }, { id: 'mg__21958042', name: '美国iTunes榜', bangid: '21958042', webId: 'itunes' }, { id: 'mg__21975570', name: '美国billboard榜', bangid: '21975570', webId: 'billboard' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815', webId: 'hito' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943', webId: 'mnet' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437', webId: 'uk' }]
export default {
limit: 200,
list: [
@ -216,4 +215,14 @@ export default {
}
})
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('mg__', '')
for (const item of boardList) {
if (item.bangid == id) {
return `https://music.migu.cn/v3/music/top/${item.webId}`
}
}
return null
},
}

View File

@ -355,6 +355,10 @@ export default {
getTags() {
return this.getTag()
},
getDetailPageUrl(id) {
return `https://music.migu.cn/v3/music/playlist/${id}`
},
}
// getList

View File

@ -237,4 +237,9 @@ export default {
})
})
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('tx__', '')
return `https://y.qq.com/n/ryqq/toplist/${id}`
},
}

View File

@ -32,7 +32,10 @@ export default {
},
handleResult(rawList) {
// console.log(rawList)
return rawList.map(item => {
const list = []
rawList.forEach(item => {
if (!item.strMediaMid) return
let types = []
let _types = {}
if (item.size128 !== 0) {
@ -64,7 +67,7 @@ export default {
}
}
// types.reverse()
return {
list.push({
singer: this.getSinger(item.singer),
name: item.songname,
albumName: item.albumname,
@ -83,8 +86,9 @@ export default {
types,
_types,
typeUrl: {},
}
})
})
return list
},
search(str, page = 1, { limit } = {}) {
if (limit == null) limit = this.limit

View File

@ -292,6 +292,10 @@ export default {
getTags() {
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'tx' }))
},
getDetailPageUrl(id) {
return `https://y.qq.com/n/ryqq/playlist/${id}`
},
}
// getList

View File

@ -199,4 +199,9 @@ export default {
source: 'wy',
}
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('wy__', '')
return `https://music.163.com/#/discover/toplist?id=${id}`
},
}

View File

@ -53,7 +53,7 @@ export default songmid => {
if (body.code !== 200 || !body?.lrc?.lyric) return Promise.reject(new Error('Get lyric failed'))
return {
lyric: body.lrc.lyric,
tlyric: body.tlyric.lyric,
tlyric: body.tlyric?.lyric ?? '',
// lxlyric: parseLyric(body.klyric.lyric),
}
})

View File

@ -289,6 +289,10 @@ export default {
getTags() {
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'wy' }))
},
getDetailPageUrl(id) {
return `https://music.163.com/#/playlist?id=${id}`
},
}
// getList

View File

@ -0,0 +1,93 @@
import { ref, computed } from '@renderer/utils/vueTools'
import { rendererSend, rendererOn, NAMES } from '@common/ipc'
import { isPlay } from '@renderer/core/share/player'
import store from '@renderer/store'
import { player as eventPlayerNames } from '@renderer/event/names'
global.isPlayedStop = false
const time = ref(-1)
const timeoutTools = {
inited: false,
isRunning: false,
timeout: null,
time: -1,
id: 'play__stop__timeout',
exit() {
const setting = store.getters.setting
global.isPlayedStop = true
if (!setting.player.waitPlayEndStop && isPlay.value) {
window.eventHub.emit(eventPlayerNames.setPause)
}
},
clearTimeout() {
rendererSend(NAMES.mainWindow.interval_cancel, this.id)
if (!this.isRunning) return
this.time = -1
time.value = -1
this.isRunning = false
},
start(_time) {
this.clearTimeout()
this.time = _time
time.value = _time
this.isRunning = true
rendererSend(NAMES.mainWindow.interval, {
time: 1000,
id: this.id,
})
},
init() {
if (this.inited) return
this.clearTimeout()
rendererOn(NAMES.mainWindow.interval_callback, (event, id) => {
if (id !== this.id) return
if (this.time > 0) {
this.time--
time.value--
} else {
this.clearTimeout()
this.exit()
}
})
this.inited = true
},
}
export const init = () => {
timeoutTools.init()
}
export const startTimeoutStop = time => {
if (global.isPlayedStop) global.isPlayedStop = false
timeoutTools.start(time)
}
export const stopTimeoutStop = () => {
if (global.isPlayedStop) global.isPlayedStop = false
timeoutTools.clearTimeout()
}
const formatTime = time => {
// let d = parseInt(time / 86400)
// d = d ? d.toString() + ':' : ''
// time = time % 86400
let h = parseInt(time / 3600)
h = h ? h.toString() + ':' : ''
time = time % 3600
const m = parseInt(time / 60).toString().padStart(2, '0')
const s = parseInt(time % 60).toString().padStart(2, '0')
return `${h}${m}:${s}`
}
export const useTimeout = () => {
const timeLabel = computed(() => {
return time.value > 0 ? formatTime(time.value) : ''
})
return {
time,
timeLabel,
}
}

View File

@ -13,6 +13,17 @@ export const setUserApi = source => {
})
}
export const onShowUserApiUpdateAlert = callback => {
rendererOn(NAMES.mainWindow.user_api_show_update_alert, callback)
return () => {
rendererOff(callback)
}
}
export const setAllowShowUserApiUpdateAlert = (id, enable) => {
return rendererInvoke(NAMES.mainWindow.user_api_set_allow_update_alert, { id, enable })
}
export const saveMyList = data => {
rendererSend(NAMES.mainWindow.save_playlist, {
type: 'myList',
@ -105,7 +116,7 @@ export const getSearchHistoryList = () => {
}
export const onUserApiStatus = callback => {
rendererOn(NAMES.mainWindow.user_api_status, (event, { status, message, apiInfo }) => callback({ status, message, apiInfo }))
rendererOn(NAMES.mainWindow.user_api_status, callback)
return () => {
rendererOff(callback)
}
@ -133,7 +144,7 @@ export const setDesktopLyricInfo = (type, data, info) => {
})
}
export const onGetDesktopLyricInfo = callback => {
rendererOn(NAMES.mainWindow.get_lyric_info, (event, info) => callback(event, info))
rendererOn(NAMES.mainWindow.get_lyric_info, callback)
return () => {
rendererOff(callback)
}
@ -299,3 +310,16 @@ export const hotKeySetConfig = (config) => {
export const hotKeyGetStatus = () => {
return rendererInvoke(NAMES.hotKey.status)
}
export const onTaskbarThumbarClick = callback => {
rendererOn(NAMES.mainWindow.taskbar_on_thumbar_button_click, callback)
return () => {
rendererOff(callback)
}
}
export const setTaskbarThumbnailClip = (clip) => {
return rendererInvoke(NAMES.mainWindow.taskbar_set_thumbnail_clip, clip)
}
export const setTaskbarThumbarButtons = (buttons) => {
rendererSend(NAMES.mainWindow.taskbar_set_thumbar_buttons, buttons)
}

View File

@ -16,6 +16,7 @@
<material-online-list
ref="songList"
@show-menu="hideListsMenu"
@play-list="handlePlayList"
@toggle-page="handleGetList"
:rowWidth="{r1: '5%', r2: 'auto', r3: '22%', r4: '22%', r5: '9%', r6: '15%'}"
:page="page"
@ -193,7 +194,6 @@ export default {
case 'play':
this.playSongListDetail({
boardId: board.id,
list: [...this.list],
id,
})
break
@ -220,12 +220,13 @@ export default {
sourceListId: `board__${boardId}`,
})
},
async playSongListDetail({ boardId, id, list }) {
async playSongListDetail({ boardId, id, index = 0 }) {
let isPlayingList = false
const list = this.tabId == boardId ? [...this.list] : null
if (list?.length) {
this.setTempList({
list,
index: 0,
index,
id,
})
isPlayingList = true
@ -242,11 +243,18 @@ export default {
} else {
this.setTempList({
list: fullList,
index: 0,
index,
id,
})
}
},
handlePlayList(index) {
this.playSongListDetail({
boardId: this.tabId,
id: `board__${this.source}__${this.tabId}`,
index,
})
},
},
}
</script>

View File

@ -2,7 +2,7 @@
div(:class="$style.search")
//- transition
div(:class="$style.header")
base-tab(:class="$style.tab" :list="sources" align="left" item-key="id" item-name="name" v-model="searchSourceId")
base-tab(:class="$style.tab" :list="sources" align="left" item-key="id" item-name="name" @change="handleSourceChange" v-model="searchSourceId")
div(:class="$style.main")
div(:class="$style.list" v-show="isLoading || listInfo.list.length")
div.thead(:class="$style.thead")
@ -109,12 +109,28 @@ export default {
isLoading: false,
}
},
beforeRouteUpdate(to, from, next) {
if (to.query.text === undefined) return
this.text = to.query.text
this.page = 1
this.handleSearch(this.text, this.page)
next()
beforeRouteUpdate(to, from) {
if (to.query.source && (this.sourceList[to.query.source] || to.query.source == 'all')) {
if (this.setting.search.searchSource != to.query.source) {
this.setSearchSource({
searchSource: to.query.source,
})
}
if (this.searchSourceId != to.query.source) {
this.searchSourceId = to.query.source
}
this.$nextTick(() => {
this.handleGetHotSearch()
})
}
if (to.query.text != null && this.text != to.query.text) {
this.text = to.query.text
}
this.$nextTick(() => {
this.page = 1
this.handleSearch(this.text, this.page)
})
},
created() {
this.listenEvent()
@ -124,18 +140,20 @@ export default {
},
mounted() {
// console.log('mounted')
//
if (!this.sourceList[this.setting.search.searchSource] && this.setting.search.searchSource != 'all') {
if (this.$route.query.source && (this.sourceList[this.$route.query.source] || this.$route.query.source == 'all')) {
this.setSearchSource({
searchSource: this.$route.query.source,
})
} else if (!this.sourceList[this.setting.search.searchSource] && this.setting.search.searchSource != 'all') { //
this.setSearchSource({
searchSource: 'kw',
})
}
this.searchSourceId = this.setting.search.searchSource
if (this.$route.query.text === undefined) {
if (this.$route.query.text == null) {
this.text = this.$store.getters['search/searchText']
this.page = this.listInfo.page
} else if (this.$route.query.text === '') {
} else if (this.$route.query.text == '') {
this.clearList()
} else {
this.text = this.$route.query.text
@ -156,18 +174,6 @@ export default {
'listInfo.list'() {
this.removeAllSelect()
},
searchSourceId(n) {
if (n === this.setting.search.searchSource) return
if (this.text !== '') this.isLoading = true
this.$nextTick(() => {
this.page = 1
this.handleSearch(this.text, this.page)
this.handleGetHotSearch()
})
this.setSearchSource({
searchSource: n,
})
},
},
computed: {
...mapGetters(['userInfo', 'setting']),
@ -417,7 +423,7 @@ export default {
this.getHotSearch(this.setting.search.searchSource)
},
handleNoitemSearch(text) {
this.$router.push({
this.$router.replace({
path: 'search',
query: {
text,
@ -512,6 +518,15 @@ export default {
break
}
},
handleSourceChange(source) {
this.$router.replace({
path: 'search',
query: {
text: this.text,
source,
},
})
},
},
}
</script>

View File

@ -19,7 +19,7 @@
<span :class="$style.listsLabel">{{loveList.name}}</span>
</li>
<li class="user-list"
:class="[$style.listsItem, {[$style.active]:item.id == listId}, {[$style.clicked]: listsData.rightClickItemIndex == index}, {[$style.fetching]: fetchingListStatus[item.id]}]" :data-index="index"
:class="[$style.listsItem, {[$style.active]: item.id == listId}, {[$style.clicked]: listsData.rightClickItemIndex == index}, {[$style.fetching]: fetchingListStatus[item.id]}]" :data-index="index"
@contextmenu="handleListsItemRigthClick($event, index)" :tips="item.name" v-for="(item, index) in userLists" :key="item.id"
>
<span :class="$style.listsLabel" @click="handleListToggle(item.id, index + 2)">{{item.name}}</span>
@ -41,7 +41,7 @@
<script>
import { mapMutations, mapActions } from 'vuex'
import { openSaveDir, saveLxConfigFile, selectDir, readLxConfigFile, filterFileName } from '@renderer/utils'
import { openSaveDir, saveLxConfigFile, selectDir, readLxConfigFile, filterFileName, openUrl } from '@renderer/utils'
import musicSdk from '@renderer/utils/music'
import DuplicateMusicModal from './DuplicateMusicModal'
import ListSortModal from './ListSortModal'
@ -97,6 +97,7 @@ export default {
rename: true,
duplicate: true,
sort: true,
sourceDetail: true,
import: true,
export: true,
sync: false,
@ -141,6 +142,11 @@ export default {
action: 'duplicate',
disabled: !this.listsData.itemMenuControl.duplicate,
},
{
name: this.$t('lists__source_detail'),
action: 'sourceDetail',
disabled: !this.listsData.itemMenuControl.sourceDetail,
},
{
name: this.$t('lists__import'),
action: 'import',
@ -197,8 +203,17 @@ export default {
...mapActions('leaderboard', {
getBoardListAll: 'getListAll',
}),
handle_key_mod_down() {
handle_key_mod_down(event) {
if (!this.keyEvent.isModDown) {
// console.log(event)
switch (event.event.target.tagName) {
case 'INPUT':
case 'SELECT':
case 'TEXTAREA':
return
default: if (event.event.target.isContentEditable) return
}
this.keyEvent.isModDown = true
this.setDisabledSort(false)
const dom_target = this.dom_lists_list.querySelector('.' + this.$style.editing)
@ -279,6 +294,7 @@ export default {
break
}
this.listsData.itemMenuControl.sort = !!getList(this.getTargetListInfo(index)?.id).length
this.listsData.itemMenuControl.sourceDetail = this.assertSupportDetail(source, index)
this.listsData.rightClickItemIndex = index
this.listsData.menuLocation.x = event.currentTarget.offsetLeft + event.offsetX
this.listsData.menuLocation.y = event.currentTarget.offsetTop + event.offsetY - this.dom_lists_list.scrollTop
@ -313,6 +329,9 @@ export default {
this.selectedSortListInfo = this.getTargetListInfo(index)
this.isShowListSortModal = true
break
case 'sourceDetail':
this.openSourceDetailPage(index)
break
case 'import':
this.handleImportList(index)
break
@ -373,6 +392,33 @@ export default {
}
return list
},
assertSupportDetail(source, index) {
if (source) {
const { sourceListId } = this.userLists[index]
if (sourceListId) {
if (/board__/.test(sourceListId)) {
// const id = sourceListId.replace(/board__/, '')
return !!musicSdk[source]?.leaderboard?.getDetailPageUrl
} else {
return !!musicSdk[source]?.songList?.getDetailPageUrl
}
}
}
return false
},
openSourceDetailPage(index) {
const { source, sourceListId } = this.userLists[index]
if (!sourceListId) return
let url
if (/board__/.test(sourceListId)) {
const id = sourceListId.replace(/board__/, '')
url = musicSdk[source].leaderboard.getDetailPageUrl(id)
} else {
url = musicSdk[source].songList.getDetailPageUrl(sourceListId)
}
if (!url) return
openUrl(url)
},
handleExportList(index) {
const list = this.getTargetListInfo(index)
if (!list) return

View File

@ -0,0 +1,175 @@
<template lang="pug">
material-modal(:show="modelValue" bg-close @close="handleCloseModal" @after-enter="$refs.dom_input.focus()" teleport="#view")
main(:class="$style.main")
h2 {{$t('play_timeout')}}
div(:class="$style.content")
div(:class="[$style.row, $style.inputGroup]")
base-input(:class="$style.input" ref="dom_input" v-model="time" type="number")
p(:class="$style.inputLabel") {{$t('play_timeout_unit')}}
div(:class="$style.row")
base-checkbox(id="play_timeout_end" v-model="currentStting.player.waitPlayEndStop" :label="$t('play_timeout_end')")
div(:class="[$style.row, $style.tip, { [$style.show]: !!timeLabel }]")
p {{$t('play_timeout_tip', { time: timeLabel })}}
div(:class="$style.footer")
base-btn(:class="$style.footerBtn" @click="handleCancel") {{$t(timeLabel ? 'play_timeout_stop' : 'play_timeout_close')}}
base-btn(:class="$style.footerBtn" @click="handleConfirm") {{$t(timeLabel ? 'play_timeout_update' : 'play_timeout_confirm')}}
</template>
<script>
import { currentStting } from '../setting'
import { useTimeout, startTimeoutStop, stopTimeoutStop } from '@renderer/utils/timeoutStop'
import { ref } from '@renderer/utils/vueTools'
const MAX_MIN = 1440
const rxp = /([1-9]\d*)/
export default {
props: {
modelValue: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const { timeLabel } = useTimeout()
const time = ref(currentStting.value.player.waitPlayEndStopTime)
const handleCloseModal = () => {
emit('update:modelValue', false)
}
const handleCancel = () => {
if (timeLabel.value) {
stopTimeoutStop()
}
handleCloseModal()
}
const verify = () => {
const orgText = time.value
let text = time.value
if (rxp.test(text)) {
text = RegExp.$1
if (parseInt(text) > MAX_MIN) {
text = text.substring(0, text.length - 1)
}
} else {
text = ''
}
time.value = text
return text && orgText == text ? parseInt(text) : ''
}
const handleConfirm = () => {
let time = verify()
if (time == '') return
currentStting.value.player.waitPlayEndStopTime = time
startTimeoutStop(time * 60)
handleCloseModal()
}
return {
currentStting,
timeLabel,
time,
handleCloseModal,
handleCancel,
handleConfirm,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
padding: 15px;
max-width: 530px;
min-width: 280px;
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;
}
}
.content {
padding-top: 15px;
font-size: 14px;
}
.row {
padding-top: 5px;
}
.inputGroup {
display: flex;
align-items: center;
}
.input {
flex: auto;
}
.inputLabel {
flex: none;
margin-left: 10px;
}
.tip {
visibility: hidden;
&.show {
visibility: visible;
}
}
.footer {
margin-top: 20px;
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(#root.@{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

@ -9,12 +9,15 @@ dd
label {{$t('theme_' + theme.className)}}
dd
.gap-top.top
base-checkbox(id="setting_show_animate" v-model="currentStting.isShowAnimation" :label="$t('setting__basic_show_animation')")
.gap-top
base-checkbox(id="setting_animate" v-model="currentStting.randomAnimate" :label="$t('setting__basic_animation')")
.gap-top
base-checkbox(id="setting_to_tray" v-model="currentStting.tray.isShow" :label="$t('setting__basic_to_tray')")
div
.gap-top.top
base-checkbox(id="setting_show_animate" v-model="currentStting.isShowAnimation" :label="$t('setting__basic_show_animation')")
.gap-top
base-checkbox(id="setting_animate" v-model="currentStting.randomAnimate" :label="$t('setting__basic_animation')")
.gap-top
base-checkbox(id="setting_to_tray" v-model="currentStting.tray.isShow" :label="$t('setting__basic_to_tray')")
p.gap-top
base-btn.btn(min @click="isShowPlayTimeoutModal = true") {{$t('setting__play_timeout')}} {{ timeLabel ? ` (${timeLabel})` : '' }}
dd(:tips="$t('setting__basic_source_title')")
h3#basic_source {{$t('setting__basic_source')}}
@ -42,12 +45,17 @@ dd(:tips="$t('setting__basic_sourcename_title')")
div
base-checkbox.gap-left(v-for="item in sourceNameTypes" :key="item.id" :id="`setting_abasic_sourcename_${item.id}`"
name="setting_basic_sourcename" need v-model="currentStting.sourceNameType" :value="item.id" :label="item.label")
dd
h3#basic_control_btn_position {{$t('setting__basic_control_btn_position')}}
div
base-checkbox.gap-left(v-for="item in controlBtnPositionList" :key="item.id" :id="`setting_basic_control_btn_position_${item.id}`"
name="setting_basic_control_btn_position" need v-model="currentStting.controlBtnPosition" :value="item.id" :label="item.name")
dd
h3#basic_font {{$t('setting__basic_font')}}
div
base-selection.gap-teft(:list="fontList" v-model="currentStting.font" item-key="id" item-name="label")
play-timeout-modal(v-model="isShowPlayTimeoutModal")
user-api-modal(v-model="isShowUserApiModal")
</template>
@ -58,12 +66,17 @@ import { langList } from '@/lang'
import { currentStting } from '../setting'
import { setWindowSize } from '@renderer/utils'
import apiSourceInfo from '@renderer/utils/music/api-source-info'
import { useTimeout } from '@renderer/utils/timeoutStop'
import { getSystemFonts } from '@renderer/utils/tools'
import PlayTimeoutModal from './PlayTimeoutModal'
import UserApiModal from './UserApiModal'
export default {
name: 'SettingBasic',
components: {
PlayTimeoutModal,
UserApiModal,
},
setup() {
@ -81,6 +94,9 @@ export default {
apiSource.value = visible
})
const isShowPlayTimeoutModal = ref(false)
const { timeLabel } = useTimeout()
const isShowUserApiModal = ref(false)
const getApiStatus = () => {
let status
@ -134,16 +150,26 @@ export default {
]
})
const systemFontList = ref([])
const fontList = computed(() => {
return [{ id: '', label: t('setting__desktop_lyric_font_default') }, ...systemFontList.value]
})
getSystemFonts().then(fonts => {
systemFontList.value = fonts.map(f => ({ id: f, label: f.replace(/(^"|"$)/g, '') }))
})
return {
currentStting,
themes,
isShowPlayTimeoutModal,
timeLabel,
apiSources,
isShowUserApiModal,
windowSizeList,
langList,
sourceNameTypes,
controlBtnPositionList,
fontList,
}
},
}

View File

@ -5,6 +5,8 @@ dd
base-checkbox(id="setting_list_showSource_enable" v-model="currentStting.list.isShowSource" :label="$t('setting__list_source')")
.gap-top
base-checkbox(id="setting_list_scroll_enable" v-model="currentStting.list.isSaveScrollLocation" :label="$t('setting__list_scroll')")
.gap-top
base-checkbox(id="setting_list_clickAction_enable" v-model="currentStting.list.isClickPlayList" :label="$t('setting__list_click_action')")
dd(:tips="$t('setting__basic_sourcename_title')")
h3#list_addMusicLocationType {{$t('setting__list_add_music_location_type')}}
div

View File

@ -7,6 +7,8 @@ material-modal(:show="modelValue" bg-close @close="handleClose" teleport="#view"
div(:class="$style.listLeft")
h3 {{api.name}}
p {{api.description}}
div
base-checkbox(:class="$style.checkbox" :id="`user_api_${api.id}`" v-model="api.allowShowUpdateAlert" @change="handleChangeAllowUpdateAlert(api, $event)" :label="$t('user_api__allow_show_update_alert')")
base-btn(:class="$style.listBtn" outline :tips="$t('user_api__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')
@ -29,6 +31,7 @@ import { promises as fsPromises } from 'fs'
import { selectDir, openUrl } from '@renderer/utils'
import apiSourceInfo from '@renderer/utils/music/api-source-info'
import { userApi, apiSource } from '@renderer/core/share'
import { setAllowShowUserApiUpdateAlert } from '@renderer/utils/tools'
export default {
props: {
@ -50,6 +53,13 @@ export default {
},
methods: {
handleImport() {
if (this.userApi.list.length > 20) {
this.$dialog({
message: this.$t('user_api__max_tip'),
confirmButtonText: this.$t('ok'),
})
return
}
selectDir({
title: this.$t('user_api__import_file'),
properties: ['openFile'],
@ -84,6 +94,9 @@ export default {
handleOpenUrl(url) {
openUrl(url)
},
handleChangeAllowUpdateAlert(api, enable) {
setAllowShowUserApiUpdateAlert(api.id, enable)
},
},
}
</script>
@ -114,6 +127,12 @@ export default {
color: @color-theme;
}
.checkbox {
margin-top: 3px;
font-size: 14px;
opacity: .86;
}
.content {
flex: auto;
min-height: 100px;

View File

@ -8,6 +8,8 @@ export const currentStting = ref({
volume: 1,
mediaDeviceId: 'default',
isMediaDeviceRemovedStopPlay: false,
waitPlayEndStop: true,
waitPlayEndStopTime: 0,
},
desktopLyric: {
enable: false,
@ -73,6 +75,7 @@ export const currentStting = ref({
langId: 'cns',
themeId: 0,
sourceId: 0,
font: '',
isShowAnimation: true,
randomAnimate: true,
isAgreePact: false,

View File

@ -11,10 +11,10 @@ div(:class="$style.container")
h3(:title="listDetail.info.name || selectListInfo.name") {{listDetail.info.name || selectListInfo.name}}
p(:title="listDetail.info.desc || selectListInfo.desc") {{listDetail.info.desc || selectListInfo.desc}}
div(:class="$style.songListHeaderRight")
base-btn(:class="$style.headerRightBtn" :disabled="detailLoading" @click="playSongListDetail") {{$t('list__play')}}
base-btn(:class="$style.headerRightBtn" :disabled="detailLoading" @click="playSongListDetail()") {{$t('list__play')}}
base-btn(:class="$style.headerRightBtn" :disabled="detailLoading" @click="addSongListDetail") {{$t('list__collect')}}
base-btn(:class="$style.headerRightBtn" @click="hideListDetail") {{$t('back')}}
material-online-list(ref="songList" @toggle-page="handleToggleListDetailPage" :page="listDetail.page" :limit="listDetail.limit" :total="listDetail.total"
material-online-list(ref="songList" @play-list="playSongListDetail" @toggle-page="handleToggleListDetailPage" :page="listDetail.page" :limit="listDetail.limit" :total="listDetail.total"
:list="listDetail.list" :noItem="isGetDetailFailed ? $t('list__load_failed') : $t('list__loading')")
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
div(:class="$style.songListContainer" v-show="!isVisibleListDetail")
@ -315,14 +315,14 @@ export default {
sourceListId: this.listDetail.id,
})
},
async playSongListDetail() {
async playSongListDetail(index = 0) {
if (!this.listDetail.info.name) return
const id = `${this.listDetail.source}__${this.listDetail.id}`
let isPlayingList = false
if (this.listDetail.list?.length) {
this.setTempList({
list: [...this.listDetail.list],
index: 0,
index,
id,
})
isPlayingList = true
@ -339,7 +339,7 @@ export default {
} else {
this.setTempList({
list,
index: 0,
index,
id,
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

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