Merge branch 'dev'

pull/930/merge v1.19.0
lyswhut 2022-03-20 13:52:15 +08:00
commit f10c1e923e
115 changed files with 2368 additions and 821 deletions

View File

@ -92,6 +92,11 @@ jobs:
name: lx-music-desktop-win_arm64-green
path: build/*win_arm64 green.7z
- name: Generate file MD5
run: |
cd build
Get-FileHash *.exe,*.7z -Algorithm MD5 | Format-List
Mac:
name: Mac
runs-on: macos-latest
@ -143,6 +148,11 @@ jobs:
name: lx-music-desktop-mac-dmg-arm64
path: build/*-arm64.dmg
- name: Generate file MD5
run: |
cd build
md5 *.dmg
Linux:
name: Linux
runs-on: ubuntu-latest
@ -231,3 +241,8 @@ jobs:
with:
name: lx-music-desktop-x64-pacman
path: build/* x64.pacman
- name: Generate file MD5
run: |
cd build
md5sum *.deb *.rpm *.pacman *.AppImage

View File

@ -49,6 +49,11 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BT_TOKEN: ${{ secrets.BT_TOKEN }}
- name: Generate file MD5
run: |
cd build
Get-FileHash *.exe,*.7z -Algorithm MD5 | Format-List
Mac:
name: Mac
runs-on: macos-latest
@ -89,6 +94,11 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BT_TOKEN: ${{ secrets.BT_TOKEN }}
- name: Generate file MD5
run: |
cd build
md5 *.dmg
Linux:
name: Linux
runs-on: ubuntu-latest
@ -134,3 +144,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BT_TOKEN: ${{ secrets.BT_TOKEN }}
- name: Generate file MD5
run: |
cd build
md5sum *.deb *.rpm *.pacman *.AppImage

View File

@ -6,6 +6,53 @@ 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.19.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.18.0...v1.19.0) - 2022-03-20
### 新增
- 新增对播放详情页歌词大小、是否缩放、对齐方式的设置,可以去设置-播放详情页设置查看
- 新增播放详情页通过歌词调整播放进度,默认关闭,需要到设置-播放详情页设置开启,开启后在播放详情页拖动歌词时将会出现跳转当前行歌词播放的按钮
- 新增全屏状态按F11可以进入、退出全屏状态由于全屏时会隐藏控制栏按钮所以需要使用鼠标右键双击详情页的任意地方都可以来关闭播放详情页
- 新增动态主题“道法自然”,你可以预先设置一个亮色主题及暗色主题,此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。注:鼠标 右击 此主题项即可打开亮、暗色主题设置窗口。
- 新增对kw源卡拉OK歌词的支持
### 优化
- 优化Windows任务栏缩略图工具栏控制按钮在浅色任务栏下的显示效果
- 添加音频可视化与音频输出设备冲突的提示
- 优化歌词的播放偏移
- 优化托盘菜单操作(#686
- 优化播放下载列表时的切歌性能
### 修复
- 修复“当前的声音输出设备被改变时暂停播放歌曲”设置无效的问题
- 修复桌面歌词没有处理停止播放状态的问题
- 修复AppImage包无法运行的问题
- 修复Windows任务栏缩略图工具栏控制按钮的歌曲收藏按钮状态更新问题
- 修复使用链接导入的歌单无法在我的列表打开原歌单详情页的问题
- 修复播放下载列表的歌曲时增删下载任务导致正在的歌曲序号改变时,不会更新到新增序号的问题
### 文档
添加LX中定义的快捷操作汇总说明到常见问题中这是目前可用的鼠标、键盘快捷操作它们都可以在更新日志中找到
- 鼠标右击播放栏的歌曲图片封面可以定位当前播放的歌曲
- 鼠标右击播放栏进度条上的LRC按钮可以锁定/解锁桌面歌词
- 歌曲搜索框、歌单链接输入框内鼠标右击可以将当前剪贴板上的文字粘贴到输入框内
- 鼠标右击搜索界面中的单条搜索历史可以将其移除
- 歌曲列表内的文字在选中后,鼠标右击可以复制已选中的文字,此功能只对搜索、歌单、排行榜、我的列表中的列表有效
- 鼠标在播放详情页内右键双击可以关闭播放详情页
- 鼠标左击播放栏上的歌曲名字可以将它复制
- 鼠标右击“道法自然英文Auto”主题可以打开亮、暗主题设置窗口
- 歌曲搜索框的候选内容可以用键盘上下方向键选择,按回车键搜索已选内容
- 在歌单详情页按退格键可以返回歌单列表
- 歌曲列表中可以使用Ctrl、Shift键进行多选这类似Windows下的文件选择详情看常见问题列表多选部分
- 在我的列表内可以使用Ctrl+f键打开搜索框进行列表内歌曲搜索搜索框按Esc键可以关闭搜索框搜索框内按上下方向键可以选择歌曲按回车键跳转到已选歌曲按Ctrl+回车可以跳转并播放已选歌曲
- 在我的列表按住Ctrl键可以进入列表拖动模式此时可以用鼠标拖动列表调整列表的位置
- 编辑列表名时按Esc键可以取消编辑
- 按F11可以进入、退出全屏状态
## [1.18.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.17.1...v1.18.0) - 2022-02-26
### 新增

20
FAQ.md
View File

@ -6,7 +6,7 @@
洛雪音乐的最初定位不是作为播放器开发的,它主要用于**查找歌曲**,软件的播放功能仅用于试听,不建议用作为常用播放器使用。
## LX Music中的音乐播放列表机制
## 音乐播放列表机制
1. 默认情况下,播放搜索列表、歌单列表、排行榜列表的歌曲时会自动将该歌曲添加到“我的列表”的试听列表后再播放,手动将歌曲添加到试听列表,再去试听列表找到这首歌点播放是等价的
2. 如果你想要播放多首歌曲,需要使用多选功能(若不知道如何多选请看常见问题)多选后,将歌曲这些歌曲添加到“我的列表”播放,或使用稍后播放功能播放
@ -15,6 +15,24 @@
5. 对于排行榜详情列表除了可以使用第2条的方式播放外你可以在右击排行榜名字后弹出的菜单中播放或收藏整个排行榜这与第四条的歌单中的播放、与收藏按钮功能一致
6. v1.18.0及之后新增了“双击列表里的歌曲时自动切换到当前列表播放”设置,默认关闭,此功能仅对歌单、排行榜有效
## 可用的鼠标、键盘快捷操作
- 鼠标右击播放栏的歌曲图片封面可以定位当前播放的歌曲
- 鼠标右击播放栏进度条上的`LRC`按钮可以锁定/解锁桌面歌词
- 歌曲搜索框、歌单链接输入框内鼠标右击可以将当前剪贴板上的文字粘贴到输入框内
- 鼠标右击搜索界面中的单条搜索历史可以将其移除
- 歌曲列表内的文字在选中后,鼠标右击可以复制已选中的文字,此功能只对搜索、歌单、排行榜、我的列表中的列表有效
- 鼠标在播放详情页内右键双击可以关闭播放详情页
- 鼠标左击播放栏上的歌曲名字可以将它复制
- 鼠标右击设置-主题设置的“道法自然英文Auto”主题可以打开亮、暗主题设置窗口
- 歌曲搜索框的候选内容可以用键盘上下方向键选择,按回车键搜索已选内容
- 在歌单详情页按退格键可以返回歌单列表
- 歌曲列表中可以使用`Ctrl`、`Shift`键进行多选这类似Windows下的文件选择详情看常见问题列表多选部分
- 在我的列表内可以使用`Ctrl + f`键打开搜索框进行列表内歌曲搜索,搜索框按`Esc`键可以关闭搜索框,搜索框内按上下方向键可以选择歌曲,按`回车`键跳转到已选歌曲,按`Ctrl + 回车`可以跳转并播放已选歌曲
- 在我的列表按住`Ctrl`键可以进入列表拖动模式,此时可以用鼠标拖动列表调整列表的位置
- 编辑列表名时按`Esc`键可以取消编辑
- 按`F11`可以进入、退出全屏状态v1.19.0新增)
## 歌曲无法试听与下载
### 所有歌曲都提示 `请求异常😮,可以多试几次,若还是不行就换一首吧。。。`

View File

@ -56,6 +56,8 @@
从v1.17.0起支持 Scheme URL可以使用此功能从浏览器等场景下调用LX Music我们开发了一个[油猴脚本](https://github.com/lyswhut/lx-music-script#readme)配套使用,<br>
脚本安装地址:<https://greasyfork.org/zh-CN/scripts/438148><br>
若你想自己调用LX Music可以看常见问题[Scheme URL支持](https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md#scheme-url%E6%94%AF%E6%8C%81)部分说明
#### 启动参数
目前软件已支持的启动参数如下:
@ -76,7 +78,7 @@
- Windows`%APPDATA%/lx-music-desktop`
- Linux`$XDG_CONFIG_HOME/lx-music-desktop` 或 `~/.config/lx-music-desktop`
- macOS`~/Library/Application/lx-music-desktop`
- macOS`~/Library/Application Support/lx-music-desktop`
在Windows平台下若程序目录下存在`portable`目录则自动使用此目录作为数据存储目录v1.17.0新增)。

818
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.18.0",
"version": "1.19.0",
"description": "一个免费的音乐查找助手",
"main": "./dist/electron/main.js",
"productName": "lx-music-desktop",
@ -173,7 +173,7 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.17.5",
"@babel/core": "^7.17.8",
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
@ -183,25 +183,25 @@
"@babel/preset-env": "^7.16.11",
"babel-loader": "^8.2.3",
"babel-preset-minify": "^0.5.1",
"browserslist": "^4.19.3",
"browserslist": "^4.20.2",
"cfonts": "^2.10.0",
"chalk": "^4.1.2",
"changelog-parser": "^2.8.0",
"changelog-parser": "^2.8.1",
"copy-webpack-plugin": "^10.2.4",
"core-js": "^3.21.1",
"cross-env": "^7.0.3",
"css-loader": "^6.6.0",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"del": "^6.0.0",
"electron": "^13.6.9",
"electron-builder": "^23.0.1",
"electron-builder": "^23.0.2",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"electron-to-chromium": "^1.4.73",
"electron-to-chromium": "^1.4.88",
"electron-updater": "^5.0.0",
"eslint": "^8.10.0",
"eslint": "^8.11.0",
"eslint-config-standard": "^16.0.3",
"eslint-formatter-friendly": "^7.0.0",
"eslint-formatter-friendly": "git+https://github.com/lyswhut/eslint-friendly-formatter.git#2170d1320e2fad13615a9dcf229669f0bb473a53",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-node": "^11.1.0",
@ -214,9 +214,9 @@
"less": "^4.1.2",
"less-loader": "^10.2.0",
"markdown-it": "^12.3.2",
"mini-css-extract-plugin": "^2.5.3",
"mini-css-extract-plugin": "^2.6.0",
"node-loader": "^2.0.0",
"postcss": "^8.4.7",
"postcss": "^8.4.12",
"postcss-loader": "^6.2.1",
"postcss-pxtorem": "^6.0.0",
"pug": "^3.0.2",
@ -224,7 +224,7 @@
"pug-plain-loader": "^1.1.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"spinnies": "^0.5.1",
"spinnies": "git+https://github.com/lyswhut/spinnies.git#233305c58694aa3b053e3ab9af9049993f918b9d",
"svg-sprite-loader": "^6.0.11",
"svg-transform-loader": "^2.0.13",
"svgo-loader": "^3.0.0",
@ -232,7 +232,7 @@
"url-loader": "^4.1.1",
"vue-loader": "^17.0.0",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.69.1",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4",
"webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1",
@ -244,7 +244,7 @@
"electron-log": "^4.4.6",
"electron-store": "^8.0.1",
"font-list": "github:lyswhut/node-font-list#4edbb1933b49a9bac1eedd63a31da16b487fe57d",
"http-terminator": "^3.0.4",
"http-terminator": "^3.2.0",
"iconv-lite": "^0.6.3",
"image-size": "^1.0.1",
"koa": "^2.13.4",
@ -255,10 +255,10 @@
"request": "^2.88.2",
"socket.io": "^4.4.1",
"sortablejs": "^1.14.0",
"utf-8-validate": "^5.0.8",
"utf-8-validate": "^5.0.9",
"vue": "^3.2.31",
"vue-i18n": "^9.2.0-beta.30",
"vue-router": "^4.0.12",
"vue-i18n": "^9.2.0-beta.32",
"vue-router": "^4.0.14",
"vuex": "^4.0.2"
}
}

View File

@ -1,29 +1,44 @@
### 新增
- 新增“双击列表里的歌曲时自动切换到当前列表播放”设置,此功能仅对歌单、排行榜有效,默认关闭
- 新增打开收藏的在线列表的对应平台详情页功能,可以在我的列表-列表右键菜单中使用
- 新增定时暂停播放功能,由于此功能大多数人可能不常用,所以将其放在设置-基本设置中
- 新增任务栏缩略图工具栏控制按钮此功能仅在Windows平台可用按钮分别为收藏/取消收藏(将歌曲添加到“我的收藏”列表)、上一曲、播放/暂停、下一曲
- 新增设置-基本设置-软件字体设置此设置可用于设置主界面的字体已知的问题Windows 7 下可能会出现字体列表为空的情况,这是当前系统的 Powershell 版本小于5.1导致的,请自行尝试看常见解决)
- 新增Scheme URL对音乐搜索的调用支持详情看常见问题-Scheme URL支持
- 新增Scheme URL以url传参的方式调用详情看常见问题-Scheme URL支持
- 自定义源新增更新弹窗方法,同时自定义源管理新增是否允许源显示更新弹窗设置(出于防止滥用考虑),当源作者想要通知用户源已更新时,可以调用此方法弹窗告诉用户,调用说明看常见问题-自定义源部分
- 新增对播放详情页歌词大小、是否缩放、对齐方式的设置,可以去设置-播放详情页设置查看
- 新增播放详情页通过歌词调整播放进度,默认关闭,需要到设置-播放详情页设置开启,开启后在播放详情页拖动歌词时将会出现跳转当前行歌词播放的按钮
- 新增全屏状态按F11可以进入、退出全屏状态由于全屏时会隐藏控制栏按钮所以需要使用鼠标右键双击详情页的任意地方都可以来关闭播放详情页
- 新增动态主题“道法自然”,你可以预先设置一个亮色主题及暗色主题,此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。注:鼠标 右击 此主题项即可打开亮、暗色主题设置窗口。
- 新增对kw源卡拉OK歌词的支持
### 优化
- 过滤tx源某些不支持播放的歌曲解决播放此类内容会导致意外的问题
- 把歌曲的热门评论与最新评论拆分成两个列表显示
- 优化Windows任务栏缩略图工具栏控制按钮在浅色任务栏下的显示效果
- 添加音频可视化与音频输出设备冲突的提示
- 优化歌词的播放偏移
- 优化托盘菜单操作(#686
- 优化播放下载列表时的切歌性能
### 修复
- 修复排行榜名字右击菜单的播放功能在播放非激活的列表时的列表获取问题
- 修复修改列表名时无法使用`Ctrl`键的问题
- 修复wy源某些歌曲获取歌词翻译的问题处理
- 修复下载功能的歌词换源时会进入死循环的问题
- 修复某些歌曲无法下载的问题
- 修复windows平台下软件目录存在`portable`文件夹时,仍会创建`C:\Users\<user>\AppData\Roaming\lx-music-desktop\Dictionaries\en-US-9-0.bdic`文件的问题现在不会再创建文件但仍会创建空目录Electron的问题目前暂无解决方法
- 修复播放器的停止逻辑问题
- 修复“当前的声音输出设备被改变时暂停播放歌曲”设置无效的问题
- 修复桌面歌词没有处理停止播放状态的问题
- 修复AppImage包无法运行的问题
- 修复Windows任务栏缩略图工具栏控制按钮的歌曲收藏按钮状态更新问题
- 修复使用链接导入的歌单无法在我的列表打开原歌单详情页的问题
- 修复播放下载列表的歌曲时增删下载任务导致正在的歌曲序号改变时,不会更新到新增序号的问题
### 其他
### 文档
- 更新electron到v13.6.9
添加LX中定义的快捷操作汇总说明到常见问题中这是目前可用的鼠标、键盘快捷操作它们都可以在更新日志中找到
- 鼠标右击播放栏的歌曲图片封面可以定位当前播放的歌曲
- 鼠标右击播放栏进度条上的LRC按钮可以锁定/解锁桌面歌词
- 歌曲搜索框、歌单链接输入框内鼠标右击可以将当前剪贴板上的文字粘贴到输入框内
- 鼠标右击搜索界面中的单条搜索历史可以将其移除
- 歌曲列表内的文字在选中后,鼠标右击可以复制已选中的文字,此功能只对搜索、歌单、排行榜、我的列表中的列表有效
- 鼠标在播放详情页内右键双击可以关闭播放详情页
- 鼠标左击播放栏上的歌曲名字可以将它复制
- 鼠标右击“道法自然英文Auto”主题可以打开亮、暗主题设置窗口
- 歌曲搜索框的候选内容可以用键盘上下方向键选择,按回车键搜索已选内容
- 在歌单详情页按退格键可以返回歌单列表
- 歌曲列表中可以使用Ctrl、Shift键进行多选这类似Windows下的文件选择详情看常见问题列表多选部分
- 在我的列表内可以使用Ctrl+f键打开搜索框进行列表内歌曲搜索搜索框按Esc键可以关闭搜索框搜索框内按上下方向键可以选择歌曲按回车键跳转到已选歌曲按Ctrl+回车可以跳转并播放已选歌曲
- 在我的列表按住Ctrl键可以进入列表拖动模式此时可以用鼠标拖动列表调整列表的位置
- 编辑列表名时按Esc键可以取消编辑
- 按F11可以进入、退出全屏状态

File diff suppressed because one or more lines are too long

View File

@ -106,7 +106,7 @@ module.exports = {
},
{
id: 13,
name: '黑纸白字',
name: '黑灯瞎火',
className: 'black',
},
{
@ -125,4 +125,6 @@ module.exports = {
className: 'happy_new_year',
},
],
themeLights: [0, 1, 2, 3, 4, 10, 5, 6, 11, 12, 7, 8, 9],
themeDarks: [13, 7],
}

View File

@ -2,7 +2,7 @@ const path = require('path')
const os = require('os')
const defaultSetting = {
version: '1.0.51',
version: '1.0.54',
player: {
togglePlayMethod: 'listLoop',
highQuality: false,
@ -19,6 +19,14 @@ const defaultSetting = {
waitPlayEndStop: true,
waitPlayEndStopTime: '',
},
playDetail: {
isZoomActiveLrc: true,
isShowLyricProgressSetting: false,
style: {
fontSize: 100,
align: 'center',
},
},
desktopLyric: {
enable: false,
isLock: false,
@ -97,7 +105,11 @@ const defaultSetting = {
port: '23332',
},
windowSizeId: 2,
themeId: 0,
theme: {
id: 0,
lightId: 0,
darkId: 13,
},
langId: null,
sourceId: 'kw',
apiSource: 'temp',
@ -116,7 +128,7 @@ const overwriteSetting = {
// 使用新年皮肤
if (new Date().getMonth() < 2) {
defaultSetting.themeId = 9
defaultSetting.theme.id = 9
defaultSetting.desktopLyric.theme = 3
}

View File

@ -4,6 +4,7 @@ const names = {
close: 'close',
min: 'min',
max: 'max',
fullscreen: 'fullscreen',
set_app_name: 'set_app_name',
clear_cache: 'clear_cache',
get_cache_size: 'get_cache_size',
@ -15,6 +16,7 @@ const names = {
interval_callback: 'interval_callback',
interval_cancel: 'interval_cancel',
open_dev_tools: 'open_dev_tools',
system_theme_change: 'system_theme_change',
set_music_meta: 'set_music_meta',
progress: 'progress',

View File

@ -157,11 +157,6 @@ exports.initSetting = isShowErrorAlert => {
electronStore_config.set('setting.list.isSaveScrollLocation', scroll.enable)
delete setting.list.scroll
}
if (setting.player.isShowLyricTransition != null) { // 修正拼写问题 v1.8.2 及以前
setting.player.isShowLyricTranslation = setting.player.isShowLyricTransition
delete setting.player.isShowLyricTransition
}
}
// 从我的列表分离下载列表 v1.7.0 后
@ -173,6 +168,18 @@ exports.initSetting = isShowErrorAlert => {
const { version: settingVersion, setting: newSetting } = exports.mergeSetting(setting, electronStore_config.get('version'))
// 修正拼写问题 v1.8.2 及以前
if (newSetting.player.isShowLyricTransition != null) {
newSetting.player.isShowLyricTranslation = newSetting.player.isShowLyricTransition
delete newSetting.player.isShowLyricTransition
}
// 迁移v1.19.0之前的主题设置
if (newSetting.themeId != null) {
newSetting.theme.id = newSetting.themeId
delete newSetting.themeId
}
// 重置 ^0.18.2 排行榜ID
if (!newSetting.leaderboard.tabId.includes('__')) newSetting.leaderboard.tabId = 'kw__16'

View File

@ -1,6 +1,7 @@
{
"action": "Manage",
"agree": "Accept",
"alert_button_text": "All right",
"audio_visualization": "Audio visualization (experimental)",
"back": "Back",
"cancel_button_text": "Cancel",
@ -168,7 +169,7 @@
"player__music_singer": "Artist: ",
"player__next": "Next",
"player__pause": "Pause",
"player__pic_tip": "Right click to locate the currently playing song in \"My List\"",
"player__pic_tip": "Play details page (right-click to locate the currently playing song in \"My List\")",
"player__play": "Play",
"player__play_toggle_mode_list": "Play in order",
"player__play_toggle_mode_list_loop": "List Loop",
@ -227,6 +228,7 @@
"setting__basic_sourcename_real": "Original",
"setting__basic_sourcename_title": "Select the name of music source",
"setting__basic_theme": "Theme",
"setting__basic_theme_auto_tip": "This is a dynamic theme, you can preset a light theme and a dark theme, and then it will automatically switch to the corresponding theme you preset according to the system's light and dark theme colors.\nNote: Right-click this theme item to open the light and dark theme setting window.",
"setting__basic_to_tray": "Do not exit the software when closing the software and minimize it to the system tray",
"setting__basic_window_size": "Window size",
"setting__basic_window_size_big": "Large",
@ -323,16 +325,29 @@
"setting__other_tray_theme_native": "White",
"setting__other_tray_theme_origin": "Primary Color",
"setting__play": "Play",
"setting__play_detail": "Play details page settings",
"setting__play_detail_align": "Lyric Alignment",
"setting__play_detail_align_center": "Centered",
"setting__play_detail_align_left": "Left",
"setting__play_detail_align_right": "Right",
"setting__play_detail_detail_lyric_progress": "Allows to adjust playback progress by lyrics",
"setting__play_detail_font_size": "Lyrics font size (you can use the keyboard + - adjust the font size on the playback details page)",
"setting__play_detail_font_size_current": "Current font size: {size}",
"setting__play_detail_font_size_reset": "Reset",
"setting__play_detail_font_zoom": "Zoom the currently playing lyrics",
"setting__play_lyric_lxlrc": "Use Karaoke-style lyrics playback (if supported)",
"setting__play_lyric_s2t": "Convert the playing and downloading lyrics to Traditional Chinese",
"setting__play_lyric_transition": "Show lyrics translation",
"setting__play_mediaDevice": "Audio output",
"setting__play_mediaDevice_remove_stop_play": "Pause the song when the current sound output device is changed",
"setting__play_mediaDevice_title": "Select a media device for audio output",
"setting__play_media_device_error_tip": "This function conflicts with the audio visualization function. You have enabled audio visualization when you started the software this time. This setting is temporarily unavailable. Please restart the software and then modify this setting.",
"setting__play_media_device_tip": "This feature conflicts with Audio Visualization, both cannot be enabled at the same time, would you like to turn Audio Visualization off and apply the selected audio output settings?",
"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__player_audio_visualization_tip": "The custom audio output device will conflict with the audio visualization function. After the audio visualization is enabled, the audio output device will be reset to the default output device. At present, this problem cannot be solved. Do you still want to enable it?",
"setting__search": "Search",
"setting__search_focus_search_box": "Automatically focus the search box on startup",
"setting__search_history": "Search history",
@ -397,6 +412,8 @@
"sync__title": "Choose how to synchronize the list with {name}",
"tag__high_quality": "HQ",
"tag__lossless": "SQ",
"theme_auto": "Auto",
"theme_auto_tip": "Right-click to open the light and dark theme settings window",
"theme_black": "Black",
"theme_blue": "Blue",
"theme_blue2": "Purple Blue",
@ -410,6 +427,10 @@
"theme_pink": "Pink",
"theme_purple": "Purple",
"theme_red": "Red",
"theme_selector_modal__dark_title": "dark theme",
"theme_selector_modal__light_title": "Bright theme",
"theme_selector_modal__title": "Follow system theme settings",
"theme_selector_modal__title_tip": "Note: You can set a light theme and a dark theme in advance, and then it will automatically switch to the corresponding theme you set in advance according to the light and dark theme colors of the system.",
"theme_yellow": "Yellow",
"user_api__allow_show_update_alert": "Allow update popup to show",
"user_api__btn_export": "Export",

View File

@ -1,6 +1,7 @@
{
"action": "操作",
"agree": "接受",
"alert_button_text": "好吧",
"audio_visualization": "音频可视化(实验性)",
"back": "返回",
"cancel_button_text": "我不",
@ -168,7 +169,7 @@
"player__music_singer": "艺术家:",
"player__next": "下一首",
"player__pause": "暂停",
"player__pic_tip": "右击在“我的列表”定位当前播放的歌曲",
"player__pic_tip": "播放详情页(右击在“我的列表”定位当前播放的歌曲",
"player__play": "播放",
"player__play_toggle_mode_list": "顺序播放",
"player__play_toggle_mode_list_loop": "列表循环",
@ -227,6 +228,7 @@
"setting__basic_sourcename_real": "原名",
"setting__basic_sourcename_title": "选择音源名字类型",
"setting__basic_theme": "主题颜色",
"setting__basic_theme_auto_tip": "此乃动态主题,你可以预先设置一个亮色主题及暗色主题,此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。\n注鼠标 右击 此主题项即可打开亮、暗色主题设置窗口。",
"setting__basic_to_tray": "关闭软件时不退出软件将其最小化到系统托盘",
"setting__basic_window_size": "窗口尺寸",
"setting__basic_window_size_big": "大",
@ -323,16 +325,29 @@
"setting__other_tray_theme_native": "白色",
"setting__other_tray_theme_origin": "原色",
"setting__play": "播放设置",
"setting__play_detail": "播放详情页设置",
"setting__play_detail_align": "歌词对齐方式",
"setting__play_detail_align_center": "居中",
"setting__play_detail_align_left": "居左",
"setting__play_detail_align_right": "居右",
"setting__play_detail_detail_lyric_progress": "允许通过歌词调整播放进度",
"setting__play_detail_font_size": "歌词字体大小(可以在播放详情页使用键盘的 + - 调整字体大小)",
"setting__play_detail_font_size_current": "当前字体大小:{size}",
"setting__play_detail_font_size_reset": "重置",
"setting__play_detail_font_zoom": "缩放当前正在播放的歌词",
"setting__play_lyric_lxlrc": "使用卡拉OK式歌词播放如果支持",
"setting__play_lyric_s2t": "将播放与下载的歌词转换为繁体中文",
"setting__play_lyric_transition": "显示歌词翻译",
"setting__play_mediaDevice": "音频输出",
"setting__play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲",
"setting__play_mediaDevice_title": "选择声音输出的媒体设备",
"setting__play_media_device_error_tip": "此功能与音频可视化功能冲突,你本次启动软件时已启用过音频可视化,此设置暂不可用,请 重启 软件后,再来修改此设置。",
"setting__play_media_device_tip": "此功能与音频可视化功能冲突,两者无法同时启用,是否将音频可视化关闭 并 应用所选音频输出设置?",
"setting__play_quality": "优先播放320K品质的歌曲如果支持",
"setting__play_save_play_time": "记住播放进度",
"setting__play_task_bar": "在任务栏上显示当前歌曲播放进度",
"setting__play_timeout": "定时暂停",
"setting__player_audio_visualization_tip": "自定义音频输出设备与音频可视化功能会冲突,启用了音频可视化后音频输出设备将会被重置为默认的输出设备,目前此问题暂无法解决,是否仍要开启?",
"setting__search": "搜索设置",
"setting__search_focus_search_box": "启动时自动聚焦搜索框",
"setting__search_history": "显示历史搜索记录",
@ -397,6 +412,8 @@
"sync__title": "选择与 {name} 的列表同步方式",
"tag__high_quality": "HQ",
"tag__lossless": "SQ",
"theme_auto": "道法自然",
"theme_auto_tip": "鼠标 右击 可打开亮、暗主题设置窗口",
"theme_black": "黑灯瞎火",
"theme_blue": "蓝田生玉",
"theme_blue2": "清热版蓝",
@ -410,6 +427,10 @@
"theme_pink": "粉装玉琢",
"theme_purple": "重斤球紫",
"theme_red": "热情似火",
"theme_selector_modal__dark_title": "暗色主题",
"theme_selector_modal__light_title": "亮色主题",
"theme_selector_modal__title": "跟随系统主题设置",
"theme_selector_modal__title_tip": "注:你可以预先设置一个亮色主题及暗色主题,此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。",
"theme_yellow": "信口雌黄",
"user_api__allow_show_update_alert": "允许显示更新弹窗",
"user_api__btn_export": "导出",

View File

@ -1,6 +1,7 @@
{
"action": "操作",
"agree": "接受",
"alert_button_text": "好吧",
"audio_visualization": "音頻可視化(實驗性)",
"back": "返回",
"cancel_button_text": "取消",
@ -168,7 +169,7 @@
"player__music_singer": "藝術家:",
"player__next": "下一首",
"player__pause": "暫停",
"player__pic_tip": "右擊在“我的列表”定位當前播放的歌曲",
"player__pic_tip": "播放詳情頁(右擊在“我的列表”定位當前播放的歌曲",
"player__play": "播放",
"player__play_toggle_mode_list": "順序播放",
"player__play_toggle_mode_list_loop": "列表循環",
@ -227,6 +228,7 @@
"setting__basic_sourcename_real": "原名",
"setting__basic_sourcename_title": "選擇音源名字類型",
"setting__basic_theme": "主題顏色",
"setting__basic_theme_auto_tip": "此乃動態主題,你可以預先設置一個亮色主題及暗色主題,此後將根據系統的亮、暗主題色自動切換為你預先設置的相應主題。\n注鼠標 右擊 此主題項即可打開亮、暗色主題設置窗口。",
"setting__basic_to_tray": "關閉軟件時不退出軟件將其最小化到系統托盤",
"setting__basic_window_size": "窗口尺寸",
"setting__basic_window_size_big": "大",
@ -323,16 +325,29 @@
"setting__other_tray_theme_native": "白色",
"setting__other_tray_theme_origin": "原色",
"setting__play": "播放設置",
"setting__play_detail": "播放詳情頁設置",
"setting__play_detail_align": "歌詞對齊方式",
"setting__play_detail_align_center": "居中",
"setting__play_detail_align_left": "居左",
"setting__play_detail_align_right": "居右",
"setting__play_detail_detail_lyric_progress": "允許通過歌詞調整播放進度",
"setting__play_detail_font_size": "歌詞字體大小(可以在播放詳情頁使用鍵盤的 + - 調整字體大小)",
"setting__play_detail_font_size_current": "當前字體大小:{size}",
"setting__play_detail_font_size_reset": "重置",
"setting__play_detail_font_zoom": "縮放當前正在播放的歌詞",
"setting__play_lyric_lxlrc": "使用卡拉OK式歌詞播放如果支持",
"setting__play_lyric_s2t": "將播放與下載的歌詞轉換為繁體中文",
"setting__play_lyric_transition": "顯示歌詞翻譯",
"setting__play_mediaDevice": "音頻輸出",
"setting__play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲",
"setting__play_mediaDevice_title": "選擇聲音輸出的媒體設備",
"setting__play_media_device_error_tip": "此功能與音頻可視化功能衝突,你本次啟動軟件時已啟用過音頻可視化,此設置暫不可用,請 重啟 軟件後,再來修改此設置。",
"setting__play_media_device_tip": "此功能與音頻可視化功能衝突,兩者無法同時啟用,是否將音頻可視化關閉 並 應用所選音頻輸出設置?",
"setting__play_quality": "優先播放320K品質的歌曲如果支持",
"setting__play_save_play_time": "記住播放進度",
"setting__play_task_bar": "在任務欄上顯示當前歌曲播放進度",
"setting__play_timeout": "定時暫停",
"setting__player_audio_visualization_tip": "自定義音頻輸出設備與音頻可視化功能會衝突,啟用了音頻可視化後音頻輸出設備將會被重置為默認的輸出設備,目前此問題暫無法解決,是否仍要開啟?",
"setting__search": "搜索設置",
"setting__search_focus_search_box": "啟動時自動聚焦搜索框",
"setting__search_history": "顯示歷史搜索記錄",
@ -397,6 +412,8 @@
"sync__title": "選擇與 {name} 的列表同步方式",
"tag__high_quality": "HQ",
"tag__lossless": "SQ",
"theme_auto": "道法自然",
"theme_auto_tip": "鼠標 右擊 可打開亮、暗主題設置窗口",
"theme_black": "黑燈瞎火",
"theme_blue": "藍田生玉",
"theme_blue2": "清熱版藍",
@ -410,6 +427,10 @@
"theme_pink": "粉裝玉琢",
"theme_purple": "重斤球紫",
"theme_red": "熱情似火",
"theme_selector_modal__dark_title": "暗色主題",
"theme_selector_modal__light_title": "亮色主題",
"theme_selector_modal__title": "跟隨系統主題設置",
"theme_selector_modal__title_tip": "注:你可以預先設置一個亮色主題及暗色主題,此後將根據系統的亮、暗主題色自動切換為你預先設置的相應主題。",
"theme_yellow": "信口雌黃",
"user_api__allow_show_update_alert": "允許顯示更新彈窗",
"user_api__btn_export": "導出",

View File

@ -26,6 +26,14 @@ class MainWindow extends EventEmitter {
this.emit(MAIN_WINDOW_EVENT_NAME.show)
}
focus() {
this.emit(MAIN_WINDOW_EVENT_NAME.focus)
}
blur() {
this.emit(MAIN_WINDOW_EVENT_NAME.blur)
}
hide() {
this.emit(MAIN_WINDOW_EVENT_NAME.hide)
}

View File

@ -14,6 +14,8 @@ exports.mainWindow = {
ready_to_show: 'ready_to_show',
show: 'show',
hide: 'hide',
focus: 'focus',
blur: 'blur',
}
exports.tray = {

View File

@ -6,7 +6,7 @@
*/
const electron = require('electron')
const electronDebug = require('electron-debug')
const { default: installExtension, VUEJS3_DEVTOOLS } = require('electron-devtools-installer')
const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer')
// Install `electron-debug` with `devtron`
electronDebug({
showDevTools: true,
@ -15,7 +15,7 @@ electronDebug({
// Install `vue-devtools`
electron.app.on('ready', () => {
installExtension(VUEJS3_DEVTOOLS)
installExtension(VUEJS_DEVTOOLS)
.then(name => console.log(`Added Extension: ${name}`))
.catch(err => console.log('An error occurred: ', err))
})

View File

@ -1,4 +1,4 @@
const { app, BrowserWindow, shell } = require('electron')
const { app, BrowserWindow, shell, nativeTheme } = require('electron')
const path = require('path')
const urlSchemeRxp = /^lxmusic:\/\//
@ -170,7 +170,7 @@ function createWindow() {
// icon: path.join(global.__static, isWin ? 'icons/256x256.ico' : 'icons/512x512.png'),
resizable: false,
maximizable: false,
fullscreenable: false,
fullscreenable: true,
show: false,
webPreferences: {
contextIsolation: false,
@ -180,7 +180,14 @@ function createWindow() {
},
})
global.modules.mainWindow.loadURL(winURL + `?dt=${!!global.envParams.cmdParams.dt}&theme=${themes.find(t => t.id == global.appSetting.themeId)?.className ?? themes[0].className}`)
const shouldUseDarkColors = nativeTheme.shouldUseDarkColors
const themeId = global.appSetting.theme.id == 'auto'
? shouldUseDarkColors
? global.appSetting.theme.darkId
: global.appSetting.theme.lightId
: global.appSetting.theme.id
const themeClass = themes.find(t => t.id == themeId)?.className ?? themes[0].className
global.modules.mainWindow.loadURL(winURL + `?dt=${!!global.envParams.cmdParams.dt}&dark=${shouldUseDarkColors}&theme=${themeClass}`)
winEvent(global.modules.mainWindow)
// global.modules.mainWindow.webContents.openDevTools()

View File

@ -1,5 +1,5 @@
const { app, Tray, Menu, nativeImage } = require('electron')
// const { isWin } = require('../../common/utils')
const { isWin } = require('@common/utils')
const { tray: TRAY_EVENT_NAME, common: COMMON_EVENT_NAME, mainWindow: MAIN_WINDOW_NAME } = require('../events/_name')
const path = require('path')
let isEnableTray = null
@ -40,6 +40,14 @@ global.lx_event.mainWindow.on(MAIN_WINDOW_NAME.ready_to_show, () => {
global.lx_event.mainWindow.on(MAIN_WINDOW_NAME.show, () => {
createMenu(global.modules.tray)
})
if (!isWin) {
global.lx_event.mainWindow.on(MAIN_WINDOW_NAME.focus, () => {
createMenu(global.modules.tray)
})
global.lx_event.mainWindow.on(MAIN_WINDOW_NAME.blur, () => {
createMenu(global.modules.tray)
})
}
global.lx_event.mainWindow.on(MAIN_WINDOW_NAME.hide, () => {
createMenu(global.modules.tray)
})
@ -75,24 +83,27 @@ const destroyTray = () => {
const createMenu = tray => {
if (!global.modules.tray) return
let menu = []
global.modules.mainWindow && menu.push(global.modules.mainWindow.isVisible()
? {
label: '隐藏主界面',
click() {
global.modules.mainWindow.hide()
},
}
: {
label: '显示主界面',
click() {
if (!global.modules.mainWindow) return
if (!global.modules.mainWindow.isVisible()) {
global.modules.mainWindow.show()
}
global.modules.mainWindow.restore()
global.modules.mainWindow.focus()
},
})
if (global.modules.mainWindow) {
const isShow = global.modules.mainWindow.isVisible() && (isWin ? true : global.modules.mainWindow.isFocused())
menu.push(isShow
? {
label: '隐藏主界面',
click() {
global.modules.mainWindow.hide()
},
}
: {
label: '显示主界面',
click() {
if (!global.modules.mainWindow) return
if (!global.modules.mainWindow.isVisible()) {
global.modules.mainWindow.show()
}
global.modules.mainWindow.restore()
global.modules.mainWindow.focus()
},
})
}
menu.push(global.appSetting.desktopLyric.enable
? {
label: '关闭桌面歌词',

View File

@ -22,10 +22,11 @@ require('./musicUrl')
require('./systemFonts')
require('./wait')
require('./openDevtools')
require('./nativeTheme')
if (isWin) require('./taskbar')
// require('./kw_decodeLyric')
require('./kw_decodeLyric')
require('./userApi')
require('./sync')

View File

@ -0,0 +1,11 @@
const { nativeTheme } = require('electron')
const { NAMES: { mainWindow: ipcMainWindowNames }, mainSend } = require('@common/ipc')
nativeTheme.addListener('updated', (event) => {
// console.log(event.sender.shouldUseDarkColors)
if (!global.modules.mainWindow) return
mainSend(global.modules.mainWindow, ipcMainWindowNames.system_theme_change, event.sender.shouldUseDarkColors)
// console.log(nativeTheme.themeSource)
})

View File

@ -1,5 +1,5 @@
const { app } = require('electron')
const { mainOn, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
const { mainOn, mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
mainOn(ipcMainWindowNames.min, event => {
if (global.modules.mainWindow) {
@ -16,3 +16,8 @@ mainOn(ipcMainWindowNames.close, (event, isForce) => {
global.isTrafficLightClose = true
if (global.modules.mainWindow) global.modules.mainWindow.close()
})
mainHandle(ipcMainWindowNames.fullscreen, async(event, isFullscreen) => {
if (!global.modules.mainWindow) return false
await global.modules.mainWindow.setFullScreen(isFullscreen)
return isFullscreen
})

View File

@ -26,6 +26,11 @@ module.exports = mainWindow => {
// })
mainWindow.on('focus', () => {
mainSend(mainWindow, ipcMainWindowNames.focus)
global.lx_event.mainWindow.focus()
})
mainWindow.on('blur', () => {
global.lx_event.mainWindow.blur()
})
mainWindow.once('ready-to-show', () => {

View File

@ -172,7 +172,6 @@ export default {
this.lyric.lines = lines
this.lyric.line = 0
},
offset: 100,
})
},
mounted() {
@ -207,6 +206,13 @@ export default {
this.isPlay = false
window.lrc.pause()
break
case 'stop':
this.isPlay = false
this.lyrics.lyric = ''
this.lyrics.tlyric = ''
this.lyrics.lxlyric = ''
this.setLyric()
break
case 'info':
// console.log('info', data)
this.lyrics.lyric = data.lrc
@ -215,7 +221,9 @@ export default {
this.setLyric()
this.$nextTick(() => {
this.lyric.line = data.line
rendererSend(NAMES.winLyric.get_lyric_info, 'status')
setTimeout(() => {
rendererSend(NAMES.winLyric.get_lyric_info, 'status')
})
})
case 'music_info':
this.musicInfo.name = data.name

View File

@ -16,24 +16,36 @@
<script>
import { useRefGetter, watch, onMounted } from '@renderer/utils/vueTools'
import useApp from '@renderer/core/useApp'
import { isFullscreen } from '@renderer/core/share'
import { getFontSizeWithScreen } from '@renderer/utils'
export default {
setup() {
const theme = useRefGetter('theme')
const font = useRefGetter('font')
const windowSizeActive = useRefGetter('windowSizeActive')
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,
})
watch(isFullscreen, val => {
if (val) {
document.body.classList.remove(window.dt ? 'disableTransparent' : 'transparent')
document.body.classList.add('fullscreen')
document.documentElement.style.fontSize = getFontSizeWithScreen(window.screen.width) + 'px'
} else {
document.body.classList.remove('fullscreen')
document.body.classList.add(window.dt ? 'disableTransparent' : 'transparent')
document.documentElement.style.fontSize = windowSizeActive.value.fontSize
}
})
useApp()
@ -85,13 +97,13 @@ body {
.transparent {
padding: @shadow-app;
#waiting-mask {
border-radius: @radius-border;
left: @shadow-app;
right: @shadow-app;
top: @shadow-app;
bottom: @shadow-app;
}
// #waiting-mask {
// border-radius: @radius-border;
// left: @shadow-app;
// right: @shadow-app;
// top: @shadow-app;
// bottom: @shadow-app;
// }
#root {
box-shadow: 0 0 @shadow-app rgba(0, 0, 0, 0.5);
border-radius: @radius-border;
@ -114,6 +126,14 @@ body {
margin-right: 5Px;
}
}
.fullscreen {
background-color: #fff;
#right {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
#container {
position: relative;

View File

@ -86,7 +86,8 @@ table {
flex-flow: row nowrap;
align-items: center;
// border-top: 1px solid rgba(0, 0, 0, 0.12);
transition: background-color 0.2s ease;
transition: 0.2s ease;
transition-property: background-color, color;
border-bottom: 1px solid @color-theme_2-line;
box-sizing: border-box;
&:hover {
@ -102,7 +103,7 @@ table {
flex: none;
padding: 0 6px;
position: relative;
transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
// transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 13px;
line-height: 16px;
vertical-align: middle;
@ -223,13 +224,13 @@ input, textarea {
border-radius: 3px;
background-color: @color-scrollbar-thumb;
// background-color: rgba(0, 0, 0, 0.2);
transition: all 0.4s ease;
transition: background-color 0.4s ease;
}
&::-webkit-scrollbar-thumb:hover {
border-radius: 3px;
background-color: @color-scrollbar-thumb-hover;
// background-color: rgba(0, 0, 0, 0.4);
transition: all 0.4s ease;
transition: background-color 0.4s ease;
}
}

View File

@ -1,6 +1,10 @@
<template>
<ul :class="$style.list" :style="listStyles" ref="dom_list">
<ul :class="$style.list" :style="listStyles" ref="dom_list" role="toolbar" :aria-hidden="!show">
<li v-for="item in menus"
role="tab"
tabindex="0"
:aria-label="item[itemName]"
ignore-tip
:key="item.action"
@click="handleClick(item)"
v-show="!item.hide && (item.action == 'download' ? setting.download.enable : true)"

View File

@ -10,7 +10,7 @@
</div>
<ul class="selection-list scroll" :class="$style.list" :style="listStyles" ref="dom_list">
<li v-for="(item, index) in list" :key="index" :class="(itemKey ? item[itemKey] : item) == modelValue ? $style.active : null"
@click="handleClick(item)" :tips="itemName ? item[itemName] : item">{{itemName ? item[itemName] : item}}</li>
@click="handleClick(item)" :aria-label="itemName ? item[itemName] : item">{{itemName ? item[itemName] : item}}</li>
</ul>
</div>
</template>

View File

@ -141,8 +141,8 @@ export default {
const scrollContainerHeight = dom_scrollContainer.value.clientHeight
const currentEndIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight)
const continuous = currentStartIndex <= endIndex && currentEndIndex >= startIndex
const currentStartRenderIndex = Math.max(Math.floor(currentScrollTop / itemHeight) - props.outsideNum, 0)
const currentEndRenderIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight) + props.outsideNum
const currentStartRenderIndex = Math.max(currentStartIndex - props.outsideNum, 0)
const currentEndRenderIndex = currentEndIndex + props.outsideNum + 1
// console.log(continuous)
// debugger
if (continuous) {
@ -161,7 +161,7 @@ export default {
// // console.log('scroll up')
// views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
// } else return
if (currentScrollTop == scrollTop) return
if (currentScrollTop == scrollTop && endIndex >= currentEndIndex) return
views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
} else {
views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
@ -215,19 +215,28 @@ export default {
return isScrolling ? scrollToValue : dom_scrollContainer.value.scrollTop
}
const handleResize = () => {
setTimeout(updateView)
}
const contentStyle = computed(() => ({
display: 'block',
height: props.list.length * props.itemHeight + 'px',
}))
watch(() => props.itemHeight, updateView)
watch(() => props.list, (list) => {
const handleReset = list => {
cachedList = Array(list.length)
startIndex = -1
endIndex = -1
nextTick(() => {
updateView()
})
}
watch(() => props.itemHeight, () => {
handleReset(props.list)
})
watch(() => props.list, (list) => {
handleReset(list)
}, {
deep: true,
})
@ -238,9 +247,11 @@ export default {
startIndex = -1
endIndex = -1
updateView()
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
dom_scrollContainer.value.removeEventListener('scroll', onScroll)
window.removeEventListener('resize', handleResize)
if (cancelScroll) cancelScroll()
})

View File

@ -26,16 +26,29 @@ const themes = {
naruto: 'rgba(87,144,167,.14)',
happy_new_year: 'rgba(192,57,43,.1)',
}
const getBarWidth = canvasWidth => {
let barWidth = (canvasWidth / 128) * 2.5
const width = canvasWidth / 86
const diffWidth = barWidth - width
// console.log(barWidth - width)
// if (barWidth - width > 20) newBarWidth = 20
// barWidth = newBarWidth
return diffWidth > 32
? canvasWidth / 128 // 4k
: diffWidth > 12 ? width : barWidth
}
export default {
setup() {
const dom_canvas = ref(null)
const analyser = getAnalyser()
let ctx
let bufferLength
let bufferLength = 0
let dataArray
let WIDTH
let HEIGHT
let MAX_HEIGHT
let barWidth
let barHeight
let x = 0
@ -89,7 +102,7 @@ export default {
// let b = 50
// ctx.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')'
barHeight = barHeight * frequencyAvg + barHeight * 0.42
barHeight = (barHeight * frequencyAvg + barHeight * 0.42) * MAX_HEIGHT
ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight)
x += barWidth
@ -101,7 +114,7 @@ export default {
analyser.fftSize = 256
bufferLength = analyser.frequencyBinCount
// console.log(bufferLength)
barWidth = (WIDTH / bufferLength) * 2.5
barWidth = getBarWidth(WIDTH)
dataArray = new Uint8Array(bufferLength)
renderFrame()
}
@ -110,14 +123,27 @@ export default {
isPlaying = false
}
const handleResize = () => {
const canvas = dom_canvas.value
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight
WIDTH = canvas.width
HEIGHT = canvas.height
MAX_HEIGHT = Math.round(HEIGHT * 0.4 / 255 * 10000) / 10000
// console.log(MAX_HEIGHT)
barWidth = getBarWidth(WIDTH)
}
window.eventHub.on(eventPlayerNames.play, handlePlay)
window.eventHub.on(eventPlayerNames.pause, handlePause)
window.eventHub.on(eventPlayerNames.error, handlePause)
window.addEventListener('resize', handleResize)
onBeforeUnmount(() => {
handlePause()
window.eventHub.off(eventPlayerNames.play, handlePlay)
window.eventHub.off(eventPlayerNames.pause, handlePause)
window.eventHub.off(eventPlayerNames.error, handlePause)
window.removeEventListener('resize', handleResize)
})
onMounted(() => {
@ -127,6 +153,8 @@ export default {
canvas.height = canvas.clientHeight
WIDTH = canvas.width
HEIGHT = canvas.height
MAX_HEIGHT = Math.round(HEIGHT * 0.4 / 255 * 10000) / 10000
// console.log(MAX_HEIGHT)
if (isPlay.value) handlePlay()
})

View File

@ -79,7 +79,7 @@ export default {
.main {
padding: 15px;
max-width: 300px;
max-width: 400px;
min-width: 200px;
display: flex;
flex-flow: column nowrap;

View File

@ -51,7 +51,7 @@ export default {
.main {
padding: 15px;
max-width: 300px;
max-width: 400px;
min-width: 200px;
display: flex;
flex-flow: column nowrap;

View File

@ -1,10 +1,10 @@
<template>
<material-modal :show="show" :bg-close="bgClose" @close="handleClose" :teleport="teleport">
<material-modal :show="show" :bg-close="bgClose" @close="handleClose" :teleport="teleport" max-width="70%">
<main :class="$style.main">
<h2>{{$t('list_add__' + (isMove ? 'title_first_move' : 'title_first_add'))}}&nbsp;<span :class="$style.name">{{this.musicInfo && `${musicInfo.name}`}}</span>&nbsp;{{$t('list_add__title_last')}}</h2>
<div class="scroll" :class="$style.btnContent">
<base-btn :class="$style.btn" :tips="$t('list_add__btn_title', { name: item.name })" :key="item.id" :disabled="item.isExist" @click="handleClick(index)" v-for="(item, index) in lists">{{item.name}}</base-btn>
<base-btn :class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" @click="handleEditing($event)" :tips="$t('lists__new_list_btn')">
<base-btn :class="$style.btn" :aria-label="$t('list_add__btn_title', { name: item.name })" :key="item.id" :disabled="item.isExist" @click="handleClick(index)" v-for="(item, index) in lists">{{item.name}}</base-btn>
<base-btn :class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" @click="handleEditing($event)" :aria-label="$t('lists__new_list_btn')">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 42 42" space="preserve">
<use xlink:href="#icon-addTo"></use>
</svg>
@ -74,15 +74,31 @@ export default {
return {
isEditing: false,
newListName: '',
rowNum: 3,
}
},
computed: {
spaceNum() {
return this.lists.length < 2 ? 0 : (3 - this.lists.length % 3 - 1)
return this.lists.length < 2 ? 0 : (this.rowNum - this.lists.length % this.rowNum - 1)
},
},
mounted() {
window.addEventListener('resize', this.handleResize)
this.handleResize()
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize)
},
methods: {
...mapMutations('list', ['listAdd', 'listMove', 'createUserList']),
handleResize() {
const width = window.innerWidth
this.rowNum = width < 1920
? 3
: width < 2560
? 4
: width < 3840 ? 5 : 6
},
handleClick(index) {
this.isMove
? this.listMove({ fromId: this.fromListId, toId: this.lists[index].id, musicInfo: this.musicInfo })
@ -117,7 +133,7 @@ export default {
.main {
// padding: 15px 0;
max-width: 620px;
// max-width: 70%;
min-width: 200px;
display: flex;
flex-flow: column nowrap;
@ -147,14 +163,17 @@ export default {
justify-content: space-evenly;
}
@item-width: (100% / 3);
.btn {
position: relative;
box-sizing: border-box;
margin-left: 15px;
margin-bottom: 15px;
height: 36px;
line-height: 36px;
padding: 0 10px !important;
width: 180px;
width: calc(@item-width - 15px);
min-width: 160px;
.mixin-ellipsis-1;
}
@ -181,6 +200,9 @@ export default {
}
}
.newListInput {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 34px;
border: none;
@ -191,9 +213,30 @@ export default {
font-size: 14px;
text-align: center;
font-family: inherit;
box-sizing: border-box;
padding: 0 10px;
display: none;
}
@item-width2: (100% / 4);
@media (min-width: 1920px){
.btn {
width: calc(@item-width2 - 15px);
}
}
@item-width3: (100% / 5);
@media (min-width: 2560px){
.btn {
width: calc(@item-width3 - 15px);
}
}
@item-width4: (100% / 6);
@media (min-width: 3840px){
.btn {
width: calc(@item-width4 - 15px);
}
}
each(@themes, {
:global(#root.@{value}) {
.main {

View File

@ -1,10 +1,10 @@
<template>
<material-modal :show="show" :bg-close="bgClose" @close="handleClose">
<material-modal :show="show" :bg-close="bgClose" @close="handleClose" max-width="70%">
<main :class="$style.main">
<h2>{{$t('list_add__multiple_' + (isMove ? 'title_move' : 'title_add'), { num: musicList.length })}}</h2>
<div class="scroll" :class="$style.btnContent">
<base-btn :class="$style.btn" :tips="$t('list_add__multiple_btn_title', { name: item.name })" :key="item.id" @click="handleClick(index)" v-for="(item, index) in lists">{{item.name}}</base-btn>
<base-btn :class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" @click="handleEditing($event)" :tips="$t('lists__new_list_btn')">
<base-btn :class="$style.btn" :aria-label="$t('list_add__multiple_btn_title', { name: item.name })" :key="item.id" @click="handleClick(index)" v-for="(item, index) in lists">{{item.name}}</base-btn>
<base-btn :class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" @click="handleEditing($event)" :aria-label="$t('lists__new_list_btn')">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 42 42" space="preserve">
<use xlink:href="#icon-addTo"></use>
</svg>
@ -74,16 +74,32 @@ export default {
return {
isEditing: false,
newListName: '',
rowNum: 3,
}
},
computed: {
spaceNum() {
return this.lists.length < 2 ? 0 : (3 - this.lists.length % 3 - 1)
return this.lists.length < 2 ? 0 : (this.rowNum - this.lists.length % this.rowNum - 1)
},
},
mounted() {
window.addEventListener('resize', this.handleResize)
this.handleResize()
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize)
},
methods: {
...mapMutations('list', ['listAddMultiple', 'listMoveMultiple', 'createUserList']),
handleResize() {
const width = window.innerWidth
this.rowNum = width < 1920
? 3
: width < 2560
? 4
: width < 3840 ? 5 : 6
},
handleClick(index) {
this.isMove
? this.listMoveMultiple({ fromId: this.fromListId, toId: this.lists[index].id, list: this.musicList })
@ -119,7 +135,7 @@ export default {
.main {
// padding: 15px 0;
max-width: 620px;
// max-width: 620px;
min-width: 200px;
display: flex;
flex-flow: column nowrap;
@ -145,14 +161,17 @@ export default {
justify-content: space-evenly;
}
@item-width: (100% / 3);
.btn {
position: relative;
box-sizing: border-box;
margin-left: 15px;
margin-bottom: 15px;
height: 36px;
line-height: 36px;
padding: 0 10px !important;
width: 180px;
width: calc(@item-width - 15px);
min-width: 160px;
.mixin-ellipsis-1;
}
@ -179,6 +198,9 @@ export default {
}
}
.newListInput {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 34px;
border: none;
@ -189,9 +211,31 @@ export default {
font-size: 14px;
text-align: center;
font-family: inherit;
box-sizing: border-box;
padding: 0 10px;
display: none;
}
@item-width2: (100% / 4);
@media (min-width: 1920px){
.btn {
width: calc(@item-width2 - 15px);
}
}
@item-width3: (100% / 5);
@media (min-width: 2560px){
.btn {
width: calc(@item-width3 - 15px);
}
}
@item-width4: (100% / 6);
@media (min-width: 3840px){
.btn {
width: calc(@item-width4 - 15px);
}
}
each(@themes, {
:global(#root.@{value}) {
.main {

View File

@ -3,7 +3,7 @@
<div :class="[$style.volume, {[$style.muted]: setting.player.isMute} ]">
<div :class="$style.volumeBar" ref="dom_volumeBar" :style="{ transform: `scaleX(${volume || 0})` }"></div>
</div>
<div :class="$style.volumeMask" @mousedown="handleVolumeMsDown" :tips="`${$t('player__volume')}${parseInt(volume * 100)}%`"></div>
<div :class="$style.volumeMask" @mousedown="handleVolumeMsDown" :aria-label="`${$t('player__volume')}${parseInt(volume * 100)}%`"></div>
</div>
</template>

View File

@ -1,11 +1,11 @@
<template>
<div :class="$style.controlBtn">
<button type="button" :class="[$style.btn, $style.close]" :tips="$t('close')" @click="close">
<div :class="$style.controlBtn" v-show="!isFullscreen">
<button type="button" :class="[$style.btn, $style.close]" :aria-label="$t('close')" @click="close">
<svg :class="$style.controlBtniIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-close"></use>
</svg>
</button>
<button type="button" :class="[$style.btn, $style.min]" :tips="$t('min')" @click="min">
<button type="button" :class="[$style.btn, $style.min]" :aria-label="$t('min')" @click="min">
<svg :class="$style.controlBtniIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-minimize"></use>
</svg>
@ -16,10 +16,12 @@
<script>
import { base as eventBaseName } from '@renderer/event/names'
// import { getRandom } from '../../utils'
import { isFullscreen } from '@renderer/core/share'
export default {
setup() {
return {
isFullscreen,
min() {
window.eventHub.emit(eventBaseName.min)
},

View File

@ -1,8 +1,8 @@
<template>
<div :class="$style.menu">
<ul :class="$style.list">
<li v-for="item in menus" :key="item.to">
<router-link :class="$style.link" :active-class="$style.active" :to="item.to" :tips="item.tips">
<ul :class="$style.list" role="toolbar">
<li v-for="item in menus" :key="item.to" :class="$style.navItem" role="presentation">
<router-link :class="$style.link" :active-class="$style.active" role="tab" :aria-selected="$route.name == item.to" :to="item.to" :aria-label="item.tips">
<div :class="$style.icon">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" :viewBox="item.iconSize" space="preserve">
<use :xlink:href="item.icon"></use>
@ -98,13 +98,26 @@ export default {
// .mixin-ellipsis-1;
// }
}
.navItem {
position: relative;
&:before {
content: '';
display: block;
width: 100%;
padding-bottom: 84%;
}
}
.link {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
// display: block;
box-sizing: border-box;
text-decoration: none;
position: relative;
padding: 18px 3px;
// padding: 18px 3px;
// margin: 5px 0;
// border-left: 5px solid transparent;
transition: @transition-theme;
@ -114,6 +127,9 @@ export default {
font-size: 11.5px;
text-align: center;
outline: none;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
// border-radius: @radius-border;

View File

@ -1,5 +1,5 @@
<template>
<div :class="$style.aside">
<div :class="[$style.aside, { [$style.fullscreen]: isFullscreen }]">
<ControlBtns v-if="setting.controlBtnPosition == 'left'" />
<div :class="$style.logo" v-else>L X</div>
<NavBar />
@ -8,6 +8,7 @@
<script>
import { mapGetters } from 'vuex'
import { isFullscreen } from '@renderer/core/share'
import ControlBtns from './ControlBtns'
import NavBar from './NavBar'
@ -15,6 +16,11 @@ import NavBar from './NavBar'
export default {
name: 'CoreAside',
components: { ControlBtns, NavBar },
setup() {
return {
isFullscreen,
}
},
computed: {
...mapGetters(['setting']),
},
@ -36,6 +42,13 @@ export default {
-webkit-user-select: none;
display: flex;
flex-flow: column nowrap;
&.fullscreen {
-webkit-app-region: no-drag;
.logo {
display: none;
}
}
}
.logo {

View File

@ -1,5 +1,5 @@
<template lang="pug">
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' style="display: none;")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' style="display: none;" aria-hidden="true")
defs
g#icon-search(fill='currentColor')
// 30.239 30.239

View File

@ -1,7 +1,7 @@
<template>
<div :class="$style.controlBtn">
<common-volume-bar :setting="setting" />
<div :class="$style.titleBtn" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric" :tips="toggleDesktopLyricBtnTitle">
<div :class="$style.titleBtn" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric" :aria-label="toggleDesktopLyricBtnTitle">
<svg v-show="setting.desktopLyric.enable" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 512 512" space="preserve">
<use xlink:href="#icon-desktop-lyric-on"></use>
</svg>
@ -9,7 +9,7 @@
<use xlink:href="#icon-desktop-lyric-off"></use>
</svg>
</div>
<div :class="$style.titleBtn" @click="toggleNextPlayMode" :tips="nextTogglePlayName">
<div :class="$style.titleBtn" @click="toggleNextPlayMode" :aria-label="nextTogglePlayName">
<svg v-show="setting.player.togglePlayMethod == 'listLoop'" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="80%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-list-loop"></use>
</svg>
@ -26,7 +26,7 @@
<use xlink:href="#icon-single"></use>
</svg>
</div>
<div :class="$style.titleBtn" @click="addMusicTo" :tips="$t('player__add_music_to')">
<div :class="$style.titleBtn" @click="addMusicTo" :aria-label="$t('player__add_music_to')">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="80%" viewBox="0 0 512 512" space="preserve">
<use xlink:href="#icon-add-2"></use>
</svg>

View File

@ -1,13 +1,13 @@
<template lang="pug">
div(:class="$style.player")
div(:class="$style.left" @contextmenu="handleToMusicLocation" @click="showPlayerDetail" :tips="$t('player__pic_tip')")
div(:class="$style.left" @contextmenu="handleToMusicLocation" @click="showPlayerDetail" :aria-label="$t('player__pic_tip')")
img(v-if="musicInfo.img" :src="musicInfo.img" @error="imgError")
svg(v-else version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='102%' width='100%' viewBox='0 0 60 60' space='preserve')
use(:xlink:href='`#${$style.iconPic}`')
div(:class="$style.middle")
div(:class="$style.column1")
div(:class="$style.container")
div(:class="$style.title" @click="handleCopy(title)" :tips="title + $t('copy_tip')") {{title}}
div(:class="$style.title" @click="handleCopy(title)" :aria-label="title + $t('copy_tip')") {{title}}
control-btns
div(:class="$style.column2")
common-progress-bar(:progress="progress" :handleTransitionEnd="handleTransitionEnd" :isActiveTransition="isActiveTransition" v-if="!isShowPlayerDetail")
@ -18,15 +18,15 @@ div(:class="$style.player")
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);")
div(:class="$style.playBtn" @click='playPrev' :aria-label="$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')
use(xlink:href='#icon-nextMusic')
div(:class="$style.playBtn" :tips="isPlay ? $t('player__pause') : $t('player__play')" @click='togglePlay')
div(:class="$style.playBtn" :aria-label="isPlay ? $t('player__pause') : $t('player__play')" @click='togglePlay')
svg(v-if="isPlay" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 277.338 277.338' space='preserve')
use(xlink:href='#icon-pause')
svg(v-else version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 170 170' space='preserve')
use(xlink:href='#icon-play')
div(:class="$style.playBtn" @click='playNext' :tips="$t('player__next')")
div(:class="$style.playBtn" @click='playNext' :aria-label="$t('player__next')")
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')
use(xlink:href='#icon-nextMusic')
@ -276,6 +276,7 @@ export default {
.column2 {
flex: none;
padding: 3px 0;
height: 10px;
position: relative;
}

View File

@ -1,10 +1,21 @@
<template>
<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="['right', $style.right]" :style="lrcFontSize">
<div :class="['lyric', $style.lyric, { [$style.draging]: isMsDown }, { [$style.lrcActiveZoom]: isZoomActiveLrc }]" :style="lrcStyles" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric">
<div :class="['pre', $style.lyricSpace]"></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.skip" v-if="isShowLyricProgressSetting" v-show="isStopScroll">
<div :class="$style.line" ref="dom_skip_line"></div>
<span :class="$style.label">{{timeStr}}</span>
<base-btn :class="$style.skipBtn" @mouseenter="handleSkipMouseEnter" @mouseleave="handleSkipMouseLeave" @click="handleSkipPlay">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="50%" viewBox="0 0 170 170" space="preserve">
<use xlink:href="#icon-play"></use>
</svg>
</base-btn>
</div>
</transition>
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<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 }]">
@ -20,27 +31,84 @@
<script>
import { clipboardWriteText } from '@renderer/utils'
import { lyric } from '@renderer/core/share/lyric'
import { isPlay, isShowLrcSelectContent } from '@renderer/core/share/player'
// import { ref } from '@renderer/utils/vueTools'
import { isFullscreen } from '@renderer/core/share'
import { isPlay, isShowLrcSelectContent, isShowPlayComment } from '@renderer/core/share/player'
import { onMounted, onBeforeUnmount, useCommit, useRefGetter, computed } from '@renderer/utils/vueTools'
import useLyric from '@renderer/utils/compositions/useLyric'
export default {
setup() {
const setting = useRefGetter('setting')
const setPlayDetailLyricFont = useCommit('setPlayDetailLyricFont')
const {
dom_lyric,
dom_lyric_text,
dom_skip_line,
isMsDown,
isStopScroll,
timeStr,
handleLyricMouseDown,
handleWheel,
handleSkipPlay,
handleSkipMouseEnter,
handleSkipMouseLeave,
} = useLyric({ isPlay, lyric })
const fontSizeUp = () => {
if (setting.value.playDetail.style.fontSize >= 200) return
setPlayDetailLyricFont(setting.value.playDetail.style.fontSize + 1)
}
const fontSizeDown = () => {
if (setting.value.playDetail.style.fontSize <= 70) return
setPlayDetailLyricFont(setting.value.playDetail.style.fontSize - 1)
}
const lrcStyles = computed(() => {
return {
textAlign: setting.value.playDetail.style.align,
}
})
const lrcFontSize = computed(() => {
let size = setting.value.playDetail.style.fontSize / 100
if (isFullscreen.value) size = size *= 1.4
return {
'--playDetail-lrc-font-size': (isShowPlayComment.value ? size * 0.82 : size) + 'rem',
}
})
const isZoomActiveLrc = computed(() => setting.value.playDetail.isZoomActiveLrc)
const isShowLyricProgressSetting = computed(() => setting.value.playDetail.isShowLyricProgressSetting)
onMounted(() => {
window.eventHub.on('key_shift++_down', fontSizeUp)
window.eventHub.on('key_numadd_down', fontSizeUp)
window.eventHub.on('key_-_down', fontSizeDown)
window.eventHub.on('key_numsub_down', fontSizeDown)
})
onBeforeUnmount(() => {
window.eventHub.off('key_shift++_down', fontSizeUp)
window.eventHub.off('key_numadd_down', fontSizeUp)
window.eventHub.off('key_-_down', fontSizeDown)
window.eventHub.off('key_numsub_down', fontSizeDown)
})
return {
dom_lyric,
dom_lyric_text,
dom_skip_line,
isMsDown,
timeStr,
handleLyricMouseDown,
handleWheel,
handleSkipPlay,
handleSkipMouseEnter,
handleSkipMouseLeave,
lyric,
lrcStyles,
lrcFontSize,
isShowLrcSelectContent,
isShowLyricProgressSetting,
isZoomActiveLrc,
isStopScroll,
}
},
methods: {
@ -90,7 +158,7 @@ export default {
text-align: center;
height: 100%;
overflow: hidden;
font-size: 16px;
font-size: var(--playDetail-lrc-font-size, 16px);
cursor: grab;
&.draging {
cursor: grabbing;
@ -98,9 +166,11 @@ export default {
:global {
.lrc-content {
line-height: 1.2;
margin: 16px 0;
padding: calc(var(--playDetail-lrc-font-size, 16px) / 2) 0;
overflow-wrap: break-word;
color: @color-player-detail-lyric;
transition: @transition-theme;
transition-property: padding;
.translation {
transition: @transition-theme !important;
@ -119,13 +189,12 @@ export default {
color: @color-theme;
}
.translation {
font-size: .94em;
color: @color-theme;
}
span {
// color: @color-theme;
font-size: 1.1em;
}
// span {
// // color: @color-theme;
// font-size: 1.1em;
// }
}
span {
@ -153,6 +222,64 @@ export default {
// font-size: 1.2em;
// }
}
.lrcActiveZoom {
:global {
.lrc-content {
&.active {
.translation {
font-size: .94em;
}
span {
font-size: 1.1em;
}
}
}
}
}
.skip {
position: absolute;
top: calc(38% + var(--playDetail-lrc-font-size, 16px) + 4px);
left: 0;
// height: 6px;
width: 100%;
pointer-events: none;
// opacity: .5;
.line {
border-top: 1px dashed @color-player-detail-lyric-active;
opacity: .15;
margin-right: 30px;
}
.label {
position: absolute;
right: 30px;
top: -14px;
line-height: 1;
font-size: 12px;
color: @color-player-detail-lyric-active;
opacity: .7;
}
.skipBtn {
position: absolute;
right: 0;
top: 0;
transform: translateY(-50%);
width: 30px;
height: 30px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: none !important;
pointer-events: initial;
transition: @transition-theme;
transition-property: opacity;
opacity: .8;
&:hover {
opacity: .6;
}
}
}
.lyricSelectContent {
position: absolute;
left: 0;
@ -217,6 +344,14 @@ each(@themes, {
// .lrc-active {
// color: ~'@{color-@{value}-theme}';
// }
.skip {
.line {
border-top-color: ~'@{color-@{value}-player-detail-lyric-active}';
}
.label {
color:~'@{color-@{value}-player-detail-lyric-active}';
}
}
.lyricSelectContent {
background-color: ~'@{color-@{value}-theme_2-background_1}';
color: ~'@{color-@{value}-player-detail-lyric}';

View File

@ -11,15 +11,15 @@ div(:class="$style.footer")
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')")
div(:class="$style.playBtn" @click="playPrev" style="transform: rotate(180deg);" :aria-label="$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')
use(xlink:href='#icon-nextMusic')
div(:class="$style.playBtn" @click="togglePlay" :tips="isPlay ? $t('player__pause') : $t('player__play')")
div(:class="$style.playBtn" @click="togglePlay" :aria-label="isPlay ? $t('player__pause') : $t('player__play')")
svg(v-if="isPlay" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 277.338 277.338' space='preserve')
use(xlink:href='#icon-pause')
svg(v-else version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 170 170' space='preserve')
use(xlink:href='#icon-play')
div(:class="$style.playBtn" @click="playNext" :tips="$t('player__next')")
div(:class="$style.playBtn" @click="playNext" :aria-label="$t('player__next')")
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')
use(xlink:href='#icon-nextMusic')
</template>

View File

@ -1,21 +1,21 @@
<template lang="pug">
div(:class="$style.footerLeftControlBtns")
common-volume-bar(:setting="setting")
div(:class="[$style.footerLeftControlBtn, $style.lrcBtn]" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric" :tips="toggleDesktopLyricBtnTitle")
div(:class="[$style.footerLeftControlBtn, $style.lrcBtn]" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric" :aria-label="toggleDesktopLyricBtnTitle")
svg(v-show="setting.desktopLyric.enable" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='125%' viewBox='0 0 512 512' space='preserve')
use(xlink:href='#icon-desktop-lyric-on')
svg(v-show="!setting.desktopLyric.enable" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='125%' viewBox='0 0 512 512' space='preserve')
use(xlink:href='#icon-desktop-lyric-off')
div(:class="[$style.footerLeftControlBtn, { [$style.active]: setting.player.audioVisualization }]" @click="toggleAudioVisualization" :tips="$t('audio_visualization')")
div(:class="[$style.footerLeftControlBtn, { [$style.active]: setting.player.audioVisualization }]" @click="toggleAudioVisualization" :aria-label="$t('audio_visualization')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-audio-wave')
div(:class="[$style.footerLeftControlBtn, { [$style.active]: isShowLrcSelectContent }]" @click="toggleVisibleLrc" :tips="$t('lyric__select')")
div(:class="[$style.footerLeftControlBtn, { [$style.active]: isShowLrcSelectContent }]" @click="toggleVisibleLrc" :aria-label="$t('lyric__select')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-text')
div(:class="[$style.footerLeftControlBtn, {[$style.active]: isShowPlayComment}]" @click="toggleVisibleComment" :tips="$t('comment__show')")
div(:class="[$style.footerLeftControlBtn, {[$style.active]: isShowPlayComment}]" @click="toggleVisibleComment" :aria-label="$t('comment__show')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-comment')
div(:class="$style.footerLeftControlBtn" @click="toggleNextPlayMode" :tips="nextTogglePlayName")
div(:class="$style.footerLeftControlBtn" @click="toggleNextPlayMode" :aria-label="nextTogglePlayName")
svg(v-show="setting.player.togglePlayMethod == 'listLoop'" 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-list-loop')
svg(v-show="setting.player.togglePlayMethod == 'random'" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 24 24' space='preserve')
@ -26,7 +26,7 @@ div(:class="$style.footerLeftControlBtns")
use(xlink:href='#icon-single-loop')
svg(v-show="!setting.player.togglePlayMethod" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='120%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-single')
div(:class="$style.footerLeftControlBtn" @click="isShowAddMusicTo = true" :tips="$t('player__add_music_to')")
div(:class="$style.footerLeftControlBtn" @click="isShowAddMusicTo = true" :aria-label="$t('player__add_music_to')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' space='preserve')
use(xlink:href='#icon-add-2')
common-list-add-modal(v-model:show="isShowAddMusicTo" :musicInfo="musicInfoItem")
@ -46,12 +46,15 @@ import {
import useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'
import useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'
import { dialog } from '@renderer/plugins/Dialog'
import { setMediaDeviceId } from '@renderer/plugins/player'
export default {
setup() {
const { t } = useI18n()
const setting = useRefGetter('setting')
const setAudioVisualization = useCommit('setAudioVisualization')
const saveMediaDeviceId = useCommit('setMediaDeviceId')
const toggleVisibleLrc = () => {
setShowPlayLrcSelectContentLrc(!isShowLrcSelectContent.value)
@ -72,8 +75,19 @@ export default {
const isShowAddMusicTo = ref(false)
const toggleAudioVisualization = () => {
setAudioVisualization(!setting.value.player.audioVisualization)
const toggleAudioVisualization = async() => {
const newSetting = !setting.value.player.audioVisualization
if (newSetting && setting.value.player.mediaDeviceId != 'default') {
const confirm = await dialog.confirm({
message: t('setting__player_audio_visualization_tip'),
cancelButtonText: t('cancel_button_text'),
confirmButtonText: t('confirm_button_text'),
})
if (!confirm) return
saveMediaDeviceId('default')
await setMediaDeviceId('default').catch(_ => _)
}
setAudioVisualization(newSetting)
}
return {

View File

@ -3,7 +3,7 @@ div.comment(:class="$style.comment" ref="dom_container")
div(:class="$style.commentHeader")
h3 {{$t('comment__title', { name: title })}}
div(:class="$style.commentHeaderBtns")
div(:class="$style.commentHeaderBtn" @click="handleShowComment" :tips="$t('comment__refresh')")
div(:class="$style.commentHeaderBtn" @click="handleShowComment" :aria-label="$t('comment__refresh')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' style='transform: rotate(45deg);' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-refresh')
div(:class="$style.commentHeaderBtn" @click="$emit('close')")
@ -132,7 +132,13 @@ export default {
},
methods: {
setWidth() {
this.$refs.dom_container.style.width = this.$refs.dom_container.clientWidth + 'px'
setTimeout(() => {
this.$refs.dom_container.style.width = Math.floor(this.$refs.dom_container.parentNode.clientWidth * 0.5) + 'px'
setTimeout(() => {
this.handleToggleTab(this.tabActiveId, true)
})
})
},
async getComment(musicInfo, page, limit, retryNum = 0) {
let resp
@ -219,8 +225,8 @@ export default {
this.newComment.nextPage = page
this.handleGetNewComment(this.currentMusicInfo, page, this.newComment.limit)
},
handleToggleTab(id) {
if (this.tabActiveId == id) return
handleToggleTab(id, force) {
if (!force && this.tabActiveId == id) return
switch (id) {
case 'hot':
this.$refs.dom_tabMain.scrollLeft = 0

View File

@ -1,32 +1,32 @@
<template lang="pug">
transition(enter-active-class="animated lightSpeedIn" leave-active-class="animated slideOutDown" @after-enter="handleAfterEnter" @after-leave="handleAfterLeave")
div(:class="$style.container" @contextmenu="handleContextMenu" v-if="isShowPlayerDetail")
div(:class="[$style.container, , { [$style.fullscreen]: isFullscreen }]" @contextmenu="handleContextMenu" v-if="isShowPlayerDetail")
//- div(:class="$style.bg" :style="bgStyle")
//- div(:class="$style.bg2")
div(:class="[$style.header, $style.controlBtnLeft]" v-if="setting.controlBtnPosition == 'left'")
div(:class="$style.controBtn")
button(type="button" :class="$style.hide" :tips="$t('player__hide_detail_tip')" @click="hide")
button(type="button" :class="$style.hide" :aria-label="$t('player__hide_detail_tip')" @click="hide")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='80%' viewBox='0 0 30.727 30.727' space='preserve')
use(xlink:href='#icon-window-hide')
button(type="button" :class="$style.min" :tips="$t('min')" @click="min")
button(type="button" :class="$style.min" :aria-label="$t('min')" @click="min")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='100%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-window-minimize')
//- button(type="button" :class="$style.max" @click="max")
button(type="button" :class="$style.close" :tips="$t('close')" @click="close")
button(type="button" :class="$style.close" :aria-label="$t('close')" @click="close")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='100%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-window-close')
div(:class="[$style.header, $style.controlBtnRight]" v-else)
div(:class="$style.controBtn")
button(type="button" :class="$style.hide" :tips="$t('player__hide_detail_tip')" @click="hide")
button(type="button" :class="$style.hide" :aria-label="$t('player__hide_detail_tip')" @click="hide")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='35%' viewBox='0 0 30.727 30.727' space='preserve')
use(xlink:href='#icon-window-hide')
button(type="button" :class="$style.min" :tips="$t('min')" @click="min")
button(type="button" :class="$style.min" :aria-label="$t('min')" @click="min")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='60%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-window-minimize-2')
//- button(type="button" :class="$style.max" @click="max")
button(type="button" :class="$style.close" :tips="$t('close')" @click="close")
button(type="button" :class="$style.close" :aria-label="$t('close')" @click="close")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='60%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-window-close-2')
@ -52,6 +52,7 @@ transition(enter-active-class="animated lightSpeedIn" leave-active-class="animat
<script>
import { useRefGetter, ref } from '@renderer/utils/vueTools'
import { isFullscreen } from '@renderer/core/share'
import { base as eventBaseName } from '@renderer/event/names'
import {
isShowPlayerDetail,
@ -117,6 +118,7 @@ export default {
handleAfterEnter,
handleAfterLeave,
visibled,
isFullscreen,
min() {
window.eventHub.emit(eventBaseName.min)
},
@ -161,6 +163,15 @@ export default {
box-sizing: border-box;
}
&.fullscreen {
.header {
-webkit-app-region: no-drag;
> * {
display: none;
}
}
}
}
// .bg {
// position: absolute;
@ -296,9 +307,6 @@ export default {
}
.right {
flex-basis: 30%;
.lyric {
font-size: 13px;
}
.lyricSelectContent {
font-size: 14px;
}

View File

@ -1,11 +1,11 @@
<template>
<div :class="$style.control">
<button type="button" :class="[$style.btn, $style.min]" :tips="$t('min')" @click="min">
<div :class="$style.control" v-show="!isFullscreen">
<button type="button" :class="[$style.btn, $style.min]" :aria-label="$t('min')" @click="min">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="60%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-minimize-2"></use>
</svg>
</button>
<button type="button" :class="[$style.btn, $style.close]" :tips="$t('close')" @click="close">
<button type="button" :class="[$style.btn, $style.close]" :aria-label="$t('close')" @click="close">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="60%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-close-2"></use>
</svg>
@ -15,6 +15,7 @@
<script>
import { base as eventBaseName } from '@renderer/event/names'
import { isFullscreen } from '@renderer/core/share'
// import { getRandom } from '../../utils'
export default {
@ -29,6 +30,7 @@ export default {
close() {
window.eventHub.emit(eventBaseName.close)
},
isFullscreen,
}
},
}

View File

@ -1,5 +1,5 @@
<template>
<div :class="[$style.toolbar, setting.controlBtnPosition == 'left' ? $style.controlBtnLeft : $style.controlBtnRight]">
<div :class="[$style.toolbar, { [$style.fullscreen]: isFullscreen }, setting.controlBtnPosition == 'left' ? $style.controlBtnLeft : $style.controlBtnRight]">
<SearchInput />
<div :class="$style.logo" v-if="setting.controlBtnPosition == 'left'">L X</div>
<ControlBtns v-else />
@ -11,10 +11,16 @@ import { mapGetters } from 'vuex'
import ControlBtns from './ControlBtns'
import SearchInput from './SearchInput'
import { isFullscreen } from '@renderer/core/share'
export default {
name: 'CoreToolBar',
components: { SearchInput, ControlBtns },
setup() {
return {
isFullscreen,
}
},
computed: {
...mapGetters(['setting']),
},
@ -34,6 +40,13 @@ export default {
-webkit-app-region: drag;
z-index: 2;
&.fullscreen {
-webkit-app-region: no-drag;
.logo {
display: none;
}
}
&.controlBtnLeft {
.control {
display: none;

View File

@ -44,11 +44,11 @@ material-modal(:show="versionInfo.showModal" @close="handleClose")
p 发现有新版本啦但是自动更新功能出问题了
p
| 你可以去&nbsp;
strong.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" tips="点击打开") 软件发布页
strong.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
| &nbsp;&nbsp;
strong.hover.underline(@click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" tips="点击打开") 网盘
strong.hover.underline(@click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
| (密码
strong.hover(@click="handleCopy('glqw')" tips="点击复制") glqw
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
| )&nbsp;下载新版本
p
| 国内Windows/MAC用户推荐到
@ -61,11 +61,11 @@ material-modal(:show="versionInfo.showModal" @close="handleClose")
p 你当前所在网络访问GitHub较慢导致新版本下载超时已经下了半个钟了😳你仍可选择继续等但墙裂建议手动更新版本
p
| 你可以去
base-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" tips="点击打开") 软件发布页
base-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
|
base-btn(min @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" tips="点击打开") 网盘
base-btn(min @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
| (密码
strong.hover(@click="handleCopy('glqw')" tips="点击复制") glqw
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
| )下载新版本
p
| 国内Windows/MAC用户推荐到
@ -82,11 +82,11 @@ material-modal(:show="versionInfo.showModal" @close="handleClose")
p 更新信息获取失败可能是无法访问Github导致的请手动检查更新
p
| 检查方法打开
base-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" tips="点击打开") 软件发布页
base-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
|
base-btn(min @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" tips="点击打开") 网盘
base-btn(min @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
| (密码
strong.hover(@click="handleCopy('glqw')" tips="点击复制") glqw
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
| )查看它们的
strong 版本号
| 与当前版本({{versionInfo.version}})对比是否一样
@ -115,11 +115,11 @@ material-modal(:show="versionInfo.showModal" @close="handleClose")
| 重新打开本弹窗
p
| 手动更新可以去&nbsp;
strong.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" tips="点击打开") 软件发布页
strong.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
| &nbsp;&nbsp;
strong.hover.underline(@click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" tips="点击打开") 网盘
strong.hover.underline(@click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
| (密码
strong.hover(@click="handleCopy('glqw')" tips="点击复制") glqw
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
| )&nbsp;下载
p 国内Windows/MAC用户推荐到网盘下载
p 当前下载进度{{progress}}

View File

@ -1,30 +1,30 @@
<template lang="pug">
div(:class="$style.btns")
button(type="button" v-if="playBtn" @contextmenu.capture.stop :tips="$t('list__play')" @click.stop="handleClick('play')")
button(type="button" v-if="playBtn" @contextmenu.capture.stop :aria-label="$t('list__play')" @click.stop="handleClick('play')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 287.386 287.386' space='preserve' v-once)
use(xlink:href='#icon-testPlay')
button(type="button" v-if="listAddBtn" @contextmenu.capture.stop :tips="$t('list__add_to')" @click.stop="handleClick('listAdd')")
button(type="button" v-if="listAddBtn" @contextmenu.capture.stop :aria-label="$t('list__add_to')" @click.stop="handleClick('listAdd')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 42 42' space='preserve' v-once)
use(xlink:href='#icon-addTo')
button(type="button" v-if="downloadBtn && setting.download.enable" @contextmenu.capture.stop :tips="$t('list__download')" @click.stop="handleClick('download')")
button(type="button" v-if="downloadBtn && setting.download.enable" @contextmenu.capture.stop :aria-label="$t('list__download')" @click.stop="handleClick('download')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 475.078 475.077' space='preserve' v-once)
use(xlink:href='#icon-download')
//- button(type="button" :tips="$t('list__add')" v-if="userInfo" @click.stop="handleClick('add')")
//- button(type="button" :aria-label="$t('list__add')" v-if="userInfo" @click.stop="handleClick('add')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 42 42' space='preserve')
use(xlink:href='#icon-addTo')
button(type="button" v-if="startBtn" @contextmenu.capture.stop :tips="$t('list__start')" @click.stop="handleClick('start')")
button(type="button" v-if="startBtn" @contextmenu.capture.stop :aria-label="$t('list__start')" @click.stop="handleClick('start')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 170 170' space='preserve' v-once)
use(xlink:href='#icon-play')
button(type="button" v-if="pauseBtn" @contextmenu.capture.stop :tips="$t('list__pause')" @click.stop="handleClick('pause')")
button(type="button" v-if="pauseBtn" @contextmenu.capture.stop :aria-label="$t('list__pause')" @click.stop="handleClick('pause')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 277.338 277.338' space='preserve' v-once)
use(xlink:href='#icon-pause')
button(type="button" v-if="fileBtn" @contextmenu.capture.stop :tips="$t('list__file')" @click.stop="handleClick('file')")
button(type="button" v-if="fileBtn" @contextmenu.capture.stop :aria-label="$t('list__file')" @click.stop="handleClick('file')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='-61 0 512 512' space='preserve' v-once)
use(xlink:href='#icon-musicFile')
button(type="button" v-if="searchBtn" @contextmenu.capture.stop :tips="$t('list__search')" @click.stop="handleClick('search')")
button(type="button" v-if="searchBtn" @contextmenu.capture.stop :aria-label="$t('list__search')" @click.stop="handleClick('search')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 30.239 30.239' space='preserve' v-once)
use(xlink:href='#icon-search')
button(type="button" v-if="removeBtn" :tips="$t('list__remove')" @click.stop="handleClick('remove')")
button(type="button" v-if="removeBtn" :aria-label="$t('list__remove')" @click.stop="handleClick('remove')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 212.982 212.982' space='preserve' v-once)
use(xlink:href='#icon-delete')

View File

@ -4,7 +4,7 @@ teleport(:to="teleport")
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
div(:class="$style.modal" v-show="showContent" @click="bgClose && close()")
transition(:enter-active-class="inClass" :leave-active-class="outClass" @after-enter="$emit('after-enter', $event)" @after-leave="handleAfterLeave")
div(:class="$style.content" v-show="showContent" @click.stop)
div(:class="$style.content" v-show="showContent" @click.stop :style="contentStyle")
header(:class="$style.header")
button(type="button" @click="close" v-if="closeBtn")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 212.982 212.982' space='preserve')
@ -34,6 +34,10 @@ export default {
type: String,
default: '#root',
},
maxWidth: {
type: String,
default: '76%',
},
},
emits: ['after-enter', 'after-leave', 'close'],
data() {
@ -91,6 +95,11 @@ export default {
},
computed: {
...mapGetters(['setting']),
contentStyle() {
return {
maxWidth: this.maxWidth,
}
},
},
watch: {
'setting.randomAnimate'(n) {
@ -182,7 +191,7 @@ export default {
box-shadow: 0 0 3px rgba(0, 0, 0, .3);
overflow: hidden;
max-height: 80%;
max-width: 76%;
// max-width: 76%;
min-width: 220px;
position: relative;
display: flex;

View File

@ -20,13 +20,13 @@ div(:class="$style.songList")
div.list-item(@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
:class="[{ selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]")
div.list-item-cell.nobreak.center(:style="{ width: rowWidth.r1 }" style="padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}}
div.list-item-cell.auto(:style="{ width: rowWidth.r2 }" :tips="item.name + ((item._types.ape || item._types.flac || item._types.wav) ? ` - ${$t('tag__lossless')}` : item._types['320k'] ? ` - ${$t('tag__high_quality')}` : '')")
div.list-item-cell.auto(:style="{ width: rowWidth.r2 }" :aria-label="item.name + ((item._types.ape || item._types.flac || item._types.wav) ? ` - ${$t('tag__lossless')}` : item._types['320k'] ? ` - ${$t('tag__high_quality')}` : '')")
span.select {{item.name}}
span.badge.badge-theme-success(:class="[$style.labelQuality, $style.noSelect]" v-if="item._types.ape || item._types.flac || item._types.wav") {{$t('tag__lossless')}}
span.badge.badge-theme-info(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item._types['320k']") {{$t('tag__high_quality')}}
div.list-item-cell(:style="{ width: rowWidth.r3 }" :tips="item.singer")
div.list-item-cell(:style="{ width: rowWidth.r3 }" :aria-label="item.singer")
span.select {{item.singer}}
div.list-item-cell(:style="{ width: rowWidth.r4 }" :tips="item.albumName")
div.list-item-cell(:style="{ width: rowWidth.r4 }" :aria-label="item.albumName")
span.select {{item.albumName}}
div.list-item-cell(:style="{ width: rowWidth.r5 }")
span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}}

View File

@ -1,5 +1,6 @@
import { computed, useRefGetter, watch, ref, onBeforeUnmount } from '@renderer/utils/vueTools'
import { windowSizeList } from '@common/config'
import { windowSizeList, isFullscreen } from '@renderer/core/share'
import { getFontSizeWithScreen } from '@renderer/utils'
const useKeyEvent = ({ handleSelectAllData }) => {
const keyEvent = {
@ -49,7 +50,7 @@ export default ({ props }) => {
const setting = useRefGetter('setting')
let lastSelectIndex = -1
const listItemHeight = computed(() => {
return parseInt(windowSizeList.find(item => item.id == setting.value.windowSizeId).fontSize) / 16 * 37
return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : parseInt(windowSizeList.find(item => item.id == setting.value.windowSizeId).fontSize)) * 2.3)
})
const removeAllSelect = () => {

View File

@ -6,24 +6,24 @@
<use xlink:href="#icon-left"></use>
</svg></span></li>
<li v-else>
<button type="button" @click="handleClick(page - 1)" :tips="$t('pagination__prev')">
<button type="button" @click="handleClick(page - 1)" :aria-label="$t('pagination__prev')">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-left"></use>
</svg>
</button>
</li>
<li v-if="allPage &gt; btnLength &amp;&amp; page &gt; pageEvg+1" :class="$style.first">
<button type="button" @click="handleClick(1)" :tips="$t('pagination__page', { num: 1 })">
<button type="button" @click="handleClick(1)" :aria-label="$t('pagination__page', { num: 1 })">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-first"></use>
</svg>
</button>
</li>
<li v-for="(p, index) in pages" :key="index" :class="{[$style.active] : p == page}"><span v-if="p === page" v-text="page"></span>
<button v-else type="button" @click="handleClick(p)" v-text="p" :tips="$t('pagination__page', { num: p })"></button>
<button v-else type="button" @click="handleClick(p)" v-text="p" :aria-label="$t('pagination__page', { num: p })"></button>
</li>
<li v-if="allPage &gt; btnLength &amp;&amp; allPage - page &gt; pageEvg" :class="$style.last">
<button type="button" @click="handleClick(allPage)" :tips="$t('pagination__page', { num: allPage })">
<button type="button" @click="handleClick(allPage)" :aria-label="$t('pagination__page', { num: allPage })">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-last"></use>
</svg>
@ -34,7 +34,7 @@
<use xlink:href="#icon-right"></use>
</svg></span></li>
<li v-else>
<button type="button" @click="handleClick(page + 1)" :tips="$t('pagination__next')">
<button type="button" @click="handleClick(page + 1)" :aria-label="$t('pagination__next')">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-right"></use>
</svg>

View File

@ -1,5 +1,5 @@
import { ref, reactive, shallowRef, markRaw } from '@renderer/utils/vueTools'
import { windowSizeList as configWindowSizeList, themes as configThemes } from '@common/config'
import { windowSizeList as configWindowSizeList, themes as configThemes, themeLights as themeLightIds, themeDarks as themeDarkIds } from '@common/config'
import { version } from '../../../../package.json'
process.versions.app = version
@ -36,9 +36,14 @@ export const sync = window.sync = reactive({
devices: [],
},
})
export const isFullscreen = ref(false)
export const windowSizeList = markRaw(configWindowSizeList)
export const themes = markRaw(configThemes)
export const themeLights = markRaw(configThemes.filter(t => themeLightIds.includes(t.id)))
export const themeDarks = markRaw(configThemes.filter(t => themeDarkIds.includes(t.id)))
export const themeShouldUseDarkColors = ref(window.shouldUseDarkColors)
delete window.shouldUseDarkColors
export const qualityList = shallowRef({})
export const setQualityList = _qualityList => {

View File

@ -1,12 +1,14 @@
import { openUrl } from '@renderer/utils'
import { base as eventBaseName } from '@renderer/event/names'
import { onSetConfig } from '@renderer/utils/tools'
import { onSetConfig, onSystemThemeChange } from '@renderer/utils/tools'
import { isFullscreen, themeShouldUseDarkColors } from '@renderer/core/share'
import { rendererSend, NAMES, rendererInvoke } from '@common/ipc'
import {
toRaw,
useCommit,
onBeforeUnmount,
watchEffect,
watch,
useRefGetter,
} from '@renderer/utils/vueTools'
@ -22,6 +24,15 @@ const handleBodyClick = event => {
event.preventDefault()
if (/^https?:\/\//.test(event.target.href)) openUrl(event.target.href)
}
const handle_open_devtools = event => {
rendererSend(NAMES.mainWindow.open_dev_tools)
}
const handle_fullscreen = event => {
if (event.event.repeat) return
rendererInvoke(NAMES.mainWindow.fullscreen, !isFullscreen.value).then(fullscreen => {
isFullscreen.value = fullscreen
})
}
export default ({
dieableIgnoreMouseEvents,
@ -32,12 +43,13 @@ export default ({
}) => {
const setSetting = useCommit('setSetting')
const windowSizeActive = useRefGetter('windowSizeActive')
const isShowAnimation = useRefGetter('isShowAnimation')
watchEffect(() => {
document.documentElement.style.fontSize = windowSizeActive.value.fontSize
watch(windowSizeActive, ({ fontSize }) => {
document.documentElement.style.fontSize = fontSize
})
watchEffect(() => {
if (setting.value.isShowAnimation) {
watch(isShowAnimation, val => {
if (val) {
if (document.body.classList.contains('disableAnimation')) {
document.body.classList.remove('disableAnimation')
}
@ -54,8 +66,15 @@ export default ({
window.eventHub.emit(eventBaseName.set_config, config)
})
const rSystemThemeChange = onSystemThemeChange((event, isDark) => {
// console.log(isDark)
themeShouldUseDarkColors.value = isDark
})
window.eventHub.emit(eventBaseName.bindKey)
window.eventHub.on('key_escape_down', handle_key_esc_down)
window.eventHub.on('key_mod+f12_down', handle_open_devtools)
window.eventHub.on('key_f11_down', handle_fullscreen)
document.body.addEventListener('click', handleBodyClick, true)
if (isProd && !window.dt && !isLinux) {
@ -68,9 +87,12 @@ export default ({
onBeforeUnmount(() => {
window.eventHub.off('key_escape_down', handle_key_esc_down)
window.eventHub.off('key_mod+f12_down', handle_open_devtools)
window.eventHub.off('key_f11_down', handle_fullscreen)
document.body.removeEventListener('click', handleBodyClick)
window.eventHub.emit(eventBaseName.unbindKey)
rSetConfig()
rSystemThemeChange()
if (isProd && !window.dt && !isLinux) {
document.body.removeEventListener('mouseenter', enableIgnoreMouseEvents)

View File

@ -44,9 +44,11 @@ export default ({ setting }) => {
setDesktopLyricInfo('lyric', { lrc: musicInfo.lrc, tlrc: musicInfo.tlrc, lxlrc: musicInfo.lxlrc })
if (isPlay.value && (musicInfo.url || playMusicInfo.listId == 'download')) {
const time = getCurrentTime() * 1000
setDesktopLyricInfo('play', time)
lrc.play(time)
setTimeout(() => {
const time = getCurrentTime() * 1000
setDesktopLyricInfo('play', time)
lrc.play(time)
})
}
}

View File

@ -7,6 +7,10 @@ import { setMediaDeviceId } from '@renderer/plugins/player'
import { isPlay } from '@renderer/core/share/player'
import { player as eventPlayerNames } from '@renderer/event/names'
const getDevices = async() => {
const devices = await navigator.mediaDevices.enumerateDevices()
return devices.filter(({ kind }) => kind == 'audiooutput')
}
export default ({ setting }) => {
let prevDeviceLabel = null
@ -15,7 +19,7 @@ export default ({ setting }) => {
const setMediaDevice = async(mediaDeviceId) => {
let label = prevDeviceLabel
const devices = await navigator.mediaDevices.enumerateDevices()
const devices = await getDevices()
let device = devices.find(device => device.deviceId === mediaDeviceId)
if (device) {
mediaDeviceId = device.deviceId
@ -41,12 +45,15 @@ export default ({ setting }) => {
setting.value.player.isMediaDeviceRemovedStopPlay &&
isPlay.value &&
device.label != prevDeviceLabel
) window.eventHub.emit(eventPlayerNames.setTogglePlay)
) {
global.isPlayedStop = true
window.eventHub.emit(eventPlayerNames.setPause)
}
}
const handleMediaListChange = async() => {
let mediaDeviceId = setting.value.player.mediaDeviceId
const devices = await navigator.mediaDevices.enumerateDevices()
const devices = await getDevices()
let device = devices.find(device => device.deviceId === mediaDeviceId)
if (!device) device = devices.find(device => device.deviceId === 'default')
if (!device) device = { label: null, deviceId: null }

View File

@ -1,10 +1,11 @@
import { onBeforeUnmount, useCommit } from '@renderer/utils/vueTools'
import { player as eventPlayerNames, taskbar as eventTaskbarNames } from '@renderer/event/names'
import { player as eventPlayerNames, taskbar as eventTaskbarNames, list as eventListNames } 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'
import { throttle } from '@renderer/utils'
export default () => {
const listAdd = useCommit('list', 'listAdd')
@ -54,6 +55,11 @@ export default () => {
const handleSetTaskbarThumbnailClip = (clip) => {
setTaskbarThumbnailClip(clip)
}
const throttleListChange = throttle(listIds => {
if (!listIds.includes(loveList.id)) return
if (!updateCollectStatus()) return
setButtons()
})
// const updateSetting = () => {
// const setting = store.getters.setting
// buttons.lrc = setting.desktopLyric.enable
@ -107,6 +113,7 @@ export default () => {
window.eventHub.on(eventPlayerNames.stop, handleStop)
window.eventHub.on(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.on(eventTaskbarNames.setTaskbarThumbnailClip, handleSetTaskbarThumbnailClip)
window.eventHub.on(eventListNames.listChange, throttleListChange)
onBeforeUnmount(() => {
rTaskbarThumbarClick()
@ -115,6 +122,7 @@ export default () => {
window.eventHub.off(eventPlayerNames.stop, handleStop)
window.eventHub.off(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.off(eventTaskbarNames.setTaskbarThumbnailClip, handleSetTaskbarThumbnailClip)
window.eventHub.off(eventListNames.listChange, throttleListChange)
})
return () => {

View File

@ -1,6 +1,6 @@
import { onBeforeUnmount } from '@renderer/utils/vueTools'
import { player as eventPlayerNames, list as eventListNames } from '@renderer/event/names'
import { player as eventPlayerNames, list as eventListNames, download as eventDownloadNames } from '@renderer/event/names'
import { playInfo, playMusicInfo, updatePlayIndex } from '@renderer/core/share/player'
import { getList } from '@renderer/core/share/utils'
import { throttle } from '@renderer/utils'
@ -30,9 +30,15 @@ export default ({ playNext }) => {
throttleListChange()
}
const handleDownloadListChange = () => {
handleListChange(['download'])
}
window.eventHub.on(eventListNames.listChange, handleListChange)
window.eventHub.on(eventDownloadNames.listChange, handleDownloadListChange)
onBeforeUnmount(() => {
window.eventHub.off(eventListNames.listChange, handleListChange)
window.eventHub.off(eventDownloadNames.listChange, handleDownloadListChange)
})
}

View File

@ -106,7 +106,3 @@ eventHub.on(syncName.send_sync_list, ({ action, data }) => {
if (!sync.enable) return
rendererSend(NAMES.mainWindow.sync_list, { action, data })
})
eventHub.on('key_mod+f12_down', ({ action, data }) => {
if (!sync.enable) return
rendererSend(NAMES.mainWindow.open_dev_tools)
})

View File

@ -6,12 +6,6 @@
<title>洛雪音乐助手</title>
</head>
<body>
<script>
if (/theme=(\w+)/.test(window.location.href)) document.body.classList.add(RegExp.$1)
;/dt=(\w+)/.exec(window.location.href)
window.dt = RegExp.$1 == 'true'
document.body.classList.add(window.dt ? 'disableTransparent' : 'transparent')
</script>
<!-- <div id="waiting-mask">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" height="50%" viewbox="0 0 447.942 447.943">
<path id="logo-path-1" d="M203.806.482c-19.668-3.346-35.76 11.139-35.76 31.086v206.166c-11.642-4.271-24.165-6.725-37.281-6.725-59.905 0-108.469 48.566-108.469 108.473 0 59.903 48.564 108.461 108.469 108.461 34.141 0 64.54-15.82 84.406-40.482l-49.658-49.664c-15.116-15.112-11.708-28.901-9.542-34.14 2.166-5.233 9.514-17.4 30.883-17.4h18.082v-56.885c0-21.132 14.617-38.862 34.266-43.745.032-44.373.032-81.808.032-81.808 140.147 0 131.724 83.974 115.325 132.196-6.42 18.884-2.601 22.05 10.893 7.354C536.473 77.106 298.38 16.566 203.806.482z"/>
@ -20,5 +14,11 @@
</div> -->
<div id="root" style="display: none;">
</div>
<script>
if (/theme=(\w+)/.test(window.location.href)) document.getElementById('root').classList.add(RegExp.$1)
window.shouldUseDarkColors = /dark=true/.test(window.location.href)
window.dt = /dt=true/.test(window.location.href)
document.body.classList.add(window.dt ? 'disableTransparent' : 'transparent')
</script>
</body>
</html>

View File

@ -1,6 +1,6 @@
<template>
<transition name="tips-fade" @after-leave="afterLeave">
<div v-show="visible" :style="{ left: position.left + 'px' , top: position.top + 'px', transform: transform }" ref="dom_tips" :class="$style.tips">{{message}}</div>
<div v-show="visible" :style="{ left: position.left + 'px' , top: position.top + 'px', transform: transform }" ref="dom_tips" :class="$style.tips" role="presentation">{{message}}</div>
</transition>
</template>

View File

@ -8,10 +8,14 @@ let prevX = 0
let prevY = 0
let isDraging = false
const getTipText = el => {
return el.getAttribute('aria-label') && el.getAttribute('ignore-tip') == null ? el.getAttribute('aria-label') : null
}
const getTips = el =>
el
? el.getAttribute('tips')
? el.getAttribute('tips')
? getTipText(el)
? getTipText(el)
: el.parentNode === document.documentElement
? null
: getTips(el.parentNode)

View File

@ -14,6 +14,7 @@ import { messages } from '@/lang'
const i18n = createI18n({
locale: 'zh-cn',
fallbackLocale: 'zh-cn',
allowComposition: true,
messages,
})

View File

@ -21,6 +21,8 @@ export const getAnalyser = () => {
return analyser
}
export const hasInitedAnalyser = () => audioContext != null
export const setResource = src => {
if (audio) audio.src = src
}

View File

@ -1,17 +1,24 @@
import music from '../utils/music'
import { themes, windowSizeList } from '@renderer/core/share'
import { themes, windowSizeList, themeShouldUseDarkColors } from '@renderer/core/share'
export default {
theme(state) {
let theme = themes.find(theme => theme.id == state.setting.themeId)
return (theme && theme.className) || ''
const themeId = state.setting.theme.id == 'auto'
? themeShouldUseDarkColors.value
? state.setting.theme.darkId
: state.setting.theme.lightId
: state.setting.theme.id
let theme = themes.find(theme => theme.id == themeId) ?? themes[0]
return theme.className
},
font(state) {
return state.setting.font
},
themes(state) {
return {
active: state.setting.themeId,
active: state.setting.theme.id,
lightId: state.setting.theme.lightId,
darkId: state.setting.theme.darkId,
list: themes,
}
},
@ -59,4 +66,7 @@ export default {
pactModalVisible(state) {
return !state.setting.isAgreePact
},
isShowAnimation(state) {
return state.setting.isShowAnimation
},
}

View File

@ -1,5 +1,7 @@
import music from '../../utils/music'
import { markRawList } from '@renderer/utils/vueTools'
import { deduplicationList } from '@renderer/utils'
const sourceList = {}
const sources = []
const cache = new Map()
@ -57,6 +59,7 @@ const actions = {
// : music[source].leaderboard.getList(bangId, page)
// ).then(result => commit('setList', { result, key }))
return music[source].leaderboard.getList(bangId, page).then(result => {
result.list = deduplicationList(result.list)
cache.set(key, result)
listInfo.list = markRawList(result.list)
listInfo.total = result.total
@ -75,6 +78,7 @@ const actions = {
return cache.has(key)
? Promise.resolve(cache.get(key))
: music[source].leaderboard.getList(bangId, page).then(result => {
result.list = markRawList(deduplicationList(result.list))
cache.set(key, result)
return result
})
@ -89,7 +93,7 @@ const actions = {
: loadData(id, loadPage).then(result1 => load(++loadPage).then(result2 => [...result1.list, ...result2]))
}
return load().then(result2 => [...result.list, ...result2])
})
}).then(list => deduplicationList(list))
},
}

View File

@ -36,29 +36,52 @@ const playMusic = () => {
window.eventHub.emit(eventPlayerNames.playMusic)
}
const filterList = async({ playedList, listInfo, savePath, commit }) => {
const filterList = async({ playedList, listInfo, savePath, commit, isCheckFile }) => {
// if (this.list.listName === null) return
let list
let canPlayList = []
const filteredPlayedList = playedList.filter(({ listId, isTempPlay }) => listInfo.id === listId && !isTempPlay).map(({ musicInfo }) => musicInfo)
if (listInfo.id == 'download') {
list = []
for (const item of listInfo.list) {
const filePath = path.join(savePath, item.metadata.fileName)
if (!await checkPath(filePath) || !item.isComplate || /\.ape$/.test(filePath)) continue
// console.log(isCheckFile)
if (isCheckFile) {
let list
let canPlayList = []
const filteredPlayedList = playedList.filter(({ listId, isTempPlay }) => listInfo.id === listId && !isTempPlay).map(({ musicInfo }) => musicInfo)
if (listInfo.id == 'download') {
list = []
for (const item of listInfo.list) {
const filePath = path.join(savePath, item.metadata.fileName)
if (!await checkPath(filePath) || !item.isComplate || /\.ape$/.test(filePath)) continue
canPlayList.push(item)
canPlayList.push(item)
// 排除已播放音乐
let index = filteredPlayedList.findIndex(m => m.songmid == item.songmid)
if (index > -1) {
filteredPlayedList.splice(index, 1)
continue
// 排除已播放音乐
let index = filteredPlayedList.findIndex(m => m.songmid == item.songmid)
if (index > -1) {
filteredPlayedList.splice(index, 1)
continue
}
list.push(item)
}
list.push(item)
} else {
list = listInfo.list.filter(s => {
// if (!assertApiSupport(s.source)) return false
canPlayList.push(s)
let index = filteredPlayedList.findIndex(m => m.songmid == s.songmid)
if (index > -1) {
filteredPlayedList.splice(index, 1)
return false
}
return true
})
}
if (!list.length && playedList.length) {
commit('clearPlayedList')
return canPlayList
}
return list
} else {
list = listInfo.list.filter(s => {
let canPlayList = []
const filteredPlayedList = playedList.filter(({ listId, isTempPlay }) => listInfo.id === listId && !isTempPlay).map(({ musicInfo }) => musicInfo)
const list = listInfo.list.filter(s => {
// if (!assertApiSupport(s.source)) return false
canPlayList.push(s)
@ -69,12 +92,12 @@ const filterList = async({ playedList, listInfo, savePath, commit }) => {
}
return true
})
if (!list.length && playedList.length) {
commit('clearPlayedList')
return canPlayList
}
return list
}
if (!list.length && playedList.length) {
commit('clearPlayedList')
return canPlayList
}
return list
}
const getMusicUrl = function(musicInfo, type, onToggleSource, retryedSource = [], originMusic) {
@ -188,6 +211,7 @@ const actions = {
},
async getLrc({ commit, state }, musicInfo) {
const lrcInfo = await getStoreLyric(musicInfo)
// lrcInfo = {}
// if (lrcRequest && lrcRequest.cancelHttp) lrcRequest.cancelHttp()
if (lrcInfo.lyric && lrcInfo.tlyric != null) {
// if (musicInfo.lrc.startsWith('\ufeff[id:$00000000]')) {
@ -198,7 +222,15 @@ const actions = {
// commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })
// }
if ((lrcInfo.lxlyric == null && musicInfo.source != 'kg') || lrcInfo.lxlyric != null) return lrcInfo
if (lrcInfo.lxlyric == null) {
switch (musicInfo.source) {
case 'kg':
case 'kw':
break
default:
return lrcInfo
}
} else return lrcInfo
}
// lrcRequest = music[musicInfo.source].getLyric(musicInfo)
@ -212,7 +244,7 @@ const actions = {
})
},
async playPrev({ state, rootState, commit, getters }) {
async playPrev({ state, rootState, commit, getters }, { findNum = 0, excludeList = [] } = {}) {
const currentListId = playInfo.playListId
const currentList = getList(currentListId)
if (playedList.length) {
@ -242,11 +274,13 @@ const actions = {
}
}
const isCheckFile = findNum > 2
let filteredList = await filterList({
listInfo: { id: currentListId, list: currentList },
playedList,
playedList: excludeList.length ? [...playedList, ...excludeList] : playedList,
savePath: rootState.setting.download.savePath,
commit,
isCheckFile,
})
if (!filteredList.length) return commit('setPlayMusicInfo', { listId: null, musicInfo: null })
@ -271,14 +305,24 @@ const actions = {
if (nextIndex < 0) return
}
commit('setPlayMusicInfo', {
const nextPlayMusicInfo = {
musicInfo: filteredList[nextIndex],
listId: currentListId,
})
}
if (currentListId == 'download' && !isCheckFile) {
if (!await checkPath(path.join(rootState.setting.download.savePath, nextPlayMusicInfo.musicInfo.metadata.fileName)) || !nextPlayMusicInfo.musicInfo.isComplate || /\.ape$/.test(nextPlayMusicInfo.musicInfo.metadata.fileName)) {
excludeList.push(nextPlayMusicInfo)
// console.log('findNum', findNum)
return this.dispatch('player/playPrev', { findNum: findNum + 1, excludeList })
}
}
commit('setPlayMusicInfo', nextPlayMusicInfo)
playMusic()
},
async playNext({ state, rootState, commit, getters }) {
if (tempPlayList.length) {
async playNext({ state, rootState, commit, getters }, { findNum = 0, excludeList = [] } = {}) {
if (tempPlayList.length) { // 如果稍后播放列表存在歌曲则直接播放改列表的歌曲
const playMusicInfo = tempPlayList[0]
commit('removeTempPlayList', 0)
commit('setPlayMusicInfo', playMusicInfo)
@ -290,7 +334,7 @@ const actions = {
const currentListId = playInfo.playListId
const currentList = getList(currentListId)
if (playedList.length) {
if (playedList.length) { // 移除已播放列表内不存在原列表的歌曲
let currentSongmid
if (playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.listPlayIndex]
@ -316,11 +360,13 @@ const actions = {
return
}
}
let filteredList = await filterList({
const isCheckFile = findNum > 2 // 针对下载列表,如果超过两次都碰到无效歌曲,则过滤整个列表内的无效歌曲
let filteredList = await filterList({ // 过滤已播放歌曲
listInfo: { id: currentListId, list: currentList },
playedList,
playedList: excludeList.length ? [...playedList, ...excludeList] : playedList,
savePath: rootState.setting.download.savePath,
commit,
isCheckFile,
})
if (!filteredList.length) return commit('setPlayMusicInfo', { listId: null, musicInfo: null })
@ -346,10 +392,19 @@ const actions = {
}
if (nextIndex < 0) return
commit('setPlayMusicInfo', {
const nextPlayMusicInfo = {
musicInfo: filteredList[nextIndex],
listId: currentListId,
})
}
if (currentListId == 'download' && !isCheckFile) { // 针对下载列表,检查文件是否可以播放
if (!await checkPath(path.join(rootState.setting.download.savePath, nextPlayMusicInfo.musicInfo.metadata.fileName)) || !nextPlayMusicInfo.musicInfo.isComplate || /\.ape$/.test(nextPlayMusicInfo.musicInfo.metadata.fileName)) {
excludeList.push(nextPlayMusicInfo)
// console.log('findNum', findNum)
return this.dispatch('player/playNext', { findNum: findNum + 1, excludeList })
}
}
commit('setPlayMusicInfo', nextPlayMusicInfo)
playMusic()
},
}

View File

@ -1,5 +1,6 @@
import music from '../../utils/music'
import { markRawList } from '@renderer/utils/vueTools'
import { deduplicationList } from '@renderer/utils'
const sources = []
const sourceList = {}
@ -85,7 +86,7 @@ sources.push({
})
// state
const state = {
const state = window.state = {
sourceList,
list: [],
text: '',
@ -151,6 +152,7 @@ const mutations = {
},
setList(state, datas) {
let source = state.sourceList[datas.source]
datas.list = deduplicationList(datas.list)
source.list = markRawList(datas.list)
source.total = datas.total
source.allPage = datas.allPage
@ -170,6 +172,7 @@ const mutations = {
total += source.total
// limit = Math.max(source.limit, limit)
}
list = deduplicationList(list)
state.allPage = Math.max(...pages)
state.total = total
state.limit = limit

View File

@ -1,5 +1,6 @@
import music from '../../utils/music'
import { markRawList } from '@renderer/utils/vueTools'
import { deduplicationList } from '@renderer/utils'
const sortList = {}
const sources = []
@ -91,6 +92,7 @@ const actions = {
? Promise.resolve(cache.get(key))
: music[source]?.songList.getListDetail(id, page).then(result => ({ ...result, list: filterList(result.list) }))
).then(result => {
result.list = markRawList(deduplicationList(result.list))
commit('setListDetail', { result, key, source, id, page })
return result.list
})
@ -103,9 +105,10 @@ const actions = {
return cache.has(key)
? Promise.resolve(cache.get(key))
: music[source]?.songList.getListDetail(id, page).then(result => {
result.list = markRawList(deduplicationList(result.list))
cache.set(key, result)
return result
})
}) ?? Promise.reject(new Error('source not found'))
}
return loadData(id, 1).then(result => {
if (result.total <= result.limit) return filterList(result.list)
@ -117,7 +120,7 @@ const actions = {
: loadData(id, loadPage).then(result1 => loadDetail(++loadPage).then(result2 => [...result1.list, ...result2]))
}
return loadDetail().then(result2 => [...result.list, ...result2]).then(list => filterList(list))
})
}).then(list => deduplicationList(list))
},
}
@ -139,7 +142,7 @@ const mutations = {
cache.set(key, result)
},
setListDetail(state, { result, key, source, id, page }) {
state.listDetail.list = markRawList(result.list)
state.listDetail.list = result.list
state.listDetail.id = id
state.listDetail.source = source
state.listDetail.total = result.total

View File

@ -1,6 +1,6 @@
export default {
setTheme(state, val) {
state.setting.themeId = val
state.setting.theme.id = val
},
setSearchSource(state, { searchSource, tempSearchSource }) {
console.log(searchSource, tempSearchSource)
@ -35,6 +35,9 @@ export default {
state.setting.player.volume = val
}
},
setPlayDetailLyricFont(state, val) {
state.setting.playDetail.style.fontSize = val
},
setPlayNextMode(state, val) {
state.setting.player.togglePlayMethod = val
},

View File

@ -1,18 +1,75 @@
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from '@renderer/utils/vueTools'
import { scrollTo } from '@renderer/utils'
import { scrollTo, throttle, formatPlayTime2 } from '@renderer/utils'
import { player as eventPlayerNames } from '@renderer/event/names'
export default ({ isPlay, lyric }) => {
const dom_lyric = ref(null)
const dom_lyric_text = ref(null)
const dom_skip_line = ref(null)
const isMsDown = ref(false)
const isStopScroll = ref(false)
const timeStr = ref('--/--')
let msDownY = 0
let msDownScrollY = 0
let isStopScroll = false
let timeout = null
let cancelScrollFn
let dom_lines
let isSetedLines = false
let point = {
x: null,
y: null,
}
let time = -1
let dom_pre_line = null
let isSkipMouseEnter = false
const handleSkipPlay = () => {
if (time == -1) return
handleSkipMouseLeave()
isStopScroll.value = false
window.eventHub.emit(eventPlayerNames.setProgress, time)
if (!isPlay.value) window.eventHub.emit(eventPlayerNames.setPlay)
}
const handleSkipMouseEnter = () => {
isSkipMouseEnter = true
clearLyricScrollTimeout()
}
const handleSkipMouseLeave = () => {
isSkipMouseEnter = false
startLyricScrollTimeout()
}
const setTime = throttle(() => {
if (point.x == null) {
if (!dom_skip_line.value) return
const rect = dom_skip_line.value.getBoundingClientRect()
point.x = rect.x
point.y = rect.y
}
let dom = document.elementFromPoint(point.x, point.y)
if (dom_pre_line === dom) return
if (dom.tagName == 'SPAN') {
dom = dom.parentNode.parentNode
} else if (dom.classList.contains('font')) {
dom = dom.parentNode
}
if (dom.time == null) {
if (lyric.lines.length) {
time = dom.classList.contains('pre') ? 0 : lyric.lines[lyric.lines.length - 1].time ?? 0
if (time) time = time / 1000
timeStr.value = formatPlayTime2(time)
} else {
time = -1
timeStr.value = '--:--'
}
} else {
time = dom.time
if (time) time = time / 1000
timeStr.value = formatPlayTime2(time)
}
dom_pre_line = dom
})
const handleScrollLrc = (duration = 300) => {
if (!dom_lines?.length || !dom_lyric.value) return
@ -20,7 +77,8 @@ export default ({ isPlay, lyric }) => {
cancelScrollFn()
cancelScrollFn = null
}
if (isStopScroll) return
if (isSkipMouseEnter) return
if (isStopScroll.value) return
let dom_p = dom_lines[lyric.line]
cancelScrollFn = scrollTo(dom_lyric.value, dom_p ? (dom_p.offsetTop - dom_lyric.value.clientHeight * 0.38) : 0, duration)
}
@ -31,9 +89,10 @@ export default ({ isPlay, lyric }) => {
}
const startLyricScrollTimeout = () => {
clearLyricScrollTimeout()
if (isSkipMouseEnter) return
timeout = setTimeout(() => {
timeout = null
isStopScroll = false
isStopScroll.value = false
if (!isPlay.value) return
handleScrollLrc()
}, 3000)
@ -53,25 +112,27 @@ export default ({ isPlay, lyric }) => {
}
const handleMouseMsMove = event => {
if (isMsDown.value) {
if (!isStopScroll) isStopScroll = true
if (!isStopScroll.value) isStopScroll.value = true
if (cancelScrollFn) {
cancelScrollFn()
cancelScrollFn = null
}
dom_lyric.value.scrollTop = msDownScrollY + msDownY - event.clientY
startLyricScrollTimeout()
setTime()
}
}
const handleWheel = (event) => {
console.log(event.deltaY)
if (!isStopScroll) isStopScroll = true
if (!isStopScroll.value) isStopScroll.value = true
if (cancelScrollFn) {
cancelScrollFn()
cancelScrollFn = null
}
dom_lyric.value.scrollTop = dom_lyric.value.scrollTop + event.deltaY
startLyricScrollTimeout()
setTime()
}
const setLyric = (lines) => {
@ -139,8 +200,14 @@ export default ({ isPlay, lyric }) => {
return {
dom_lyric,
dom_lyric_text,
dom_skip_line,
isStopScroll,
isMsDown,
timeStr,
handleLyricMouseDown,
handleWheel,
handleSkipPlay,
handleSkipMouseEnter,
handleSkipMouseLeave,
}
}

View File

@ -560,3 +560,24 @@ export const saveStrToFile = (path, str) => new Promise((resolve, reject) => {
const fileNameRxp = /[\\/:*?#"<>|]/g
export const filterFileName = name => name.replace(fileNameRxp, '')
export const getFontSizeWithScreen = (screenWidth = window.innerWidth) => {
return screenWidth <= 1440
? 16
: screenWidth <= 1920
? 18
: screenWidth <= 2560
? 20
: screenWidth <= 2560 ? 20 : 22
}
export const deduplicationList = list => {
const ids = new Set()
return list.filter(s => {
if (ids.has(s.songmid)) return false
ids.add(s.songmid)
return true
})
}

View File

@ -20,7 +20,8 @@ const createAnimation = (dom, duration) => new window.Animation(new window.Keyfr
// https://jsfiddle.net/ceqpnbky/1/
module.exports = class FontPlayer {
constructor({ lyric = '', translationLyric = '', lineClassName = '', fontClassName = '', translationClassName = '', lineModeClassName = '', shadowContent = false, shadowClassName = '' }) {
constructor({ time = 0, lyric = '', translationLyric = '', lineClassName = '', fontClassName = '', translationClassName = '', lineModeClassName = '', shadowContent = false, shadowClassName = '' }) {
this.time = time
this.lyric = lyric
this.translationLyric = translationLyric
@ -35,7 +36,7 @@ module.exports = class FontPlayer {
this.curFontNum = 0
this.maxFontNum = 0
this._performanceTime = 0
this._performanceOffsetTime = 0
this._startTime = 0
this.fontContent = null
@ -51,6 +52,7 @@ module.exports = class FontPlayer {
this.isLineMode = false
this.lineContent = document.createElement('div')
this.lineContent.time = this.time
if (this.lineClassName) this.lineContent.classList.add(this.lineClassName)
this.fontContent = document.createElement('div')
this.fontContent.style = 'position:relative;display:inline-block;'
@ -139,12 +141,12 @@ module.exports = class FontPlayer {
}
_currentTime() {
return getNow() - this._performanceTime + this._performanceOffsetTime
return getNow() - this._performanceTime + this._startTime
}
_findcurFontNum(curTime) {
_findcurFontNum(curTime, startIndex = 0) {
const length = this.fonts.length
for (let index = 0; index < length; index++) if (curTime <= this.fonts[index].startTime) return index === 0 ? 0 : index - 1
for (let index = startIndex; index < length; index++) if (curTime < this.fonts[index].startTime) return index == 0 ? 0 : index - 1
return length - 1
}
@ -193,9 +195,8 @@ module.exports = class FontPlayer {
_refresh() {
this.curFontNum++
// console.log('curFontNum time', this.fonts[this.curFontNum].time)
if (this.curFontNum === this.maxFontNum) return this._handlePlayMaxFontNum()
if (this.curFontNum >= this.maxFontNum) return this._handlePlayMaxFontNum()
let curFont = this.fonts[this.curFontNum]
let nextFont = this.fonts[this.curFontNum + 1]
// console.log(curFont, nextFont, this.curFontNum, this.maxFontNum)
const currentTime = this._currentTime()
// console.log(curFont.text)
@ -204,27 +205,38 @@ module.exports = class FontPlayer {
// console.log(currentTime, driftTime)
if (driftTime >= 0 || this.curFontNum == 0) {
let nextFont = this.fonts[this.curFontNum + 1]
this.delay = nextFont.startTime - curFont.startTime - driftTime
if (this.delay > 0) {
if (this.isPlay) {
this.timeoutTools.start(() => {
if (!this.isPlay) return
this._refresh()
}, this.delay)
}
this._handlePlayFont(curFont, driftTime)
this.timeoutTools.start(() => {
if (!this.isPlay) return
this._refresh()
}, this.delay)
return
} else {
let newCurLineNum = this._findcurFontNum(currentTime, this.curFontNum + 1)
if (newCurLineNum > this.curFontNum) this.curFontNum = newCurLineNum - 1
for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
this._refresh()
return
}
} else if (this.curFontNum == 0) {
this.curFontNum--
this.waitPlayTimeout.start(() => {
if (!this.isPlay) return
this._refresh()
}, -driftTime)
if (this.isPlay) {
this.waitPlayTimeout.start(() => {
if (!this.isPlay) return
this._refresh()
}, -driftTime)
}
return
}
this.curFontNum = this._findcurFontNum(currentTime)
for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
this.curFontNum--
this.curFontNum = this._findcurFontNum(currentTime, this.curFontNum) - 1
for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
// this.curFontNum--
this._refresh()
}
@ -235,14 +247,11 @@ module.exports = class FontPlayer {
if (this.isLineMode) return this._handlePlayLine(true)
this.isPlay = true
this._performanceTime = getNow() - curTime
this._performanceOffsetTime = 0
if (this._performanceTime < 0) {
this._performanceOffsetTime = -this._performanceTime
this._performanceTime = 0
}
this._performanceTime = getNow()
this._startTime = curTime
this.curFontNum = this._findcurFontNum(curTime)
for (let i = this.curFontNum; i > -1; i--) {
this._handlePlayFont(this.fonts[i], 0, true)
}

View File

@ -7,7 +7,7 @@ module.exports = class Lyric {
constructor({
lyric = '',
translationLyric = '',
offset = 150,
offset = 0,
lineClassName = '',
fontClassName = 'font',
translationClassName = 'translation',
@ -61,7 +61,7 @@ module.exports = class Lyric {
font.reset()
font.lineContent.classList.remove(this.activeLineClassName)
}
} else if (num > this.playingLineNum + 1) {
} else if (num > this.playingLineNum) {
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
const font = this._lineFonts[i]
font.reset()
@ -79,7 +79,7 @@ module.exports = class Lyric {
font.lineContent.classList.remove(this.activeLineClassName)
font.reset()
}
} else if (num > this.playingLineNum + 1) {
} else if (num > this.playingLineNum) {
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
const font = this._lineFonts[i]
font.lineContent.classList.remove(this.activeLineClassName)
@ -106,6 +106,7 @@ module.exports = class Lyric {
if (this.isLineMode) {
this._lines = lyricLines.map(line => {
const fontPlayer = new FontPlayer({
time: line.time,
lyric: line.text,
translationLyric: line.translation,
lineClassName: this.lineClassName,
@ -127,6 +128,7 @@ module.exports = class Lyric {
} else {
this._lines = lyricLines.map(line => {
const fontPlayer = new FontPlayer({
time: line.time,
lyric: line.text,
translationLyric: line.translation,
lineClassName: this.lineClassName,
@ -164,5 +166,6 @@ module.exports = class Lyric {
this.lyric = lyric
this.translationLyric = translationLyric
this._init()
this.linePlayer.offset = this.isLineMode ? this.offset + 90 : this.offset
}
}

View File

@ -24,9 +24,8 @@ module.exports = class LinePlayer {
this.curLineNum = 0
this.maxLine = 0
this.offset = offset
this.isOffseted = false
this._performanceTime = 0
this._performanceOffsetTime = 0
this._startTime = 0
this._init()
}
@ -39,10 +38,17 @@ module.exports = class LinePlayer {
}
_initTag() {
this.tags = {}
for (let tag in tagRegMap) {
const matches = this.lyric.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i'))
this.tags[tag] = (matches && matches[1]) || ''
}
if (this.tags.offset) {
let offset = parseInt(this.tags.offset)
this.tags.offset = Number.isNaN(offset) ? 0 : offset
} else {
this.tags.offset = 0
}
}
_initLines() {
@ -93,12 +99,13 @@ module.exports = class LinePlayer {
}
_currentTime() {
return getNow() - this._performanceTime + this._performanceOffsetTime
return getNow() - this._performanceTime + this._startTime
}
_findCurLineNum(curTime) {
_findCurLineNum(curTime, startIndex = 0) {
if (curTime <= 0) return 0
const length = this.lines.length
for (let index = 0; index < length; index++) if (curTime <= this.lines[index].time) return index === 0 ? 0 : index - 1
for (let index = startIndex; index < length; index++) if (curTime <= this.lines[index].time) return index === 0 ? 0 : index - 1
return length - 1
}
@ -110,30 +117,35 @@ module.exports = class LinePlayer {
_refresh() {
this.curLineNum++
// console.log('curLineNum time', this.lines[this.curLineNum].time)
if (this.curLineNum >= this.maxLine) return this._handleMaxLine()
let curLine = this.lines[this.curLineNum]
let nextLine = this.lines[this.curLineNum + 1]
const currentTime = this._currentTime()
const driftTime = currentTime - curLine.time
if (driftTime >= 0 || this.curLineNum === 0) {
if (this.curLineNum === this.maxLine) return this._handleMaxLine()
let nextLine = this.lines[this.curLineNum + 1]
this.delay = nextLine.time - curLine.time - driftTime
if (this.delay > 0) {
if (!this.isOffseted && this.delay >= this.offset) {
this._performanceOffsetTime += this.offset
this.delay -= this.offset
this.isOffseted = true
if (this.isPlay) {
timeoutTools.start(() => {
if (!this.isPlay) return
this._refresh()
}, this.delay)
}
timeoutTools.start(() => {
if (!this.isPlay) return
this._refresh()
}, this.delay)
this.onPlay(this.curLineNum, curLine.text, currentTime)
return
} else {
let newCurLineNum = this._findCurLineNum(currentTime, this.curLineNum + 1)
if (newCurLineNum > this.curLineNum) this.curLineNum = newCurLineNum - 1
this._refresh()
return
}
}
this.curLineNum = this._findCurLineNum(currentTime) - 1
this.curLineNum = this._findCurLineNum(currentTime, this.curLineNum) - 1
this._refresh()
}
@ -142,14 +154,10 @@ module.exports = class LinePlayer {
this.pause()
this.isPlay = true
this._performanceOffsetTime = 0
this._performanceTime = getNow() - curTime
if (this._performanceTime < 0) {
this._performanceOffsetTime = -this._performanceTime
this._performanceTime = 0
}
this._performanceTime = getNow() - parseInt(this.tags.offset + this.offset)
this._startTime = curTime
this.curLineNum = this._findCurLineNum(curTime) - 1
this.curLineNum = this._findCurLineNum(this._currentTime()) - 1
this._refresh()
}
@ -157,7 +165,6 @@ module.exports = class LinePlayer {
pause() {
if (!this.isPlay) return
this.isPlay = false
this.isOffseted = false
timeoutTools.clear()
if (this.curLineNum === this.maxLine) return
const currentTime = this._currentTime()

View File

@ -720,7 +720,10 @@ export default {
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('id_', '')
if (typeof id == 'string') {
if (/^https?:\/\//.test(id)) return id
id = id.replace('id_', '')
}
return `https://www.kugou.com/yy/special/single/${id}.html`
},
}

View File

@ -40,7 +40,7 @@ const kw = {
comment,
getLyric(songInfo, isGetLyricx) {
// let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer
return lyric.getLyric(songInfo.songmid, isGetLyricx)
return lyric.getLyric(songInfo, isGetLyricx)
},
handleMusicInfo(songInfo) {
return this.getMusicInfo(songInfo).then(info => {

View File

@ -1,6 +1,8 @@
import { httpFetch } from '../../request'
import { decodeLyric, lrcTools } from './util'
import { decodeName } from '../../index'
/*
export default {
formatTime(time) {
let m = parseInt(time / 60)
@ -63,33 +65,146 @@ export default {
return requestObj
},
}
*/
const buf_key = Buffer.from('yeelion')
const buf_key_len = buf_key.length
const buildParams = (id, isGetLyricx) => {
let params = `user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_${id}`
if (isGetLyricx) params += '&lrcx=1'
const buf_str = Buffer.from(params)
const buf_str_len = buf_str.length
const output = new Uint16Array(buf_str_len)
let i = 0
while (i < buf_str_len) {
let j = 0
while (j < buf_key_len && i < buf_str_len) {
output[i] = buf_key[j] ^ buf_str[i]
i++
j++
}
}
return Buffer.from(output).toString('base64')
}
/* export default {
lrcInfoRxp: /<lyric>(.+?)<\/lyric>[\s\S]+<lyric_zz>(.+?)<\/lyric_zz>/,
parseLyricInfo(str) {
let result = str.match(this.lrcInfoRxp)
return result ? { lyric: result[1], lyric_zz: result[2] } : null
// console.log(buildParams('207527604', false))
// console.log(buildParams('207527604', true))
const timeExp = /^\[([\d:.]*)\]{1}/g
export default {
sortLrcArr(arr) {
const lrcSet = new Set()
let lrc = []
let lrcT = []
for (const item of arr) {
if (lrcSet.has(item.time)) {
if (lrc.length < 2) continue
const tItem = lrc.pop()
tItem.time = lrc[lrc.length - 1].time
lrcT.push(tItem)
lrc.push(item)
} else {
lrc.push(item)
lrcSet.add(item.time)
}
}
if (lrcT.length) {
if (lrc.length * 0.4 < lrcT.length) { // 翻译数量需大于歌词数量的0.4倍,否则认为没有翻译
const tItem = lrc.pop()
tItem.time = lrc[lrc.length - 1].time
lrcT.push(tItem)
} else {
lrc = arr
lrcT = []
}
}
return {
lrc,
lrcT,
}
},
getLyric(songId, isGetLyricx = false) {
const requestObj = httpFetch(`http://player.kuwo.cn/webmusic/st/getNewMuiseByRid?rid=MUSIC_${songId}`)
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
console.log(body)
if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body)))
let info = this.parseLyricInfo(body)
if (!info) return Promise.reject(new Error(JSON.stringify(body)))
Object.assign(requestObj, httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${isGetLyricx ? info.lyric_zz : info.lyric}`))
return requestObj.promise.then(({ statusCode, body, raw }) => {
if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body)))
return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => {
return {
lyric: Buffer.from(base64Data, 'base64').toString(),
tlyric: '',
}
transformLrc(tags, lrclist) {
return `${tags.join('\n')}\n${lrclist ? lrclist.map(l => `[${l.time}]${l.text}\n`).join('') : '暂无歌词'}`
},
parseLrc(lrc) {
const lines = lrc.split(/\r\n|\r|\n/)
let tags = []
let lrcArr = []
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
let result = timeExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
lrcArr.push({
time: RegExp.$1,
text,
})
} else if (lrcTools.rxps.tagLine.test(line)) {
tags.push(line)
}
}
const lrcInfo = this.sortLrcArr(lrcArr)
return {
lyric: decodeName(this.transformLrc(tags, lrcInfo.lrc)),
tlyric: lrcInfo.lrcT.length ? decodeName(this.transformLrc(tags, lrcInfo.lrcT)) : '',
}
},
// getLyric2(musicInfo, isGetLyricx = true) {
// const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`)
// requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => {
// if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body)))
// return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => {
// let lrcInfo
// console.log(Buffer.from(base64Data, 'base64').toString())
// try {
// lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())
// } catch {
// return Promise.reject(new Error('Get lyric failed'))
// }
// if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '')
// lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric)
// // console.log(lrcInfo.lyric)
// // console.log(lrcInfo.tlyric)
// // console.log(lrcInfo.lxlyric)
// // console.log(JSON.stringify(lrcInfo))
// })
// })
// return requestObj
// },
getLyric(musicInfo, isGetLyricx = true) {
// this.getLyric2(musicInfo)
const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`)
requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => {
if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body)))
return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => {
// let lrcInfo
// try {
// lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())
// } catch {
// return Promise.reject(new Error('Get lyric failed'))
// }
let lrcInfo
// console.log(Buffer.from(base64Data, 'base64').toString())
try {
lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())
} catch (err) {
return Promise.reject(new Error('Get lyric failed'))
}
// console.log(lrcInfo)
if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '')
try {
lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric)
} catch {
lrcInfo.lxlyric = ''
}
lrcInfo.lyric = lrcInfo.lyric.replace(lrcTools.rxps.wordTimeAll, '')
// console.log(lrcInfo)
return lrcInfo
})
})
return requestObj
},
}
*/

View File

@ -308,7 +308,8 @@ export default {
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'kw' }))
},
getDetailPageUrl(id) {
if (/^digest-/.test(id)) {
if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')
else if (/^digest-/.test(id)) {
let result = id.split('__')
id = result[1]
}

View File

@ -1,5 +1,5 @@
import { httpGet, httpFetch } from '../../request'
// import { rendererInvoke, NAMES } from '../../../../common/ipc'
import { rendererInvoke, NAMES } from '@common/ipc'
const kw_token = {
token: null,
@ -54,7 +54,7 @@ export const getToken = (retryNum = 0) => new Promise((resolve, reject) => {
})
})
// export const decodeLyric = base64Data => rendererInvoke(NAMES.mainWindow.handle_kw_decode_lyric, base64Data)
export const decodeLyric = base64Data => rendererInvoke(NAMES.mainWindow.handle_kw_decode_lyric, base64Data)
export const tokenRequest = async(url, options = {}) => {
let token = kw_token.token
@ -76,3 +76,83 @@ export const tokenRequest = async(url, options = {}) => {
})
return requestObj
}
export const lrcTools = {
rxps: {
wordLine: /^(\[\d{1,2}:.*\d{1,4}\])\s*(\S+(?:\s+\S+)*)?\s*/,
tagLine: /\[(ver|ti|ar|al|offset|by|kuwo):\s*(\S+(?:\s+\S+)*)\s*\]/,
wordTimeAll: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/g,
wordTime: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/,
},
offset: 1,
offset2: 1,
isOK: false,
lines: [],
tags: [],
getWordInfo(str, str2) {
const offset = parseInt(str)
const offset2 = parseInt(str2)
const startTime = Math.floor((offset + offset2) / (this.offset * 2))
const timeLength = Math.floor((offset - offset2) / (this.offset2 * 2))
return {
startTime,
timeLength,
}
},
parseLine(line) {
if (line.length < 6) return
let result = this.rxps.wordLine.exec(line)
if (result) {
const time = result[1]
let words = result[2]
if (words == null) {
words = ''
}
const wordTimes = words.match(this.rxps.wordTimeAll)
if (!wordTimes) return
// console.log(wordTimes)
for (const timeStr of wordTimes) {
const result = this.rxps.wordTime.exec(timeStr)
const wordInfo = this.getWordInfo(result[1], result[2])
words = words.replace(timeStr, `<${wordInfo.startTime},${wordInfo.timeLength}>`)
}
this.lines.push(time + words)
return
}
result = this.rxps.tagLine.exec(line)
if (!result) return
if (result[1] == 'kuwo') {
let content = result[2]
if (content != null && content.includes('][')) {
content = content.substring(0, content.indexOf(']['))
}
const valueOf = parseInt(content, 8)
this.offset = Math.floor(valueOf / 10)
this.offset2 = Math.floor(valueOf % 10)
if (this.offset == 0 || Number.isNaN(this.offset) || this.offset2 == 0 || Number.isNaN(this.offset2)) {
this.isOK = false
}
} else {
this.tags.push(line)
}
},
parse(lrc) {
// console.log(lrc)
const lines = lrc.split(/\r\n|\r|\n/)
const tools = Object.create(this)
tools.isOK = true
tools.offset = 1
tools.offset2 = 1
tools.lines = []
tools.tags = []
for (const line of lines) {
if (!tools.isOK) return ''
tools.parseLine(line)
}
if (!tools.lines.length) return ''
let lrcs = tools.lines.join('\n')
if (tools.tags.length) lrcs = `${tools.tags.join('\n')}\n${lrcs}`
return lrcs
},
}

View File

@ -357,6 +357,11 @@ export default {
},
getDetailPageUrl(id) {
if (/playlist\/index\.html\?/.test(id)) {
id = id.replace(/.*(?:\?|&)id=(\d+)(?:&.*|$)/, '$1')
} else if (this.regExps.listDetailLink.test(id)) {
id = id.replace(this.regExps.listDetailLink, '$1')
}
return `https://music.migu.cn/v3/music/playlist/${id}`
},
}

View File

@ -185,11 +185,7 @@ export default {
return location == null ? link : location
},
// 获取歌曲列表内的音乐
async getListDetail(id, tryNum = 0) {
if (this._requestObj_listDetail) this._requestObj_listDetail.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
async getListId(id) {
if ((/[?&:/]/.test(id))) {
let regx = /\/\/i\.y\.qq\.com/.test(id) ? this.regExps.listDetailLink1 : this.regExps.listDetailLink2
if (!regx.test(id)) {
@ -200,6 +196,14 @@ export default {
id = id.replace(regx, '$1')
// console.log(id)
}
return id
},
// 获取歌曲列表内的音乐
async getListDetail(id, tryNum = 0) {
if (this._requestObj_listDetail) this._requestObj_listDetail.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
id = await this.getListId(id)
this._requestObj_listDetail = httpFetch(this.getListDetailUrl(id), {
headers: {
@ -293,7 +297,9 @@ export default {
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'tx' }))
},
getDetailPageUrl(id) {
async getDetailPageUrl(id) {
id = await this.getListId(id)
return `https://y.qq.com/n/ryqq/playlist/${id}`
},
}

View File

@ -51,23 +51,24 @@ export default {
async handleParseId(link, retryNum = 0) {
if (this._requestObj_listDetailLink) this._requestObj_listDetailLink.cancelHttp()
if (retryNum > 2) return Promise.reject(new Error('link try max num'))
if (retryNum > 2) throw new Error('link try max num')
this._requestObj_listDetailLink = httpFetch(link)
const { headers: { location }, statusCode } = await this._requestObj_listDetailLink.promise
// console.log(headers)
if (statusCode > 400) return this.handleParseId(link, ++retryNum)
return location == null ? link : location
const url = location == null ? link : location
return this.regExps.listDetailLink.test(url)
? url.replace(this.regExps.listDetailLink, '$1')
: url.replace(this.regExps.listDetailLink2, '$1')
},
async getListDetail(id, page, tryNum = 0) { // 获取歌曲列表内的音乐
if (this._requestObj_listDetail) this._requestObj_listDetail.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
async getListId(id) {
let cookie
if (/###/.test(id)) {
const [url, token] = id.split('###')
id = url
this.cookie = `MUSIC_U=${token}`
cookie = `MUSIC_U=${token}`
}
if ((/[?&:/]/.test(id))) {
if (this.regExps.listDetailLink.test(id)) {
@ -79,6 +80,14 @@ export default {
}
// console.log(id)
}
return { id, cookie }
},
async getListDetail(rawId, page, tryNum = 0) { // 获取歌曲列表内的音乐
if (this._requestObj_listDetail) this._requestObj_listDetail.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
const { id, cookie } = await this.getListId(rawId)
if (cookie) this.cookie = cookie
this._requestObj_listDetail = httpFetch('https://music.163.com/api/linux/forward', {
method: 'post',
@ -290,7 +299,8 @@ export default {
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'wy' }))
},
getDetailPageUrl(id) {
async getDetailPageUrl(rawId) {
const { id } = await this.getListId(rawId)
return `https://music.163.com/#/playlist?id=${id}`
},
}

View File

@ -323,3 +323,10 @@ export const setTaskbarThumbnailClip = (clip) => {
export const setTaskbarThumbarButtons = (buttons) => {
rendererSend(NAMES.mainWindow.taskbar_set_thumbar_buttons, buttons)
}
export const onSystemThemeChange = callback => {
rendererOn(NAMES.mainWindow.system_theme_change, callback)
return () => {
rendererOff(callback)
}
}

View File

@ -20,10 +20,10 @@ div(:class="$style.download")
div.list-item(@click="handleDoubleClick($event, index)" @contextmenu="handleListItemRigthClick($event, index)"
:class="[{[$style.active]: playListIndex == index }, { selected: selectedIndex == index }, { active: selectedData.includes(item) }]")
div.list-item-cell.nobreak.center(style="width: 5%; padding-left: 3px; padding-right: 3px;" @click.stop) {{index + 1}}
div.list-item-cell.auto(:tips="item.name")
div.list-item-cell.auto(:aria-label="item.name")
span.select {{item.name}}
div.list-item-cell(style="width: 20%;") {{item.progress.progress}}%
div.list-item-cell(style="width: 22%;" :tips="item.statusText") {{item.statusText}}
div.list-item-cell(style="width: 22%;" :aria-label="item.statusText") {{item.statusText}}
div.list-item-cell(style="width: 10%;") {{item.metadata.type && item.metadata.type.toUpperCase()}}
div.list-item-cell(style="width: 13%; padding-left: 0; padding-right: 0;")
material-list-buttons(:index="index" :download-btn="false" :file-btn="item.status != downloadStatus.ERROR" remove-btn
@ -36,10 +36,10 @@ div(:class="$style.download")
<script>
import { mapGetters, mapActions, mapMutations } from 'vuex'
import { checkPath, openDirInExplorer, openUrl } from '@renderer/utils'
import { checkPath, openDirInExplorer, openUrl, getFontSizeWithScreen } from '@renderer/utils'
import musicSdk from '@renderer/utils/music'
import path from 'path'
import { windowSizeList } from '@renderer/core/share'
import { windowSizeList, isFullscreen } from '@renderer/core/share'
import { playInfo, playMusicInfo } from '@renderer/core/share/player'
import { downloadList, downloadStatus } from '@renderer/core/share/download'
@ -51,6 +51,7 @@ export default {
playInfo,
downloadList,
downloadStatus,
isFullscreen,
}
},
data() {
@ -181,7 +182,7 @@ export default {
]
},
listItemHeight() {
return parseInt(windowSizeList.find(item => item.id == this.setting.windowSizeId).fontSize) / 16 * 37
return Math.ceil((this.isFullscreen ? getFontSizeWithScreen() : parseInt(windowSizeList.find(item => item.id == this.setting.windowSizeId).fontSize)) * 2.3)
},
},
watch: {

View File

@ -6,7 +6,7 @@
</div>
<ul class="scroll" :class="$style.listsContent" ref="dom_lists_list">
<li :class="[$style.listsItem, { [$style.active]: item.id == tabId }, { [$style.clicked]: boardListData.rightClickItemIndex == index }]"
:tips="item.name" v-for="(item, index) in boardList" :key="item.id"
:aria-label="item.name" v-for="(item, index) in boardList" :key="item.id"
@click="handleToggleList(item.id)" @contextmenu="handleListsItemRigthClick($event, index)">
<span :class="$style.listsLabel">{{item.name}}</span>
</li>

View File

@ -50,10 +50,10 @@ div(:class="$style.search")
dl(:class="$style.noitemList" v-if="setting.search.isShowHistorySearch && historyList.length")
dt(:class="$style.noitemListTitle")
span {{$t('history_search')}}
span(:class="$style.historyClearBtn" @click="clearHistory" :tips="$t('history_clear')")
span(:class="$style.historyClearBtn" @click="clearHistory" :aria-label="$t('history_clear')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 512 512' space='preserve')
use(xlink:href='#icon-eraser')
dd(:class="$style.noitemListItem" v-for="(item, index) in historyList" @contextmenu="removeHistory(index)" :key="index + item" @click="handleNoitemSearch(item)" :tips="$t('history_remove')") {{item}}
dd(:class="$style.noitemListItem" v-for="(item, index) in historyList" @contextmenu="removeHistory(index)" :key="index + item" @click="handleNoitemSearch(item)" :aria-label="$t('history_remove')") {{item}}
div(v-else :class="$style.noitem_list")
p {{$t('search__welcome')}}
//- common-flow-btn(:show="isShowEditBtn && (searchSourceId == 'all' || assertApiSupport(searchSourceId))" :remove-btn="false" @btn-click="handleFlowBtnClick")

View File

@ -18,12 +18,12 @@ div(:class="$style.list")
:class="[{ [$style.active]: playerInfo.isPlayList && playerInfo.playIndex === index }, { selected: selectedIndex == index || rightClickSelectedIndex == index }, { active: selectedList.includes(item) }, { [$style.disabled]: !assertApiSupport(item.source) }]"
@contextmenu="handleListItemRightClick($event, index)")
div.list-item-cell.nobreak.center(style="flex: 0 0 5%; padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}}
div.list-item-cell.auto(:tips="item.name + (isShowSource ? ` - ${item.source}` : '')")
div.list-item-cell.auto(:aria-label="item.name + (isShowSource ? ` - ${item.source}` : '')")
span.select {{item.name}}
span(:class="[$style.labelSource, $style.noSelect]" v-if="isShowSource") {{item.source}}
div.list-item-cell(style="flex: 0 0 22%;" :tips="item.singer")
div.list-item-cell(style="flex: 0 0 22%;" :aria-label="item.singer")
span.select {{item.singer}}
div.list-item-cell(style="flex: 0 0 22%;" :tips="item.albumName")
div.list-item-cell(style="flex: 0 0 22%;" :aria-label="item.albumName")
span.select {{item.albumName}}
div.list-item-cell(style="flex: 0 0 9%;")
span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}}

View File

@ -1,5 +1,6 @@
import { computed, watch, ref, onBeforeUnmount } from '@renderer/utils/vueTools'
import { windowSizeList } from '@renderer/core/share'
import { windowSizeList, isFullscreen } from '@renderer/core/share'
import { getFontSizeWithScreen } from '@renderer/utils'
const useKeyEvent = ({ handleSelectAllData }) => {
const keyEvent = {
@ -48,7 +49,7 @@ export default ({ list, setting }) => {
let lastSelectIndex = -1
const listItemHeight = computed(() => {
return parseInt(windowSizeList.find(item => item.id == setting.value.windowSizeId).fontSize) / 16 * 37
return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : parseInt(windowSizeList.find(item => item.id == setting.value.windowSizeId).fontSize)) * 2.3)
})
const removeAllSelect = () => {

View File

@ -2,31 +2,31 @@
<div :class="$style.lists" ref="dom_lists">
<div :class="$style.listHeader">
<h2 :class="$style.listsTitle">{{$t('my_list')}}</h2>
<button :class="$style.listsAdd" @click="handleShowNewList" :tips="$t('lists__new_list_btn')">
<button :class="$style.listsAdd" @click="handleShowNewList" :aria-label="$t('lists__new_list_btn')">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="70%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-list-add"></use>
</svg>
</button>
</div>
<ul class="scroll" :class="[$style.listsContent, { [$style.sortable]: keyEvent.isModDown }]" ref="dom_lists_list">
<li class="default-list" :class="[$style.listsItem, {[$style.active]: defaultList.id == listId}]" :tips="defaultList.name"
<li class="default-list" :class="[$style.listsItem, {[$style.active]: defaultList.id == listId}]" :aria-label="defaultList.name" :aria-selected="defaultList.id == listId"
@contextmenu="handleListsItemRigthClick($event, -2)" @click="handleListToggle(defaultList.id)"
>
<span :class="$style.listsLabel">{{defaultList.name}}</span>
</li>
<li class="default-list" :class="[$style.listsItem, {[$style.active]: loveList.id == listId}]" :tips="loveList.name"
<li class="default-list" :class="[$style.listsItem, {[$style.active]: loveList.id == listId}]" :aria-label="loveList.name" :aria-selected="loveList.id == listId"
@contextmenu="handleListsItemRigthClick($event, -1)" @click="handleListToggle(loveList.id)">
<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"
@contextmenu="handleListsItemRigthClick($event, index)" :tips="item.name" v-for="(item, index) in userLists" :key="item.id"
@contextmenu="handleListsItemRigthClick($event, index)" :aria-label="item.name" v-for="(item, index) in userLists" :key="item.id" :aria-selected="defaultList.id == listId"
>
<span :class="$style.listsLabel" @click="handleListToggle(item.id, index + 2)">{{item.name}}</span>
<input class="key-bind" :class="$style.listsInput" @contextmenu.stop type="text"
@keyup.enter="handleListsSave(index, $event)" @blur="handleListsSave(index, $event)" :value="item.name" :placeholder="item.name"/>
</li>
<transition enter-active-class="animated-fast slideInLeft" leave-active-class="animated-fast fadeOut" @after-leave="handleListsNewAfterLeave">
<transition enter-active-class="animated-fast slideInLeft" leave-active-class="animated-fast fadeOut" @after-leave="handleListsNewAfterLeave" @after-enter="$refs.dom_listsNewInput.focus()">
<li :class="[$style.listsItem, $style.listsNew, listsData.isNewLeave ? $style.newLeave : null]" v-if="listsData.isShowNewList">
<input class="key-bind" :class="$style.listsInput" @contextmenu.stop ref="dom_listsNewInput" type="text" @keyup.enter="handleListsCreate"
@blur="handleListsCreate" :placeholder="$t('lists__new_list_input')"/>
@ -263,9 +263,6 @@ export default {
},
handleShowNewList() {
this.listsData.isShowNewList = true
this.$nextTick(() => {
this.$refs.dom_listsNewInput.focus()
})
},
handleListsNewAfterLeave() {
this.listsData.isNewLeave = false
@ -406,15 +403,15 @@ export default {
}
return false
},
openSourceDetailPage(index) {
async 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)
} else if (musicSdk[source].songList?.getDetailPageUrl) {
url = await musicSdk[source].songList.getDetailPageUrl(sourceListId)
}
if (!url) return
openUrl(url)

View File

@ -3,12 +3,12 @@
<!-- <div class="scroll" :class="$style.toc">
<ul :class="$style.tocList">
<li :class="$style.tocListItem" v-for="h2 in toc.list" :key="h2.id">
<h2 :class="[$style.tocH2, toc.activeId == h2.id ? $style.active : null]" :tips="h2.title">
<h2 :class="[$style.tocH2, toc.activeId == h2.id ? $style.active : null]" :aria-label="h2.title">
<a :href="'#' + h2.id" @click.stop="toc.activeId = h2.id">{{h2.title}}</a>
</h2>
<ul :class="$style.tocList" v-if="h2.children.length">
<li :class="$style.tocSubListItem" v-for="h3 in h2.children" :key="h3.id">
<h3 :class="[$style.tocH3, toc.activeId == h3.id ? $style.active : null]" :tips="h3.title">
<h3 :class="[$style.tocH3, toc.activeId == h3.id ? $style.active : null]" :aria-label="h3.title">
<a :href="'#' + h3.id" @click.stop="toc.activeId = h3.id">{{h3.title}}</a>
</h3>
</li>
@ -20,6 +20,7 @@
<dl ref="dom_setting_list">
<SettingBasic />
<SettingPlay />
<SettingPlayDetail />
<SettingDesktopLyric />
<SettingSearch />
<SettingList />
@ -43,6 +44,7 @@ import { currentStting } from './setting'
import SettingBasic from './components/SettingBasic'
import SettingPlay from './components/SettingPlay'
import SettingPlayDetail from './components/SettingPlayDetail'
import SettingDesktopLyric from './components/SettingDesktopLyric'
import SettingSearch from './components/SettingSearch'
import SettingList from './components/SettingList'
@ -61,6 +63,7 @@ export default {
components: {
SettingBasic,
SettingPlay,
SettingPlayDetail,
SettingDesktopLyric,
SettingSearch,
SettingList,
@ -94,6 +97,9 @@ export default {
watch(() => setting.value.player.mediaDeviceId, val => {
currentStting.value.player.mediaDeviceId = val
})
watch(() => setting.value.playDetail.style.fontSize, val => {
currentStting.value.playDetail.style.fontSize = val
})
watch(() => setting.value.player.isMute, val => {
currentStting.value.player.isMute = val
})
@ -109,6 +115,9 @@ export default {
watch(() => setting.value.player.togglePlayMethod, val => {
currentStting.value.player.togglePlayMethod = val
})
watch(() => setting.value.player.audioVisualization, val => {
currentStting.value.player.audioVisualization = val
})
},
data() {
return {

View File

@ -3,15 +3,15 @@ dt#about {{$t('setting__about')}}
dd
p.small
| 本软件完全免费代码已开源开源地址
span.hover.underline(:tips="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop#readme')") https://github.com/lyswhut/lx-music-desktop
span.hover.underline(:aria-label="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop#readme')") https://github.com/lyswhut/lx-music-desktop
p.small
| 最新版网盘下载地址网盘内有WindowsMAC版
span.hover.underline(:tips="$t('setting__click_open')" @click="openUrl('https://www.lanzoui.com/b0bf2cfa/')") 网盘地址
span.hover.underline(:aria-label="$t('setting__click_open')" @click="openUrl('https://www.lanzoui.com/b0bf2cfa/')") 网盘地址
| &nbsp;&nbsp;密码
span.hover(:tips="$t('setting__click_copy')" @click="clipboardWriteText('glqw')") glqw
span.hover(:aria-label="$t('setting__click_copy')" @click="clipboardWriteText('glqw')") glqw
p.small
| 软件的常见问题可转至
span.hover.underline(:tips="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md')") 常见问题
span.hover.underline(:aria-label="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md')") 常见问题
p.small
strong 本软件没有客服
| 但我们整理了一些常见的使用问题
@ -19,11 +19,11 @@ dd
| 地阅读常见问题后
p.small
| 仍有问题可加企鹅群&nbsp;
span.hover(:tips="$t('setting__click_open')" @click="openUrl('https://jq.qq.com/?_wv=1027&k=51ECeq2')") 830125506
span.hover(:aria-label="$t('setting__click_open')" @click="openUrl('https://jq.qq.com/?_wv=1027&k=51ECeq2')") 830125506
| &nbsp;反馈
strong (为免满人无事勿加入群先看群公告)
| 或到 GitHub 提交&nbsp;
span.hover.underline(:tips="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop/issues')") issue
span.hover.underline(:aria-label="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop/issues')") issue
br
p.small
@ -32,7 +32,7 @@ dd
|
p
| 可以加入测试企鹅群&nbsp;
span.hover(:tips="$t('setting__click_open')" @click="openUrl('https://qm.qq.com/cgi-bin/qm/qr?k=zR6aYosQoKb07g4FGFZdO9n9zL1dhFpE&jump_from=webapi')") 768786588
span.hover(:aria-label="$t('setting__click_open')" @click="openUrl('https://qm.qq.com/cgi-bin/qm/qr?k=zR6aYosQoKb07g4FGFZdO9n9zL1dhFpE&jump_from=webapi')") 768786588
| &nbsp;注意测试版的功可能会不稳定
strong 打算潜水的勿加
@ -43,7 +43,7 @@ dd
| 你已签署本软件的&nbsp;
base-btn(min @click="handleShowPact") 许可协议
| 协议的在线版本在&nbsp;
strong.hover.underline(:tips="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop#%E9%A1%B9%E7%9B%AE%E5%8D%8F%E8%AE%AE')") 这里
strong.hover.underline(:aria-label="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop#%E9%A1%B9%E7%9B%AE%E5%8D%8F%E8%AE%AE')") 这里
| &nbsp;
br

View File

@ -4,9 +4,15 @@ dd
h3#basic_theme {{$t('setting__basic_theme')}}
div
ul(:class="$style.theme")
li(v-for="theme in themes.list" :key="theme.id" :tips="$t('theme_' + theme.className)" @click="currentStting.themeId = theme.id" :class="[theme.className, {[$style.active]: themes.active == theme.id}]")
span
li(v-for="theme in themes.list" :key="theme.id" :aria-label="$t('theme_' + theme.className)" @click="currentStting.theme.id = theme.id" :class="[theme.className, {[$style.active]: themes.active == theme.id}]")
div(:class="$style.bg")
label {{$t('theme_' + theme.className)}}
li(:aria-label="$t('theme_auto_tip')" @click="handleSetThemeAuto" @contextmenu="isShowThemeSelectorModal = true" :class="[$style.auto, themeClassName, {[$style.active]: themes.active == 'auto'}]")
div(:class="$style.bg")
div(:class="$style.bgContent")
div(:class="[$style.light, themes.lightTheme.className]")
div(:class="[$style.dark, themes.darkTheme.className]")
label {{$t('theme_auto')}}
dd
div
@ -19,7 +25,7 @@ dd
p.gap-top
base-btn.btn(min @click="isShowPlayTimeoutModal = true") {{$t('setting__play_timeout')}} {{ timeLabel ? ` (${timeLabel})` : '' }}
dd(:tips="$t('setting__basic_source_title')")
dd(:aria-label="$t('setting__basic_source_title')")
h3#basic_source {{$t('setting__basic_source')}}
div
.gap-top(v-for="item in apiSources" :key="item.id")
@ -28,19 +34,19 @@ dd(:tips="$t('setting__basic_source_title')")
p.gap-top
base-btn.btn(min @click="isShowUserApiModal = true") {{$t('setting__basic_source_user_api_btn')}}
dd(:tips="$t('setting__basic_window_size_title')")
dd(:aria-label="$t('setting__basic_window_size_title')")
h3#basic_window_size {{$t('setting__basic_window_size')}}
div
base-checkbox.gap-left(v-for="(item, index) in windowSizeList" :id="`setting_window_size_${item.id}`" name="setting_window_size"
need v-model="currentStting.windowSizeId" :value="item.id" :label="$t('setting__basic_window_size_' + item.name)" :key="item.id")
need v-model="currentStting.windowSizeId" :disabled="isFullscreen" :value="item.id" :label="$t('setting__basic_window_size_' + item.name)" :key="item.id")
dd(:tips="$t('setting__basic_lang_title')")
dd(:aria-label="$t('setting__basic_lang_title')")
h3#basic_lang {{$t('setting__basic_lang')}}
div
base-checkbox.gap-left(v-for="item in langList" :key="item.locale" :id="`setting_lang_${item.locale}`" name="setting_lang"
need v-model="currentStting.langId" :value="item.locale" :label="item.name")
dd(:tips="$t('setting__basic_sourcename_title')")
dd(:aria-label="$t('setting__basic_sourcename_title')")
h3#basic_sourcename {{$t('setting__basic_sourcename')}}
div
base-checkbox.gap-left(v-for="item in sourceNameTypes" :key="item.id" :id="`setting_abasic_sourcename_${item.id}`"
@ -55,20 +61,23 @@ dd
div
base-selection.gap-teft(:list="fontList" v-model="currentStting.font" item-key="id" item-name="label")
ThemeSelectorModal(v-model="isShowThemeSelectorModal")
play-timeout-modal(v-model="isShowPlayTimeoutModal")
user-api-modal(v-model="isShowUserApiModal")
</template>
<script>
import { computed, ref, useI18n, watch } from '@renderer/utils/vueTools'
import { themes as themeList, windowSizeList, apiSource, userApi } from '@renderer/core/share'
import { computed, ref, useI18n, watch, useRefGetter } from '@renderer/utils/vueTools'
import { themes as themeList, windowSizeList, apiSource, userApi, isFullscreen } from '@renderer/core/share'
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 { dialog } from '@renderer/plugins/Dialog'
import ThemeSelectorModal from './ThemeSelectorModal'
import PlayTimeoutModal from './PlayTimeoutModal'
import UserApiModal from './UserApiModal'
@ -76,19 +85,34 @@ import UserApiModal from './UserApiModal'
export default {
name: 'SettingBasic',
components: {
ThemeSelectorModal,
PlayTimeoutModal,
UserApiModal,
},
setup() {
const { t, locale } = useI18n()
const themeClassName = useRefGetter('theme')
const themes = computed(() => {
return {
active: currentStting.value.themeId,
active: currentStting.value.theme.id,
lightTheme: themeList.find(t => t.id == currentStting.value.theme.lightId) ?? themeList[0],
darkTheme: themeList.find(t => t.id == currentStting.value.theme.darkId) ?? themeList[0],
list: themeList,
}
})
const isShowThemeSelectorModal = ref(false)
const handleSetThemeAuto = () => {
if (currentStting.value.theme.id == 'auto') return
currentStting.value.theme.id = 'auto'
if (window.localStorage.getItem('theme-auto-tip') != 'true') {
window.localStorage.setItem('theme-auto-tip', 'true')
dialog({
message: t('setting__basic_theme_auto_tip'),
confirmButtonText: t('ok'),
})
}
}
watch(() => currentStting.value.apiSource, visible => {
apiSource.value = visible
@ -161,6 +185,9 @@ export default {
return {
currentStting,
themes,
themeClassName,
isShowThemeSelectorModal,
handleSetThemeAuto,
isShowPlayTimeoutModal,
timeLabel,
apiSources,
@ -170,6 +197,7 @@ export default {
sourceNameTypes,
controlBtnPositionList,
fontList,
isFullscreen,
}
},
}
@ -199,7 +227,7 @@ export default {
margin-right: 0;
}
span {
.bg {
display: block;
width: 36px;
height: 36px;
@ -229,7 +257,7 @@ export default {
each(@themes, {
&:global(.@{value}) {
span {
.bg {
&:after {
background-color: ~'@{color-@{value}-theme}';
background-image: ~'@{color-@{value}-theme-bgimg}';
@ -237,6 +265,68 @@ export default {
}
}
})
&.auto {
>.bg {
&:after {
content: none;
}
}
.bgContent {
position: relative;
height: 100%;
overflow: hidden;
border-radius: 5px;
}
.light, .dark {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
&:after {
display: block;
content: ' ';
width: 100%;
height: 100%;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
}
.light {
&:after {
clip-path: polygon(0 0, 100% 0, 0 100%);
}
each(@themes, {
&:global(.@{value}) {
svg {
fill: ~'@{color-@{value}-theme}';
}
&:after {
background-color: ~'@{color-@{value}-theme}';
background-image: ~'@{color-@{value}-theme-bgimg}';
}
}
})
}
.dark {
&:after {
clip-path: polygon(0 100%, 100% 0, 100% 100%);
}
each(@themes, {
&:global(.@{value}) {
svg {
fill: ~'@{color-@{value}-theme}';
}
&:after {
background-color: ~'@{color-@{value}-theme}';
background-image: ~'@{color-@{value}-theme-bgimg}';
}
}
})
}
}
}
}
@ -247,7 +337,7 @@ each(@themes, {
&.active {
&:global(.@{value}) {
color: ~'@{color-@{value}-theme}';
span {
.bg {
border-color: ~'@{color-@{value}-theme}';
}
}

View File

@ -2,22 +2,22 @@
dt#download {{$t('setting__download')}}
dd
base-checkbox(id="setting_download_enable" v-model="currentStting.download.enable" :label="$t('setting__download_enable')")
dd(:tips="$t('setting__download_path_title')")
dd(:aria-label="$t('setting__download_path_title')")
h3#download_path {{$t('setting__download_path')}}
div
p
| {{$t('setting__download_path_label')}}
span.auto-hidden.hover(:class="$style.savePath" @click="openDirInExplorer(currentStting.download.savePath)" :tips="$t('setting__download_path_open_label')") {{currentStting.download.savePath}}
span.auto-hidden.hover(:class="$style.savePath" @click="openDirInExplorer(currentStting.download.savePath)" :aria-label="$t('setting__download_path_open_label')") {{currentStting.download.savePath}}
p
base-btn.btn(min @click="handleChangeSavePath") {{$t('setting__download_path_change_btn')}}
dd
h3#download_use_other_source
| {{$t('setting__download_use_other_source')}}
svg-icon(class="help-icon" name="help-circle-outline" :tips="$t('setting__download_use_other_source_tip')")
svg-icon(class="help-icon" name="help-circle-outline" :aria-label="$t('setting__download_use_other_source_tip')")
div
base-checkbox(id="setting_download_isUseOtherSource" v-model="currentStting.download.isUseOtherSource" :label="$t('setting__is_enable')")
div
dd(:tips="$t('setting__download_name_title')")
dd(:aria-label="$t('setting__download_name_title')")
h3#download_name {{$t('setting__download_name')}}
div
base-checkbox.gap-left(:id="`setting_download_musicName_${item.value}`" name="setting_download_musicName" :value="item.value" :key="item.value" need
@ -28,7 +28,7 @@ dd
base-checkbox(id="setting_download_isEmbedPic" v-model="currentStting.download.isEmbedPic" :label="$t('setting__download_embed_pic')")
.gap-top
base-checkbox(id="setting_download_isEmbedLyric" v-model="currentStting.download.isEmbedLyric" :label="$t('setting__download_embed_lyric')")
dd(:tips="$t('setting__download_lyric_title')")
dd(:aria-label="$t('setting__download_lyric_title')")
h3#download_lyric {{$t('setting__download_lyric')}}
div
base-checkbox(id="setting_download_isDownloadLrc" v-model="currentStting.download.isDownloadLrc" :label="$t('setting__is_enable')")

View File

@ -7,7 +7,7 @@ dd
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')")
dd(:aria-label="$t('setting__basic_sourcename_title')")
h3#list_addMusicLocationType {{$t('setting__list_add_music_location_type')}}
div
base-checkbox.gap-left(id="setting_list_add_music_location_type_top"

View File

@ -8,7 +8,7 @@ dd
dd
h3#other_resource_cache
| {{$t('setting__other_resource_cache')}}
svg-icon(class="help-icon" name="help-circle-outline" :tips="$t('setting__other_resource_cache_tip')")
svg-icon(class="help-icon" name="help-circle-outline" :aria-label="$t('setting__other_resource_cache_tip')")
div
p
| {{$t('setting__other_resource_cache_label')}}
@ -18,7 +18,7 @@ dd
dd
h3#other_play_list_cache
| {{$t('setting__other_play_list_cache')}}
svg-icon(class="help-icon" name="help-circle-outline" :tips="$t('setting__other_play_list_cache_tip')")
svg-icon(class="help-icon" name="help-circle-outline" :aria-label="$t('setting__other_play_list_cache_tip')")
div
base-btn.btn(min :disabled="isDisabledListCacheClear" @click="clearListCache") {{$t('setting__other_play_list_cache_clear_btn')}}

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