Compare commits

..

48 Commits

Author SHA1 Message Date
lyswhut
c24d1d1e41 发布v2.3.0 2023-06-29 23:48:02 +08:00
lyswhut
1b2daef90c 更新依赖 2023-06-29 23:30:01 +08:00
lyswhut
6284fd8024 更新依赖 2023-06-27 18:58:51 +08:00
lyswhut
763874ac55 更新依赖 2023-06-26 10:53:46 +08:00
lyswhut
4defaa103e 修复linux arm64下无法启动的问题 2023-06-23 10:28:48 +08:00
Folltoshe
181ac937d2 修复mg歌单搜索 (#1414) 2023-06-21 12:30:06 +08:00
lyswhut
f6884524e2 更新依赖 2023-06-21 12:16:06 +08:00
lyswhut
23f118fb1a 过滤嵌入、下载的翻译、罗马音歌词时间标签(#1358) 2023-06-21 11:59:38 +08:00
lyswhut
a3b8f31b1e 修复tx翻译歌词解析丢失的问题 2023-06-21 10:58:04 +08:00
lyswhut
c6fb2232d0 修复mg歌单搜索歌单播放数量显示问题 2023-06-17 20:05:03 +08:00
lyswhut
ef411ddb47 更新依赖 2023-06-16 11:23:11 +08:00
lyswhut
7aed2828bb 修复添加单首歌曲弹窗列表创建按钮无法取消的问题 2023-06-16 11:23:08 +08:00
lyswhut
560445a518 添加提示 2023-06-14 11:46:50 +08:00
Folltoshe
3549ed2da0 修复wy热门评论显示 (#1411) 2023-06-14 11:25:10 +08:00
lyswhut
ec9f86c450 修复构建 2023-06-10 23:44:42 +08:00
lyswhut
c6e829be4f 更新依赖 2023-06-10 20:40:22 +08:00
lyswhut
5ef84916bb 修复初始化列表信息可能导致崩溃的问题 2023-06-10 20:34:25 +08:00
Folltoshe
94403be9a8 预留歌手API (#1407)
* 更新歌手API

---------

Co-authored-by: lyswhut <lyswhut@qq.com>
Co-authored-by: 彭狸花喵 <helloplhm-qwq@outlook.com>
2023-06-10 20:33:21 +08:00
lyswhut
6403b8ce38 code lint 2023-06-09 09:55:54 +08:00
lyswhut
3f34fa26bf 更新依赖 2023-06-09 09:46:35 +08:00
彭狸花喵
b1a6c6b59c 更新kg最新评论接口(cmtlist->newest) & kg源歌手相关 (#1405)
* 修复wy源热搜词失效

* 更新kg最新评论接口(cmtlist->newest) & kg源歌手相关

---------

Co-authored-by: Folltoshe <folltoshe@foxmail.com>
2023-06-09 09:45:01 +08:00
lyswhut
76981a59d4 更新默认设置 2023-06-08 09:57:15 +08:00
Folltoshe
d93fbd699d 修复wy源热搜词 (#1403)
* 修复wy源热搜词失效

* 添加更新日志

---------

Co-authored-by: lyswhut <lyswhut@qq.com>
2023-06-08 09:50:16 +08:00
lyswhut
fd2dca0769 移除桌面歌词窗口透明边距 2023-06-05 23:12:43 +08:00
lyswhut
5097102ca1 修复Deepin 20下启用桌面歌词时可能会导致桌面卡死的问题 2023-06-05 22:36:00 +08:00
lyswhut
fa3fd8d8a4 Windows、MacOS平台下的字体列表改用原生方式获取 2023-06-05 15:05:03 +08:00
lyswhut
5582e78315 更新版本号 2023-06-04 13:13:02 +08:00
lyswhut
b7fd32067f 优化升降调性能占用 2023-06-04 12:53:46 +08:00
彭狸花喵
2a2fdc57bd 修复tx热门评论昵称被错误切割的问题 (#1397)
* 修复tx热门评论昵称被错误切割的问题

* fix

---------

Co-authored-by: Folltoshe <folltoshe@foxmail.com>
2023-06-04 12:14:34 +08:00
lyswhut
025739e35c 优化&更新依赖 2023-06-02 12:27:41 +08:00
lyswhut
c8f6153831 移除升降调预设 2023-06-01 14:35:46 +08:00
lyswhut
3c02e3dddc 更新版本号 2023-05-31 21:41:22 +08:00
lyswhut
40997c34ff 添加音调升降调节 2023-05-31 21:40:25 +08:00
lyswhut
677c3f16e4 播放速率设置面板新增是否音调补偿设置 2023-05-31 20:26:19 +08:00
lyswhut
0adb4f5ae5 修复改变播放速率时会导致歌词报错的问题 2023-05-31 09:07:35 +08:00
lyswhut
cbbcf914ad 更新依赖 2023-05-30 14:43:44 +08:00
lyswhut
d68a29a5a2 更新依赖 2023-05-25 14:30:20 +08:00
lyswhut
368eeed0d0 修复不同数据类型的列表id合并问题 2023-05-23 14:33:36 +08:00
lyswhut
a522b78496 更新依赖 2023-05-23 11:56:54 +08:00
lyswhut
dab81116e9 修复播放某些在线音频会没有声音的问题 2023-05-22 15:37:28 +08:00
lyswhut
b7f355bd1a 更新翻译 2023-05-21 10:34:56 +08:00
lyswhut
dbeaebaee3 更新版本号 2023-05-19 22:06:48 +08:00
lyswhut
fe94a1eac9 更新依赖,code lint 2023-05-19 22:01:18 +08:00
lyswhut
bd5107bbaf 修复下载时出现302错误的问题 2023-05-18 16:16:58 +08:00
lyswhut
8018d9b418 修复因插入数字类型的ID导致其意外在末尾追加 .0 导致列表数据异常的问题 2023-05-18 15:22:45 +08:00
lyswhut
0454d300ec 修改code判断方式 2023-05-16 17:35:42 +08:00
lyswhut
1e5c9ebc91 更新默认主题设置 2023-05-15 10:33:15 +08:00
lyswhut
5c7e9728e6 修复列表名翻译显示 2023-05-13 12:39:25 +08:00
144 changed files with 6297 additions and 4717 deletions

119
.eslintrc
View File

@@ -1,119 +0,0 @@
{
"root": true,
"extends": [
"standard"
],
"plugins": [
"html"
],
"parser": "@babel/eslint-parser",
"parserOptions": {
// "requireConfigFile": false
},
"rules": {
"no-new": "off",
"camelcase": "off",
"no-return-assign": "off",
"space-before-function-paren": ["error", "never"],
"no-var": "error",
"no-fallthrough": "off",
"prefer-promise-reject-errors": "off",
"eqeqeq": "off",
"no-multiple-empty-lines": [1, {"max": 2}],
"comma-dangle": [2, "always-multiline"],
"standard/no-callback-literal": "off",
"prefer-const": "off",
"no-labels": "off",
"node/no-callback-literal": "off"
},
"ignorePatterns": ["vendors", "*.min.js", "dist"],
"overrides": [
{
"files": [ "*.vue" ],
"rules": {
"no-new": "off",
"camelcase": "off",
"no-return-assign": "off",
"space-before-function-paren": ["error", "never"],
"no-var": "error",
"no-fallthrough": "off",
"prefer-promise-reject-errors": "off",
"eqeqeq": "off",
"no-multiple-empty-lines": [1, {"max": 2}],
"comma-dangle": [2, "always-multiline"],
"standard/no-callback-literal": "off",
"prefer-const": "off",
"no-labels": "off",
"node/no-callback-literal": "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/ban-ts-comment": "off",
"vue/max-attributes-per-line": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/use-v-on-exact": "off",
"@typescript-eslint/restrict-template-expressions": "off",
// "no-undef": "off"
},
"parser": "vue-eslint-parser",
"extends": [
"plugin:vue/base",
// "plugin:vue/strongly-recommended"
"plugin:vue/vue3-recommended",
"standard-with-typescript",
],
"parserOptions": {
"sourceType": "module",
"parser": {
// Script parser for `<script>`
"js": "@typescript-eslint/parser",
// Script parser for `<script lang="ts">`
"ts": "@typescript-eslint/parser"
},
"extraFileExtensions": [".vue"]
}
},
{
"files": [ "*.ts" ],
"rules": {
"no-new": "off",
"camelcase": "off",
"no-return-assign": "off",
"space-before-function-paren": ["error", "never"],
"no-var": "error",
"no-fallthrough": "off",
"prefer-promise-reject-errors": "off",
"eqeqeq": "off",
"no-multiple-empty-lines": [1, {"max": 2}],
"comma-dangle": [2, "always-multiline"],
"standard/no-callback-literal": "off",
"prefer-const": "off",
"no-labels": "off",
"node/no-callback-literal": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/restrict-template-expressions": [1, {
"allowBoolean": true
}],
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/return-await": "off",
"@typescript-eslint/ban-ts-comment": "off",
"multiline-ternary": "off",
"@typescript-eslint/comma-dangle": "off",
},
"parser": "@typescript-eslint/parser",
"extends": [
"standard-with-typescript"
],
"parserOptions": {
"project": "./src/**/tsconfig.json"
}
}
]
}

91
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,91 @@
const javascript = {
'no-new': 'off',
camelcase: 'off',
'no-return-assign': 'off',
'space-before-function-paren': ['error', 'never'],
'no-var': 'error',
'no-fallthrough': 'off',
'prefer-promise-reject-errors': 'off',
eqeqeq: 'off',
'no-multiple-empty-lines': [1, { max: 2 }],
'comma-dangle': [2, 'always-multiline'],
'standard/no-callback-literal': 'off',
'prefer-const': 'off',
'no-labels': 'off',
'node/no-callback-literal': 'off',
'multiline-ternary': 'off',
}
const typescript = {
...javascript,
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/restrict-template-expressions': [1, {
allowBoolean: true,
}],
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/return-await': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/comma-dangle': 'off',
}
const vue = {
...typescript,
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/use-v-on-exact': 'off',
}
delete vue['@typescript-eslint/restrict-template-expressions']
module.exports = {
root: true,
extends: [
'standard',
],
plugins: [
'html',
],
parser: '@babel/eslint-parser',
parserOptions: {
// "requireConfigFile": false
},
rules: javascript,
ignorePatterns: ['vendors', '*.min.js', 'dist'],
overrides: [
{
files: ['*.vue'],
rules: vue,
parser: 'vue-eslint-parser',
extends: [
'plugin:vue/base',
// "plugin:vue/strongly-recommended"
'plugin:vue/vue3-recommended',
'plugin:vue-pug/vue3-recommended',
'standard-with-typescript',
],
parserOptions: {
sourceType: 'module',
parser: {
// Script parser for `<script>`
js: '@typescript-eslint/parser',
// Script parser for `<script lang="ts">`
ts: '@typescript-eslint/parser',
},
extraFileExtensions: ['.vue'],
},
},
{
files: ['*.ts'],
rules: typescript,
parser: '@typescript-eslint/parser',
extends: [
'standard-with-typescript',
],
parserOptions: {
project: './src/**/tsconfig.json',
},
},
],
}

View File

@@ -5,15 +5,25 @@ module.exports = {
'chalk',
'del',
'comlink',
'vue',
'eslint-plugin-n',
'@types/ws',
],
// target: 'newest',
// filter: [
// 'electron-builder',
// 'electron-updater',
// ],
// target: 'patch',
// filter: [
// 'vue',
// ],
// target: 'minor',
// filter: [
// 'eslint-plugin-n',
// 'electron',
// ],
}

View File

@@ -6,6 +6,37 @@ 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/).
## [2.3.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.2...v2.3.0) - 2023-06-29
### 新增
- 新增音效设置实验性功能支持10段均衡器设置、内置的一些环境混响音效、音调升降调节、3D立体环绕音效由于升降调需要实时处理音频数据这会导致额外的CPU占用已知问题如果CPU资源不够时将处理导致任务堆积而出现声音异常这时需要暂停播放一段时间等堆积的任务处理完毕再播放
- 播放速率设置面板新增是否音调补偿设置,在调整播放速率后,可以选择是否启用音调补偿,默认启用
### 优化
- Windows、MacOS平台下的字体列表改用原生方式获取现在Windows平台下能显示当前已安装的更多类型字体了MacOS平台未测可用性未知
- 移除桌面歌词窗口透明边距在Linux下的桌面歌词可以完全拖到贴合屏幕边缘了
- 过滤嵌入、下载的翻译、罗马音歌词时间标签,与主歌词时间不匹配的歌词将被丢弃,防止出现原歌词与翻译歌词顺序错乱的问题(#1358
### 修复
- 修复列表名翻译显示
- 修复因插入数字类型的ID导致其意外在末尾追加 .0 导致列表数据异常的问题,同时也可能导致同步数据丢失的问题(要完全修复这个问题还需要同时将移动端、同步服务更新到最新版本)
- 修复下载时出现302错误的问题
- 修复播放某些在线音频会没有声音的问题
- 修复改变播放速率时会导致歌词报错的问题
- 修复tx热门评论昵称被错误切割的问题 (#1397, By: @helloplhm-qwq, @Folltoshe)
- 修复wy源热搜词失效的问题#1401, @Folltoshe
- 修复Deepin 20下启用桌面歌词时可能会导致桌面卡死的问题#1288
- 修复添加单首歌曲弹窗列表创建按钮无法取消的问题
- 修复mg歌单搜索歌单播放数量显示问题
- 修复tx翻译歌词解析丢失的问题更新版本后需手动清理一次歌词缓存
### 其他
- 更新 electron 到 v22.3.15
## [2.2.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.1...v2.2.2) - 2023-05-01
### 修复

View File

@@ -15,6 +15,7 @@ module.exports = {
externals: {
'font-list': 'font-list',
'better-sqlite3': 'better-sqlite3',
'electron-font-manager': 'electron-font-manager',
bufferutil: 'bufferutil',
'utf-8-validate': 'utf-8-validate',
'qrc_decode.node': isDev ? path.join(__dirname, '../../build/Release/qrc_decode.node') : path.join('../build/Release/qrc_decode.node'),

View File

@@ -17,6 +17,7 @@ const { Worker, isMainThread, parentPort } = require('worker_threads')
function build() {
console.time('build')
del.sync(['dist/**', 'build/**'])
const spinners = new Spinnies({ color: 'blue' })
@@ -36,6 +37,7 @@ function build() {
process.stdout.write('\x1B[2J\x1B[0f')
console.log(`\n\n${results}`)
console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`)
console.timeEnd('build')
process.exit()
}

View File

@@ -20,7 +20,7 @@ module.exports = {
type: 'commonjs2',
},
path: path.join(__dirname, '../../dist'),
publicPath: 'auto',
publicPath: '',
},
resolve: {
alias: {

View File

@@ -12,7 +12,7 @@ module.exports = {
type: 'commonjs2',
},
path: path.join(__dirname, '../../dist'),
publicPath: 'auto',
publicPath: '',
},
resolve: {
alias: {

View File

@@ -20,7 +20,7 @@ module.exports = {
type: 'commonjs2',
},
path: path.join(__dirname, '../../dist'),
publicPath: 'auto',
publicPath: '',
},
resolve: {
alias: {
@@ -49,6 +49,12 @@ module.exports = {
appendTsSuffixTo: [/\.vue$/],
},
},
parser: {
worker: [
'*audioContext.audioWorklet.addModule()',
'...',
],
},
},
{
test: /\.node$/,

View File

@@ -9,5 +9,10 @@
"@common/*": ["src/common/*"],
}
},
"vueCompilerOptions": {
"plugins": [
"@vue/language-plugin-pug"
]
},
"exclude": ["node_modules", "build", "dist"]
}

7020
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": "2.3.0-beta.3",
"version": "2.3.0",
"description": "一个免费的音乐查找助手",
"main": "./dist/main.js",
"productName": "lx-music-desktop",
@@ -97,6 +97,9 @@
"node_modules/better-sqlite3/lib",
"node_modules/better-sqlite3/package.json",
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
"node_modules/electron-font-manager/index.js",
"node_modules/electron-font-manager/package.json",
"node_modules/electron-font-manager/build/Release/font_manager.node",
"node_modules/node-gyp-build",
"node_modules/bufferutil",
"node_modules/utf-8-validate",
@@ -205,83 +208,88 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.21.8",
"@babel/eslint-parser": "^7.21.8",
"@babel/core": "^7.22.5",
"@babel/eslint-parser": "^7.22.5",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-umd": "^7.18.6",
"@babel/plugin-transform-runtime": "^7.21.4",
"@babel/preset-env": "^7.21.5",
"@babel/preset-typescript": "^7.21.5",
"@babel/plugin-transform-modules-umd": "^7.22.5",
"@babel/plugin-transform-runtime": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@tsconfig/recommended": "^1.0.2",
"@types/better-sqlite3": "^7.6.4",
"@types/needle": "^3.2.0",
"@types/tunnel": "^0.0.3",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"@volar/vue-language-plugin-pug": "^1.6.4",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@volar/vue-language-plugin-pug": "^1.6.5",
"@vue/language-plugin-pug": "^1.8.3",
"babel-loader": "^9.1.2",
"browserslist": "^4.21.5",
"browserslist": "^4.21.9",
"chalk": "^4.1.2",
"changelog-parser": "^3.0.1",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.30.2",
"core-js": "^3.31.0",
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^5.0.0",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1",
"del": "^6.1.1",
"electron": "^22.3.8",
"electron-builder": "^24.3.0",
"electron": "^22.3.15",
"electron-builder": "^24.5.1",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"electron-to-chromium": "^1.4.385",
"electron-updater": "^6.1.0",
"eslint": "^8.40.0",
"eslint-config-standard": "^17.0.0",
"eslint-config-standard-with-typescript": "^34.0.1",
"electron-to-chromium": "^1.4.445",
"electron-updater": "^6.1.2",
"eslint": "^8.43.0",
"eslint-config-standard": "^17.1.0",
"eslint-config-standard-with-typescript": "^35.0.0",
"eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.11.1",
"eslint-plugin-vue": "^9.15.1",
"eslint-plugin-vue-pug": "^0.6.0",
"eslint-webpack-plugin": "^4.0.1",
"html-webpack-plugin": "^5.5.1",
"html-webpack-plugin": "^5.5.3",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"mini-css-extract-plugin": "^2.7.5",
"less-loader": "^11.1.3",
"mini-css-extract-plugin": "^2.7.6",
"node-loader": "^2.0.0",
"postcss": "^8.4.23",
"postcss-loader": "^7.3.0",
"postcss": "^8.4.24",
"postcss-loader": "^7.3.3",
"postcss-pxtorem": "^6.0.0",
"pug": "^3.0.2",
"pug-plain-loader": "^1.1.0",
"rimraf": "^5.0.0",
"rimraf": "^5.0.1",
"spinnies": "github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d",
"svg-sprite-loader": "^6.0.11",
"svg-transform-loader": "^2.0.13",
"svgo-loader": "^4.0.0",
"terser": "^5.17.1",
"terser-webpack-plugin": "^5.3.8",
"ts-loader": "^9.4.2",
"typescript": "^5.0.4",
"vue-eslint-parser": "^9.2.1",
"vue-loader": "^17.1.0",
"terser": "^5.18.2",
"terser-webpack-plugin": "^5.3.9",
"ts-loader": "^9.4.4",
"typescript": "^5.1.6",
"vue-eslint-parser": "^9.3.1",
"vue-loader": "^17.2.2",
"vue-template-compiler": "^2.7.14",
"webpack": "^5.82.0",
"webpack-cli": "^5.1.0",
"webpack-dev-server": "^4.15.0",
"webpack": "^5.88.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1",
"webpack-merge": "^5.8.0"
"webpack-merge": "^5.9.0"
},
"dependencies": {
"@simonwep/pickr": "^1.8.2",
"better-sqlite3": "^8.3.0",
"better-sqlite3": "^8.4.0",
"bufferutil": "^4.0.7",
"comlink": "~4.3.1",
"crypto-js": "^4.1.1",
"electron-font-manager": "github:lyswhut/electron-font-manager#6d2f5ecf850c4fe34812b9394913680462ee0dae",
"electron-log": "^4.4.8",
"electron-store": "^8.1.0",
"font-list": "^1.4.5",
"font-list": "^1.5.0",
"iconv-lite": "^0.6.3",
"image-size": "^1.0.2",
"jschardet": "^3.0.0",
@@ -292,14 +300,15 @@
"sortablejs": "^1.15.0",
"tunnel": "^0.0.6",
"utf-8-validate": "^6.0.3",
"vue": "^3.2.47",
"vue-router": "^4.1.6",
"vue": "~3.2.47",
"vue-router": "^4.2.2",
"ws": "^8.13.0"
},
"overrides": {
"got": "^11",
"json5": "latest",
"minimatch": "latest",
"semver": "latest",
"svg-sprite-loader": {
"postcss": "latest"
},

View File

@@ -1,7 +1,28 @@
### 新增
- 新增音效设置实验性功能支持10段均衡器设置、内置的一些环境混响音效、3D立体环绕音效
- 新增音效设置实验性功能支持10段均衡器设置、内置的一些环境混响音效、音调升降调节、3D立体环绕音效由于升降调需要实时处理音频数据这会导致额外的CPU占用已知问题如果CPU资源不够时将处理导致任务堆积而出现声音异常这时需要暂停播放一段时间等堆积的任务处理完毕再播放
- 播放速率设置面板新增是否音调补偿设置,在调整播放速率后,可以选择是否启用音调补偿,默认启用
### 优化
- Windows、MacOS平台下的字体列表改用原生方式获取现在Windows平台下能显示当前已安装的更多类型字体了MacOS平台未测可用性未知
- 移除桌面歌词窗口透明边距在Linux下的桌面歌词可以完全拖到贴合屏幕边缘了
- 过滤嵌入、下载的翻译、罗马音歌词时间标签,与主歌词时间不匹配的歌词将被丢弃,防止出现原歌词与翻译歌词顺序错乱的问题(#1358
### 修复
- 修复列表名翻译显示
- 修复因插入数字类型的ID导致其意外在末尾追加 .0 导致列表数据异常的问题,同时也可能导致同步数据丢失的问题(要完全修复这个问题还需要同时将移动端、同步服务更新到最新版本)
- 修复下载时出现302错误的问题
- 修复播放某些在线音频会没有声音的问题
- 修复改变播放速率时会导致歌词报错的问题
- 修复tx热门评论昵称被错误切割的问题 (#1397, By: @helloplhm-qwq, @Folltoshe)
- 修复wy源热搜词失效的问题#1401, @Folltoshe
- 修复Deepin 20下启用桌面歌词时可能会导致桌面卡死的问题#1288
- 修复添加单首歌曲弹窗列表创建按钮无法取消的问题
- 修复mg歌单搜索歌单播放数量显示问题
- 修复tx翻译歌词解析丢失的问题更新版本后需手动清理一次歌词缓存
### 其他
- 更新 electron 到 v22.3.8
- 更新 electron 到 v22.3.15

File diff suppressed because one or more lines are too long

View File

@@ -29,6 +29,7 @@ const defaultSetting: LX.AppSetting = {
'player.volume': 1,
'player.isMute': false,
'player.playbackRate': 1,
'player.preservesPitch': true,
'player.mediaDeviceId': 'default',
'player.isMediaDeviceRemovedStopPlay': false,
'player.isShowLyricTranslation': false,
@@ -56,6 +57,7 @@ const defaultSetting: LX.AppSetting = {
'player.soundEffect.panner.enable': false,
'player.soundEffect.panner.soundR': 5,
'player.soundEffect.panner.speed': 25,
'player.soundEffect.pitchShifter.playbackRate': 1,
'playDetail.isZoomActiveLrc': false,
'playDetail.isShowLyricProgressSetting': false,
@@ -73,7 +75,7 @@ const defaultSetting: LX.AppSetting = {
'desktopLyric.height': 300,
'desktopLyric.x': null,
'desktopLyric.y': null,
'desktopLyric.isLockScreen': true,
'desktopLyric.isLockScreen': isWin,
'desktopLyric.isDelayScroll': true,
'desktopLyric.scrollAlign': 'center',
'desktopLyric.isHoverHide': false,
@@ -134,8 +136,8 @@ const defaultSetting: LX.AppSetting = {
'sync.server.maxSsnapshotNum': 5,
'sync.client.host': '',
'theme.id': 'blue_plus',
// 'theme.id': 'green',
// 'theme.id': 'blue_plus',
'theme.id': 'green',
'theme.lightId': 'green',
'theme.darkId': 'black',

View File

@@ -94,6 +94,8 @@ const modules = {
save_sound_effect_eq_preset: 'save_sound_effect_eq_preset',
get_sound_effect_convolution_preset: 'get_sound_effect_convolution_preset',
save_sound_effect_convolution_preset: 'save_sound_effect_convolution_preset',
// get_sound_effect_pitch_shifter_preset: 'get_sound_effect_pitch_shifter_preset',
// save_sound_effect_pitch_shifter_preset: 'save_sound_effect_pitch_shifter_preset',
get_hot_key: 'get_hot_key',
import_user_api: 'import_user_api',

View File

@@ -1,16 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "nodenext",
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
"paths": {
"@common/*": ["common/*"],
},
"typeRoots": [
"./types"
],
},
// "include": [
// "**/*.ts",
// "**/*.js",
// "**/*.vue",
// "**/*.json",
// ],
}

View File

@@ -113,6 +113,11 @@ declare global {
*/
'player.playbackRate': number
/**
* 是否自动调整音频的音高以补偿对播放速率设置所做的更改
*/
'player.preservesPitch': boolean
/**
* 音频输出设备id
*/
@@ -243,6 +248,11 @@ declare global {
*/
'player.soundEffect.panner.speed': number
/**
* 升降声调
*/
'player.soundEffect.pitchShifter.playbackRate': number
/**
* 是否启用音频加载失败时自动切歌
*/

View File

@@ -21,5 +21,10 @@ declare namespace LX {
mainGain: number
sendGain: number
}
// interface PitchShifterPreset {
// id: string
// name: string
// playbackRate: number
// }
}
}

View File

@@ -37,6 +37,8 @@ class Task extends EventEmitter {
progress = { total: 0, downloaded: 0, speed: 0, progress: 0 }
statsEstimate = { time: 0, bytes: 0, prevBytes: 0 }
requestInstance: http.ClientRequest | null = null
maxRedirectNum = 2
private redirectNum = 0
constructor(url: string, savePath: string, filename: string, options: Partial<Options> = {}) {
@@ -59,6 +61,7 @@ class Task extends EventEmitter {
async __init() {
const { path, startByte, endByte } = this.chunkInfo
this.redirectNum = 0
this.progress.downloaded = 0
this.progress.progress = 0
this.progress.speed = 0
@@ -112,6 +115,7 @@ class Task extends EventEmitter {
__httpFetch(url: string, options: Options['requestOptions']) {
// console.log(options)
let redirected = false
this.requestInstance = request(url, options)
.on('response', response => {
if (response.statusCode !== 200 && response.statusCode !== 206) {
@@ -125,6 +129,15 @@ class Task extends EventEmitter {
})
return
}
if ((response.statusCode == 301 || response.statusCode == 302) && response.headers.location && this.redirectNum < this.maxRedirectNum) {
console.log('current url:', url)
console.log('redirect to:', response.headers.location)
redirected = true
this.redirectNum++
const location = response.headers.location
this.__httpFetch(location, options)
return
}
this.status = STATUS.failed
this.emit('fail', response)
this.__closeRequest()
@@ -153,6 +166,7 @@ class Task extends EventEmitter {
})
.on('error', err => { this.__handleError(err) })
.on('close', () => {
if (redirected) return
void this.__closeWriteStream()
})
.end()

View File

@@ -4,24 +4,24 @@
// -------------------------------------------------------------------------------------------------
// Change the following variables to customize the appearance of the particles
var numParticles = window.innerWidth / 50;
var maxSpeed = 2.5;
var minSpeed = 1;
var maxSize = 5;
var minSize = 3;
var maxOpacity = 1;
var minOpacity = 0.2;
let numParticles = window.innerWidth / 50
let maxSpeed = 2.5
let minSpeed = 1
let maxSize = 5
let minSize = 3
let maxOpacity = 1
let minOpacity = 0.2
// If the following is true, the canvas resolution will be set to match the full viewport width and height.
// var fixCanvasResolution = true;
var snow_particles = [];
let snow_particles = []
var canvas
let canvas
window.onload = function() {
canvas = document.createElement('canvas')
canvas.style.position = 'fixed';
canvas.style.position = 'fixed'
canvas.style.top = '0px'
canvas.style.left = '0px'
canvas.style.pointerEvents = 'none'
@@ -30,62 +30,62 @@ window.onload = function() {
canvas.height = document.documentElement.clientHeight
document.body.appendChild(canvas)
// if (fixCanvasResolution) {
// // canvas.width = window.innerWidth;
// canvas.height = window.innerHeight;
// }
// if (fixCanvasResolution) {
// // canvas.width = window.innerWidth;
// canvas.height = window.innerHeight;
// }
InitPoints();
InitPoints()
requestAnimationFrame(Redraw, 15);
window.requestAnimationFrame(Redraw, 15)
window.addEventListener('resize', this.onWindowResize)
}
function onWindowResize (e) {
canvas.width = document.documentElement.clientWidth
canvas.height = document.documentElement.clientHeight
}
// function onWindowResize(e) {
// canvas.width = document.documentElement.clientWidth
// canvas.height = document.documentElement.clientHeight
// }
function Redraw() {
var ctx = canvas.getContext("2d");
ctx.clearRect(0,0,canvas.width,canvas.height);
for (var i = 0; i < snow_particles.length; i++) {
var newYPos = snow_particles[i].yPos + snow_particles[i].speed;
if (newYPos > window.innerHeight) {
newYPos = getRandomInt(-100,-10);
snow_particles[i].xPos = getRandomInt(0, window.innerWidth);
snow_particles[i].speed = Math.random() * (maxSpeed - minSpeed) + minSpeed;
snow_particles[i].opacity = Math.random() * (maxOpacity - minOpacity) + minOpacity;
snow_particles[i].size = Math.random() * (maxSize - minSize) + minSize;
}
snow_particles[i].yPos = newYPos;
ctx.beginPath();
ctx.arc(snow_particles[i].xPos, newYPos, snow_particles[i].size, 0, 2 * Math.PI);
ctx.fillStyle = "rgba(255, 255, 255, " + snow_particles[i].opacity + ")";
ctx.fill();
}
requestAnimationFrame(Redraw)
let ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < snow_particles.length; i++) {
let newYPos = snow_particles[i].yPos + snow_particles[i].speed
if (newYPos > window.innerHeight) {
newYPos = getRandomInt(-100, -10)
snow_particles[i].xPos = getRandomInt(0, window.innerWidth)
snow_particles[i].speed = Math.random() * (maxSpeed - minSpeed) + minSpeed
snow_particles[i].opacity = Math.random() * (maxOpacity - minOpacity) + minOpacity
snow_particles[i].size = Math.random() * (maxSize - minSize) + minSize
}
snow_particles[i].yPos = newYPos
ctx.beginPath()
ctx.arc(snow_particles[i].xPos, newYPos, snow_particles[i].size, 0, 2 * Math.PI)
ctx.fillStyle = 'rgba(255, 255, 255, ' + snow_particles[i].opacity + ')'
ctx.fill()
}
window.requestAnimationFrame(Redraw)
}
function InitPoints() {
for (var i = 0; i < numParticles; i++) {
var startX = getRandomInt(0, window.innerWidth);
var startY = getRandomInt(0, window.innerHeight);
var speed = Math.random() * (maxSpeed - minSpeed) + minSpeed;
var opacity = Math.random() * (maxOpacity - minOpacity) + minOpacity;
var size = Math.random() * (maxSize - minSize) + minSize;
for (let i = 0; i < numParticles; i++) {
let startX = getRandomInt(0, window.innerWidth)
let startY = getRandomInt(0, window.innerHeight)
let speed = Math.random() * (maxSpeed - minSpeed) + minSpeed
let opacity = Math.random() * (maxOpacity - minOpacity) + minOpacity
let size = Math.random() * (maxSize - minSize) + minSize
snow_particles.push({
"xPos": startX,
"yPos": startY,
"speed": speed,
"opacity": opacity,
"size": size,
});
}
snow_particles.push({
xPos: startX,
yPos: startY,
speed,
opacity,
size,
})
}
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min)) + min
}

View File

@@ -133,8 +133,8 @@ module.exports = class FontPlayer {
// let lineText = ''
let lrcShadowContent
for (const font of fonts) {
if (!timeRxp.test(font)) return this._handleLineParse()
text = font.replace(timeRxp, '')
if (RegExp.$2 == '') return this._handleLineParse()
const time = parseInt(RegExp.$2)
const dom = document.createElement('span')

View File

@@ -17,11 +17,7 @@ import {
type ComputedRef,
type Ref,
type ShallowRef,
defineProps,
defineEmits,
defineComponent,
shallowReactive,
defineExpose,
withDefaults,
} from 'vue'
// import { useStore } from 'vuex'
@@ -75,11 +71,7 @@ export {
unref,
onMounted,
markRaw,
defineProps,
defineEmits,
defineComponent,
shallowReactive,
defineExpose,
withDefaults,
}

View File

@@ -223,6 +223,7 @@
"player__play_toggle_mode_off": "Disable",
"player__play_toggle_mode_random": "List Random",
"player__play_toggle_mode_single_loop": "Single Loop",
"player__playback_preserves_pitch": "Pitch compensation",
"player__playback_rate": "Current playback rate:",
"player__playback_rate_reset_btn": "Reset",
"player__playing": "Now playing...",
@@ -263,6 +264,10 @@
"player__sound_effect_panner_enabled": "enable",
"player__sound_effect_panner_sound_r": "Sound distance",
"player__sound_effect_panner_sound_speed": "Surround speed",
"player__sound_effect_pitch_shifter": "Pitch adjustment",
"player__sound_effect_pitch_shifter_preset_semitones": "{num} semitones",
"player__sound_effect_pitch_shifter_reset_btn": "Reset",
"player__sound_effect_pitch_shifter_tip": "Since ups and downs need to process audio data in real time, this will cause additional CPU usage\n\nKnown issues:\nIf the CPU resources are not enough, the processing will cause the task to accumulate and the sound will be abnormal. At this time, it is necessary to pause the playback for a period of time and wait for the accumulated tasks to be processed before playing.",
"player__stop": "Paused",
"player__volume": "Volume: ",
"player__volume_mute_label": "Mute",

View File

@@ -1,14 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "esnext", /* Specify what module code is generated. */
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "typeRoots": [
// "./types"
// ],
},
// "include": [
// "**/*.ts",
// "**/*.js",
// "**/*.vue",
// "**/*.json",
// ],
// "exclude": ["dbService"]
}

View File

@@ -223,6 +223,7 @@
"player__play_toggle_mode_off": "禁用",
"player__play_toggle_mode_random": "列表随机",
"player__play_toggle_mode_single_loop": "单曲循环",
"player__playback_preserves_pitch": "音调补偿",
"player__playback_rate": "当前播放速率:",
"player__playback_rate_reset_btn": "重置",
"player__playing": "播放中...",
@@ -262,6 +263,10 @@
"player__sound_effect_panner_enabled": "启用",
"player__sound_effect_panner_sound_r": "声音距离",
"player__sound_effect_panner_sound_speed": "环绕速度",
"player__sound_effect_pitch_shifter": "音调升降调节",
"player__sound_effect_pitch_shifter_preset_semitones": "{num} 半音",
"player__sound_effect_pitch_shifter_reset_btn": "重置",
"player__sound_effect_pitch_shifter_tip": "由于升降调需要实时处理音频数据这会导致额外的CPU占用\n\n已知问题\n如果CPU资源不够时将处理导致任务堆积而出现声音异常这时需要暂停播放一段时间等堆积的任务处理完毕再播放",
"player__stop": "暂停播放",
"player__volume": "当前音量:",
"player__volume_mute_label": "静音",

View File

@@ -223,6 +223,7 @@
"player__play_toggle_mode_off": "禁用",
"player__play_toggle_mode_random": "列表隨機",
"player__play_toggle_mode_single_loop": "單曲循環",
"player__playback_preserves_pitch": "音調補償",
"player__playback_rate": "當前播放速率:",
"player__playback_rate_reset_btn": "重置",
"player__playing": "播放中...",
@@ -244,7 +245,7 @@
"player__sound_effect_biquad_filter_save_input": "新預設...",
"player__sound_effect_convolution": "環境混響音效",
"player__sound_effect_convolution_file_bright_hall": "大廳",
"player__sound_effect_convolution_file_cardiod_35_10_spread": "搖滾",
"player__sound_effect_convolution_file_cardiod_35_10_spread": "心形擴散",
"player__sound_effect_convolution_file_cinema_diningroom": "電影院",
"player__sound_effect_convolution_file_dining_living_true_stereo": "餐廳",
"player__sound_effect_convolution_file_feedback_spring": "反饋彈簧",
@@ -263,6 +264,10 @@
"player__sound_effect_panner_enabled": "啟用",
"player__sound_effect_panner_sound_r": "聲音距離",
"player__sound_effect_panner_sound_speed": "環繞速度",
"player__sound_effect_pitch_shifter": "音調升降調節",
"player__sound_effect_pitch_shifter_preset_semitones": "{num} 半音",
"player__sound_effect_pitch_shifter_reset_btn": "重置",
"player__sound_effect_pitch_shifter_tip": "由於升降調需要實時處理音頻數據這會導致額外的CPU佔用\n\n已知問題\n如果CPU資源不夠時將處理導致任務堆積而出現聲音異常這時需要暫停播放一段時間等堆積的任務處理完畢再播放",
"player__stop": "暫停播放",
"player__volume": "當前音量:",
"player__volume_mute_label": "靜音",

View File

@@ -102,4 +102,4 @@ declare class EventType extends Event {
off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
}
export declare type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>
export type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>

View File

@@ -14,6 +14,18 @@ import { EventEmitter } from 'events'
// musicOverwrite,
// } from '@main/workers/dbService/modules/list'
// 兼容v2.3.0之前版本插入数字类型的ID导致其意外在末尾追加 .0 的问题确保所有ID都是字符串类型
const fixListIdType = (lists: LX.List.UserListInfo[] | LX.List.UserListInfoFull[]) => {
for (const list of lists) {
if (typeof list.sourceListId == 'number') {
list.sourceListId = String(list.sourceListId)
if (typeof list.id == 'number') {
list.id = String(list.id)
}
}
}
}
export class Event extends EventEmitter {
list_changed() {
this.emit('list_changed')
@@ -25,6 +37,7 @@ export class Event extends EventEmitter {
* @param isRemote 是否属于远程操作
*/
async list_data_overwrite(listData: MakeOptional<LX.List.ListDataFull, 'tempList'>, isRemote: boolean = false) {
fixListIdType(listData.userList)
await global.lx.worker.dbService.listDataOverwrite(listData)
this.emit('list_data_overwrite', listData, isRemote)
this.list_changed()
@@ -37,6 +50,7 @@ export class Event extends EventEmitter {
* @param isRemote 是否属于远程操作
*/
async list_create(position: number, lists: LX.List.UserListInfo[], isRemote: boolean = false) {
fixListIdType(lists)
await global.lx.worker.dbService.createUserLists(position, lists)
this.emit('list_create', position, lists, isRemote)
this.list_changed()
@@ -171,4 +185,4 @@ declare class EventType extends Event {
once<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
}
export declare type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>
export type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>

View File

@@ -6,7 +6,7 @@
*/
import { app } from 'electron'
import * as electronDebug from 'electron-debug'
import electronDebug from 'electron-debug'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
// Install `electron-debug` with `devtron`
electronDebug({

View File

@@ -110,10 +110,10 @@ const setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.Li
})
})
type UserDataObj = Record<string, LX.List.UserListInfoFull>
type UserDataObj = Map<string, LX.List.UserListInfoFull>
const createUserListDataObj = (listData: LX.Sync.ListData): UserDataObj => {
const userListDataObj: UserDataObj = {}
for (const list of listData.userList) userListDataObj[list.id] = list
const userListDataObj: UserDataObj = new Map()
for (const list of listData.userList) userListDataObj.set(list.id, list)
return userListDataObj
}
@@ -171,7 +171,7 @@ const mergeList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync.Lis
targetListData.userList.forEach((list, index) => {
const targetUpdateTime = list?.locationUpdateTime ?? 0
const sourceList = userListDataObj[list.id]
const sourceList = userListDataObj.get(list.id)
if (sourceList) {
sourceList.list = handleMergeList(sourceList.list, list.list, addMusicLocationType)
@@ -205,7 +205,7 @@ const overwriteList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync
newListData.userList = [...sourceListData.userList]
targetListData.userList.forEach((list, index) => {
if (userListDataObj[list.id]) return
if (userListDataObj.has(list.id)) return
if (list?.locationUpdateTime) {
newListData.userList.splice(index, 0, list)
} else {
@@ -367,10 +367,10 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, sna
let newUserList: LX.List.UserListInfoFull[] = []
for (const list of localListData.userList) {
if (removedListIds.has(list.id)) continue
const remoteList = remoteUserListData[list.id]
const remoteList = remoteUserListData.get(list.id)
let newList: LX.List.UserListInfoFull
if (remoteList) {
newList = { ...list, list: mergeListDataFromSnapshot(list.list, remoteList.list, snapshotUserListData[list.id].list, addMusicLocationType) }
newList = { ...list, list: mergeListDataFromSnapshot(list.list, remoteList.list, snapshotUserListData.get(list.id)?.list ?? [], addMusicLocationType) }
} else {
newList = { ...list }
}
@@ -380,8 +380,8 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, sna
remoteListData.userList.forEach((list, index) => {
if (removedListIds.has(list.id)) return
const remoteUpdateTime = list?.locationUpdateTime ?? 0
if (localUserListData[list.id]) {
const localUpdateTime = localUserListData[list.id]?.locationUpdateTime ?? 0
if (localUserListData.has(list.id)) {
const localUpdateTime = localUserListData.get(list.id)?.locationUpdateTime ?? 0
if (localUpdateTime >= remoteUpdateTime) return
// 调整位置
const [newList] = newUserList.splice(newUserList.findIndex(l => l.id == list.id), 1)

View File

@@ -140,15 +140,11 @@ const handleShowUpdateAlert = (data, resolve, reject) => {
contextBridge.exposeInMainWorld('lx', {
EVENT_NAMES,
request(url, { method = 'get', timeout, headers, body, form, formData, bodyBinaryBase64 }, callback) {
request(url, { method = 'get', timeout, headers, body, form, formData }, callback) {
let options = { headers }
let data
if (body) {
data = body
} else if (bodyBinaryBase64) {
try {
data = Buffer.from(bodyBinaryBase64, 'base64')
} catch {}
} else if (form) {
data = form
// data.content_type = 'application/x-www-form-urlencoded'
@@ -257,7 +253,7 @@ contextBridge.exposeInMainWorld('lx', {
},
},
},
version: '1.4.0',
version: '1.3.0',
// removeEvent(eventName, handler) {
// if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
// let handlers

View File

@@ -1,6 +1,6 @@
import { join } from 'path'
import { BrowserWindow } from 'electron'
import { debounce, isLinux } from '@common/utils'
import { debounce, isLinux, isWin } from '@common/utils'
import { initWindowSize } from './utils'
import { mainSend } from '@common/mainIpc'
import { encodePath } from '@common/utils/electron'
@@ -41,7 +41,7 @@ const winEvent = () => {
'desktopLyric.width': bounds.width,
'desktopLyric.height': bounds.height,
})
} else {
} else if (isWin) { // Linux 不允许将窗口设置出屏幕之外MacOS未知故只在Windows下执行强制设置
// 非主动调整窗口触发的窗口位置变化将重置回设置值
browserWindow!.setBounds({
x: global.lx.appSetting['desktopLyric.x'] ?? 0,

View File

@@ -1,5 +1,4 @@
// 设置窗口位置、大小
export const padding = 8
export let minWidth = 80
export let minHeight = 50
@@ -21,25 +20,23 @@ export const getLyricWindowBounds = (bounds: Electron.Rectangle, { x = 0, y = 0,
if (global.lx.appSetting['desktopLyric.isLockScreen']) {
if (!global.envParams.workAreaSize) return bounds
const maxWinW = global.envParams.workAreaSize.width + padding * 2
const maxWinH = global.envParams.workAreaSize.height + padding * 2
const maxWinW = global.envParams.workAreaSize.width
const maxWinH = global.envParams.workAreaSize.height
if (w > maxWinW) w = maxWinW
if (h > maxWinH) h = maxWinH
const maxX = global.envParams.workAreaSize.width + padding - w
const minX = -padding
const maxY = global.envParams.workAreaSize.height + padding - h
const minY = -padding
const maxX = global.envParams.workAreaSize.width - w
const maxY = global.envParams.workAreaSize.height - h
x += bounds.x
y += bounds.y
if (x > maxX) x = maxX
else if (x < minX) x = minX
else if (x < 0) x = 0
if (y > maxY) y = maxY
else if (y < minY) y = minY
else if (y < 0) y = 0
} else {
y += bounds.y
x += bounds.x
@@ -101,10 +98,10 @@ export const initWindowSize = (x: LX.AppSetting['desktopLyric.x'], y: LX.AppSett
if (width < minWidth) width = minWidth
if (height < minHeight) height = minHeight
if (global.envParams.workAreaSize) {
x = global.envParams.workAreaSize.width + padding - width
y = global.envParams.workAreaSize.height + padding - height
x = global.envParams.workAreaSize.width - width
y = global.envParams.workAreaSize.height - height
} else {
x = y = -padding
x = y = 0
}
} else {
let bounds = getLyricWindowBounds({ x, y, width, height }, { x: 0, y: 0, w: width, h: height })

View File

@@ -17,4 +17,11 @@ export default () => {
mainOn<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, ({ params }) => {
getStore(STORE_NAMES.SOUND_EFFECT).set('convolutionPreset', params)
})
// mainHandle<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_pitch_shifter_preset, async() => {
// return getStore(STORE_NAMES.SOUND_EFFECT).get('pitchShifterPreset') as LX.SoundEffect.PitchShifterPreset[] | null ?? []
// })
// mainOn<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_pitch_shifter_preset, ({ params }) => {
// getStore(STORE_NAMES.SOUND_EFFECT).set('pitchShifterPreset', params)
// })
}

View File

@@ -1,23 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "esnext", /* Specify what module code is generated. */
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "module": "esnext",
"typeRoots": [
"./types"
],
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
"@common/*": ["common/*"],
"@main/*": ["main/*"],
"@static/*": ["static/*"],
"@/*": ["./*"],
},
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
"./types",
],
},
// "include": [
// "**/*.ts",
// "**/*.js",
// "**/*.vue",
// "**/*.json",
// ],
// "exclude": ["dbService"]
}

View File

@@ -3,8 +3,18 @@
// exports.getAvailableFontFamilies = getAvailableFontFamilies
import { getFonts as getFontsByCommand } from 'font-list'
import { getAvailableFontFamilies } from 'electron-font-manager'
import { getFonts } from 'font-list'
const getFonts = async() => {
switch (process.platform) {
case 'win32':
case 'darwin':
return getAvailableFontFamilies()
default: return getFontsByCommand()
}
}
export {
getFonts,

View File

@@ -9,7 +9,7 @@ const common = {
exposeWorker(Object.assign(common, list, lyric, music_url, music_other_source, download))
export declare type workerDBSeriveTypes = typeof common
export type workerDBSeriveTypes = typeof common
& typeof list
& typeof lyric
& typeof music_url

View File

@@ -19,13 +19,20 @@ import {
createMusicInfoByMusicInfoIdQueryStatement,
} from './statements'
const idFixRxp = /\.0$/
/**
* 获取用户列表
* @returns
*/
export const queryAllUserList = () => {
return createListQueryStatement().all() as LX.DBService.UserListInfo[]
const list = createListQueryStatement().all() as LX.DBService.UserListInfo[]
for (const info of list) {
// 兼容v2.3.0之前版本插入数字类型的ID导致其意外在末尾追加 .0 的问题
if (info.sourceListId?.endsWith?.('.0')) {
info.sourceListId = info.sourceListId.replace(idFixRxp, '')
}
}
return list
}
/**

View File

@@ -2,7 +2,7 @@ import { Worker } from 'worker_threads'
import * as Comlink from 'comlink'
import nodeEndpoint from 'comlink/dist/esm/node-adapter'
export declare type DBSeriveTypes = Comlink.Remote<LX.WorkerDBSeriveListTypes>
export type DBSeriveTypes = Comlink.Remote<LX.WorkerDBSeriveListTypes>
export const createDBServiceWorker = () => {
const worker: Worker = new Worker(new URL(

View File

@@ -12,14 +12,14 @@
<common-audio-visualizer v-if="setting['desktopLyric.audioVisualization']" />
</transition>
</div>
<div class="resize-left" @mousedown.self="handleMouseDown('left', $event)" @touchstart.self="handleTouchDown('left', $event)" />
<div class="resize-top" @mousedown.self="handleMouseDown('top', $event)" @touchstart.self="handleTouchDown('top', $event)" />
<div class="resize-right" @mousedown.self="handleMouseDown('right', $event)" @touchstart.self="handleTouchDown('right', $event)" />
<div class="resize-bottom" @mousedown.self="handleMouseDown('bottom', $event)" @touchstart.self="handleTouchDown('bottom', $event)" />
<div class="resize-top-left" @mousedown.self="handleMouseDown('top-left', $event)" @touchstart.self="handleTouchDown('top-left', $event)" />
<div class="resize-top-right" @mousedown.self="handleMouseDown('top-right', $event)" @touchstart.self="handleTouchDown('top-right', $event)" />
<div class="resize-bottom-left" @mousedown.self="handleMouseDown('bottom-left', $event)" @touchstart.self="handleTouchDown('bottom-left', $event)" />
<div class="resize-bottom-right" @mousedown.self="handleMouseDown('bottom-right', $event)" @touchstart.self="handleTouchDown('bottom-right', $event)" />
<div class="resize resize-left" @mousedown.self="handleMouseDown('left', $event)" @touchstart.self="handleTouchDown('left', $event)" />
<div class="resize resize-top" @mousedown.self="handleMouseDown('top', $event)" @touchstart.self="handleTouchDown('top', $event)" />
<div class="resize resize-right" @mousedown.self="handleMouseDown('right', $event)" @touchstart.self="handleTouchDown('right', $event)" />
<div class="resize resize-bottom" @mousedown.self="handleMouseDown('bottom', $event)" @touchstart.self="handleTouchDown('bottom', $event)" />
<div class="resize resize-top-left" @mousedown.self="handleMouseDown('top-left', $event)" @touchstart.self="handleTouchDown('top-left', $event)" />
<div class="resize resize-top-right" @mousedown.self="handleMouseDown('top-right', $event)" @touchstart.self="handleTouchDown('top-right', $event)" />
<div class="resize resize-bottom-left" @mousedown.self="handleMouseDown('bottom-left', $event)" @touchstart.self="handleTouchDown('bottom-left', $event)" />
<div class="resize resize-bottom-right" @mousedown.self="handleMouseDown('bottom-right', $event)" @touchstart.self="handleTouchDown('bottom-right', $event)" />
<layout-icons />
</div>
</template>
@@ -70,7 +70,6 @@ body {
}
#container {
padding: 8px;
box-sizing: border-box;
height: 100%;
transition: opacity .3s ease;
@@ -85,12 +84,16 @@ body {
}
}
@resize-width: 6px;
.resize {
z-index: 2;
}
.resize-left {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 10px;
width: @resize-width;
cursor: ew-resize;
// background-color: rgba(0, 0, 0, 1);
}
@@ -99,14 +102,14 @@ body {
right: 0;
top: 0;
height: 100%;
width: 10px;
width: @resize-width;
cursor: ew-resize;
}
.resize-top {
position: absolute;
left: 0;
top: 0;
height: 10px;
height: 4px;
width: 100%;
cursor: ns-resize;
}
@@ -114,7 +117,7 @@ body {
position: absolute;
left: 0;
bottom: 0;
height: 10px;
height: @resize-width;
width: 100%;
cursor: ns-resize;
}
@@ -122,8 +125,8 @@ body {
position: absolute;
left: 0;
top: 0;
width: 14px;
height: 14px;
width: @resize-width;
height: @resize-width;
cursor: nwse-resize;
// background-color: rgba(0, 0, 0, 1);
}
@@ -131,26 +134,17 @@ body {
position: absolute;
right: 0;
top: 0;
width: 14px;
height: 14px;
width: @resize-width;
height: @resize-width;
cursor: nesw-resize;
// background-color: rgba(0, 0, 0, 1);
}
.resize-top-left {
position: absolute;
left: 0;
top: 0;
width: 14px;
height: 14px;
cursor: nwse-resize;
// background-color: rgba(0, 0, 0, 1);
}
.resize-bottom-left {
position: absolute;
left: 0;
bottom: 0;
width: 14px;
height: 14px;
width: @resize-width;
height: @resize-width;
cursor: nesw-resize;
// background-color: rgba(0, 0, 0, 1);
}
@@ -158,8 +152,8 @@ body {
position: absolute;
right: 0;
bottom: 0;
width: 14px;
height: 14px;
width: @resize-width;
height: @resize-width;
cursor: nwse-resize;
// background-color: rgba(0, 0, 0, 1);
}

View File

@@ -80,7 +80,7 @@ export const setting = shallowReactive<LX.DesktopLyric.Config>({
// },
// ])
// export declare type Status = 'playing' | 'paused' | 'stopped'
// export type Status = 'playing' | 'paused' | 'stopped'
// export const status = ref<Status>('stopped')
export const isPlay = ref(false)

View File

@@ -1,9 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "nodenext",
"isolatedModules": true,
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
"@common/*": ["common/*"],
// "@renderer/*": ["renderer/*"],
@@ -15,11 +13,11 @@
"./types"
],
},
"vueCompilerOptions": {
"plugins": [
"@volar/vue-language-plugin-pug"
]
}
// "vueCompilerOptions": {
// "plugins": [
// "@volar/vue-language-plugin-pug"
// ]
// }
// "include": [
// "./**/*.ts",
// // "./**/*.js",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11 7V9H13V7H11M14 17V15H13V11H10V13H11V15H10V17H14M22 12C22 17.5 17.5 22 12 22C6.5 22 2 17.5 2 12C2 6.5 6.5 2 12 2C17.5 2 22 6.5 22 12M20 12C20 7.58 16.42 4 12 4C7.58 4 4 7.58 4 12C4 16.42 7.58 20 12 20C16.42 20 20 16.42 20 12Z" /></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -51,7 +51,7 @@ export default {
default: false,
},
},
emits: ['update:modelValue', 'change'],
emits: ['update:model-value', 'change'],
data() {
return {
checked: false,
@@ -80,7 +80,7 @@ export default {
modelValue = checked
} else modelValue = checked ? this.value : ''
}
this.$emit('update:modelValue', modelValue)
this.$emit('update:model-value', modelValue)
this.$emit('change', modelValue)
},
setValue(value) {

View File

@@ -52,7 +52,7 @@ export default {
// default: true,
// },
},
emits: ['update:modelValue', 'submit', 'change'],
emits: ['update:model-value', 'submit', 'change'],
methods: {
handleInput(event) {
let value = event.target.value
@@ -60,7 +60,7 @@ export default {
value = value.trim()
event.target.value = value
}
this.$emit('update:modelValue', value)
this.$emit('update:model-value', value)
},
focus() {
this.$refs.dom_input.focus()
@@ -78,7 +78,7 @@ export default {
// if (dom_input.selectionStart == dom_input.selectionEnd) {
const value = text.substring(0, dom_input.selectionStart) + str + text.substring(dom_input.selectionEnd, text.length)
event.target.value = value
this.$emit('update:modelValue', value)
this.$emit('update:model-value', value)
// } else {
// clipboardWriteText(text.substring(dom_input.selectionStart, dom_input.selectionEnd))
// }

View File

@@ -48,13 +48,13 @@ export default {
default: 'name',
},
},
emits: ['update:modelValue', 'menu-click'],
emits: ['update:model-value', 'menu-click'],
setup(props, { emit }) {
const visible = computed(() => props.modelValue)
const location = computed(() => props.xy)
const onHide = () => {
emit('update:modelValue', false)
emit('update:model-value', false)
menuClick(null)
}

View File

@@ -17,7 +17,7 @@
</template>
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, reactive, defineEmits } from '@common/utils/vueTools'
import { ref, watch, onMounted, onBeforeUnmount, reactive } from '@common/utils/vueTools'
// https://github.com/vuejs/core/issues/2855#issuecomment-768388962
import {

View File

@@ -42,7 +42,7 @@ export default {
default: '',
},
},
emits: ['update:modelValue', 'change'],
emits: ['update:model-value', 'change'],
data() {
return {
show: false,
@@ -84,7 +84,7 @@ export default {
handleClick(item) {
// console.log(this.modelValue)
if (item === this.modelValue) return
this.$emit('update:modelValue', this.itemKey ? item[this.itemKey] : item)
this.$emit('update:model-value', this.itemKey ? item[this.itemKey] : item)
this.$emit('change', item)
},
handleShow() {

View File

@@ -37,11 +37,11 @@ export default {
default: '',
},
},
emits: ['update:modelValue', 'change'],
emits: ['update:model-value', 'change'],
setup(props, { emit }) {
const handleToggle = id => {
if (id == props.modelValue) return
emit('update:modelValue', id)
emit('update:model-value', id)
emit('change', id)
}

View File

@@ -172,7 +172,7 @@ export default {
handleSaveList(event) {
let name = event.target.value
this.newListName = event.target.value = ''
// this.isEditing = false
this.isEditing = false
if (!name) return
createUserList({ name })
},

View File

@@ -9,9 +9,17 @@
<div :class="$style.setting">
<div :class="$style.info">
<span>{{ playbackRate.toFixed(2) }}x</span>
<base-btn min @click="handleUpdatePlaybackRate(100)">{{ $t('player__playback_rate_reset_btn') }}</base-btn>
<div :class="$style.control">
<base-checkbox
id="player__playback_preserves_pitch"
:model-value="appSetting['player.preservesPitch']"
:label="$t('player__playback_preserves_pitch')"
@update:model-value="updatePreservesPitch"
/>
<base-btn min @click="handleUpdatePlaybackRate(100)">{{ $t('player__playback_rate_reset_btn') }}</base-btn>
</div>
</div>
<base-slider-bar :class="$style.slider" :value="playbackRate * 100" :min="60" :max="200" @change="handleUpdatePlaybackRate" />
<base-slider-bar :class="$style.slider" :value="playbackRate * 100" :min="50" :max="200" @change="handleUpdatePlaybackRate" />
</div>
</template>
</material-popup-btn>
@@ -20,11 +28,17 @@
<script setup>
// import { computed } from '@common/utils/vueTools'
import { playbackRate } from '@renderer/store/player/playbackRate'
import { appSetting, updateSetting } from '@renderer/store/setting'
const handleUpdatePlaybackRate = (val) => {
window.app_event.setPlaybackRate(Math.round(val) / 100)
}
const updatePreservesPitch = (enabled) => {
updateSetting({ 'player.preservesPitch': enabled })
}
// const icon = computed(() => {
// return playbackRate.value == 0
// ? '#icon-volume-off-outline'
@@ -100,6 +114,11 @@ const handleUpdatePlaybackRate = (val) => {
line-height: 1;
}
}
.control {
align-items: center;
display: flex;
gap: 10px;
}
.slider {
width: 100%;

View File

@@ -0,0 +1,90 @@
<template>
<base-btn min :class="[$style.newPreset, {[$style.editing]: isEditing}]" :aria-label="$t('player__sound_effect_biquad_filter_save_btn')" @click="handleEditing($event)">
<svg-icon name="plus" />
<base-input ref="input" :class="$style.newPresetInput" :value="newPresetName" :placeholder="$t('player__sound_effect_biquad_filter_save_input')" @keyup.enter="handleSave($event)" @blur="handleSave($event)" />
</base-btn>
</template>
<script setup>
import { ref, nextTick } from '@common/utils/vueTools'
import { appSetting } from '@renderer/store/setting'
import { saveUserPitchShifterPreset } from '@renderer/store/soundEffect'
const isEditing = ref(false)
const input = ref(false)
const newPresetName = ref('')
const handleEditing = () => {
if (isEditing.value) return
// if (!this.newPresetName) this.newPresetName = this.listName
isEditing.value = true
nextTick(() => {
input.value.$el.focus()
})
}
const handleSave = (event) => {
let name = event.target.value.trim()
newPresetName.value = event.target.value = ''
isEditing.value = false
if (!name) return
if (name.length > 20) name = name.substring(0, 20)
saveUserPitchShifterPreset({
id: Date.now().toString(),
name,
playbackRate: appSetting['player.soundEffect.pitchShifter.playbackRate'],
})
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.newPreset {
position: relative;
border: 1px dashed var(--color-primary-font-hover);
// background-color: var(--color-main-background);
color: var(--color-primary-font-hover);
opacity: .7;
height: 22px;
&.editing {
opacity: 1;
width: 90px;
svg {
display: none;
}
.newPresetInput {
display: block;
}
}
:global {
.svg-icon {
vertical-align: 0;
}
}
}
.newPresetInput {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
// line-height: 16px;
background: none !important;
font-size: 12px;
text-align: center;
font-family: inherit;
box-sizing: border-box;
padding: 0 3px;
border-radius: 0;
display: none;
&::placeholder {
font-size: 12px;
}
}
</style>

View File

@@ -27,7 +27,7 @@
</div>
</div>
</div>
<div :class="['scroll', $style.saveList]">
<div :class="$style.saveList">
<base-btn v-for="item in userPresetList" :key="item.id" min @click="handleSetPreset(item)" @contextmenu="handleRemovePreset(item.id)">{{ item.name }}</base-btn>
<AddConvolutionPresetBtn v-if="userPresetList.length < 31" />
</div>
@@ -88,6 +88,7 @@ onMounted(() => {
flex-flow: column nowrap;
gap: 3px;
min-height: 0;
flex: none;
}
.convolution {
display: flex;

View File

@@ -11,7 +11,7 @@
<span :class="$style.value">{{ appSetting[`player.soundEffect.biquadFilter.hz${v}`] }}db</span>
</div>
</div>
<div :class="['scroll', $style.saveList]">
<div :class="$style.saveList">
<!-- <base-btn min @click="handleSetPreset(item)">{{ $t(`player__sound_effect_biquad_filter_preset_slow`) }}</base-btn> -->
<base-btn v-for="item in freqsPreset" :key="item.name" min @click="handleSetPreset(item)">{{ $t(`player__sound_effect_biquad_filter_preset_${item.name}`) }}</base-btn>
<base-btn v-for="item in userPresetList" :key="item.id" min @click="handleSetPreset(item)" @contextmenu="handleRemovePreset(item.id)">{{ item.name }}</base-btn>
@@ -83,6 +83,7 @@ onMounted(() => {
flex-flow: column nowrap;
gap: 8px;
min-height: 0;
flex: none;
}
.header {
display: flex;

View File

@@ -0,0 +1,152 @@
<template>
<div :class="$style.contnet">
<div class="player__sound_effect_title" :class="$style.header">
<h3>
{{ $t('player__sound_effect_pitch_shifter') }}
<svg-icon class="help-icon" name="information-slab-circle-outline" :aria-label="$t('player__sound_effect_pitch_shifter_tip')" />
</h3>
<base-btn min @click="handleSetPreset(1)">{{ $t('player__sound_effect_pitch_shifter_reset_btn') }}</base-btn>
</div>
<div :class="$style.eqList">
<div :class="$style.eqItem">
<span :class="$style.label">{{ playbackRate.toFixed(2) }}x</span>
<base-slider-bar :class="$style.slider" :value="playbackRate * 100" :min="50" :max="150" @change="handleUpdatePlaybackRate" />
</div>
</div>
<!-- <div :class="$style.saveList">
<base-btn v-for="num in semitones" :key="num" min @click="handleSetSemitones(num)">{{ $t(`player__sound_effect_pitch_shifter_preset_semitones`, { num: num > 0 ? `+${num}` : num }) }}</base-btn>
<base-btn v-for="item in userPresetList" :key="item.id" min @click="handleSetPreset(item.playbackRate)" @contextmenu="handleRemovePreset(item.id)">{{ item.name }}</base-btn>
<AddPitchShifterPresetBtn v-if="userPresetList.length < 31" />
</div> -->
</div>
</template>
<script setup>
import { computed } from '@common/utils/vueTools'
import { appSetting, updateSetting } from '@renderer/store/setting'
// import AddPitchShifterPresetBtn from './AddPitchShifterPresetBtn.vue'
// import { getUserPitchShifterPresetList, removeUserPitchShifterPreset } from '@renderer/store/soundEffect'
// import { semitones } from '@renderer/plugins/player'
// const setting = reactive({
// enabled: false,
// soundR: 5,
// speed: 25,
// })
const playbackRate = computed(() => appSetting['player.soundEffect.pitchShifter.playbackRate'])
const handleSetPreset = (value) => {
updateSetting({ 'player.soundEffect.pitchShifter.playbackRate': value })
}
// const handleSetSemitones = (value) => {
// // https://zpl.fi/pitch-shifting-in-web-audio-api/
// handleSetPreset(2 ** (value / 12))
// }
const handleUpdatePlaybackRate = (value) => {
value = parseFloat((Math.round(value) / 100).toFixed(2))
handleSetPreset(value)
}
// const userPresetList = ref([])
// const handleRemovePreset = id => {
// removeUserPitchShifterPreset(id)
// }
// onMounted(() => {
// getUserPitchShifterPresetList().then(list => {
// userPresetList.value = list
// })
// })
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.contnet {
padding-top: 15px;
position: relative;
display: flex;
flex-flow: column nowrap;
gap: 8px;
min-height: 0;
flex: none;
&:before {
.mixin-after;
position: absolute;
top: 0;
height: 1px;
width: 100%;
border-top: 1px dashed var(--color-primary-light-100-alpha-700);
}
}
.header {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
padding-bottom: 5px;
// padding-top: 5px;
}
.eqList {
display: flex;
flex-flow: column nowrap;
gap: 15px;
width: 100%;
}
.eqItem {
display: flex;
flex-flow: row nowrap;
gap: 8px;
}
.label {
flex: none;
// width: 50px;
font-size: 12px;
}
.value {
flex: none;
width: 40px;
font-size: 12px;
text-align: center;
&.active {
color: var(--color-primary-font);
}
}
.footer {
display: flex;
flex-flow: row nowrap;
// justify-content: space-between;
justify-content: center;
align-items: center;
// font-size: 13px;
span {
line-height: 1;
}
}
.slider {
flex: auto;
}
.checkbox {
margin-right: 10px;
font-size: 13px;
}
.saveList {
display: flex;
flex-flow: row wrap;
margin-top: 10px;
gap: 10px;
}
</style>

View File

@@ -8,11 +8,12 @@
<!-- <main :class="$style.main"> -->
<!-- <h2 :class="$style.title">{{ $t('theme_edit_modal__title') }}</h2> -->
<div :class="$style.content">
<div :class="$style.row">
<div :class="['scroll', $style.row]">
<AudioConvolution />
<PitchShifter />
<AudioPanner />
</div>
<div :class="$style.row">
<div :class="['scroll', $style.row]">
<BiquadFilter />
</div>
</div>
@@ -31,6 +32,7 @@ import { ref } from '@common/utils/vueTools'
import BiquadFilter from './BiquadFilter'
import AudioPanner from './AudioPanner'
import AudioConvolution from './AudioConvolution'
import PitchShifter from './PitchShifter.vue'
defineProps({
teleport: {
@@ -97,9 +99,9 @@ const visible = ref(false)
.content {
display: flex;
flex-flow: row nowrap;
padding: 0 15px;
padding: 0 5px;
margin: 15px 0;
gap: 30px;
gap: 10px;
position: relative;
min-height: 0;
@@ -129,6 +131,7 @@ const visible = ref(false)
display: flex;
gap: 15px;
flex-flow: column nowrap;
padding: 0 10px;
}
</style>

View File

@@ -1,24 +1,24 @@
<template lang="pug">
material-modal(:show="isShowChangeLog" @close="isShowChangeLog = false" max-width="60%")
material-modal(:show="isShowChangeLog" max-width="60%" @close="isShowChangeLog = false")
main(:class="$style.main")
h2 当前版本更新日志
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 当前版本{{versionInfo.version}}
h3 当前版本{{ versionInfo.version }}
template(v-if="info.desc")
h3 版本变化
pre(:class="$style.desc" v-text="info.desc")
div(:class="[$style.history, $style.desc]" v-if="info.history.length")
div(v-if="info.history.length" :class="[$style.history, $style.desc]")
h3 历史版本
div(:class="$style.item" v-for="ver in info.history")
h4 v{{ver.version}}
div(v-for="(ver, index) in info.history" :key="index" :class="$style.item")
h4 v{{ ver.version }}
pre(v-text="ver.desc")
div(:class="$style.footer")
div(:class="$style.desc")
p 📢&nbsp;为了减少疑问我们墙裂建议阅读版本更新日志来了解当前所用版本的变化
p 📢&nbsp;若遇到问题可以去设置-关于查看常见问题
p(v-if="!info.isLatest") 🚀&nbsp;发现新版本v{{versionInfo.newVersion.version}}建议 去设置-软件更新 更新新版本
p(v-if="!info.isLatest") 🚀&nbsp;发现新版本v{{ versionInfo.newVersion.version }}建议 去设置-软件更新 更新新版本
</template>
<script>

View File

@@ -25,7 +25,7 @@ export default {
setup() {
const visible = ref(false)
const visibleProgress = ref(false)
const dom_btn = ref<HTMLElement | null>(null)
const dom_btn = ref(null)
const handleShowPopup = (evt) => {
if (visible.value) {

View File

@@ -1,27 +1,27 @@
<template lang="pug">
div(:class="$style.footerLeftControlBtns")
button(:class="[$style.footerLeftControlBtn, $style.lrcBtn]" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric" :aria-label="toggleDesktopLyricBtnTitle")
svg(v-show="appSetting['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="!appSetting['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')
button(:class="[$style.footerLeftControlBtn, { [$style.active]: appSetting['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')
button(: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')
button(: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')
button(:class="[$style.footerLeftControlBtn, $style.lrcBtn]" :aria-label="toggleDesktopLyricBtnTitle" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric")
svg(v-show="appSetting['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="!appSetting['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")
button(:class="[$style.footerLeftControlBtn, { [$style.active]: appSetting['player.audioVisualization'] }]" :aria-label="$t('audio_visualization')" @click="toggleAudioVisualization")
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")
button(:class="[$style.footerLeftControlBtn, { [$style.active]: isShowLrcSelectContent }]" :aria-label="$t('lyric__select')" @click="toggleVisibleLrc")
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")
button(:class="[$style.footerLeftControlBtn, {[$style.active]: isShowPlayComment}]" :aria-label="$t('comment__show')" @click="toggleVisibleComment")
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")
common-sound-effect-btn
common-playback-rate-btn
common-volume-btn
common-toggle-play-mode-btn
button(: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="playMusicInfo.musicInfo")
button(:class="$style.footerLeftControlBtn" :aria-label="$t('player__add_music_to')" @click="isShowAddMusicTo = true")
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" :music-info="playMusicInfo.musicInfo")
</template>

View File

@@ -83,7 +83,7 @@ export default {
required: true,
},
},
emits: ['updateLyric', 'update:modelValue'],
emits: ['updateLyric', 'update:model-value'],
setup(props, { emit }) {
// const appSetting = useRefGetter('appSetting')
// const playDetailSetting = useRefGetter('playDetailSetting')
@@ -98,7 +98,7 @@ export default {
const location = computed(() => props.xy)
const onHide = () => {
emit('update:modelValue', false)
emit('update:model-value', false)
}
const setFontAlign = val => {

View File

@@ -1,24 +1,24 @@
<template lang="pug">
div(:class="$style.container")
ul
li(v-for="(item, index) in comments" :key="item.id" :class="$style.listItem")
li(v-for="item in comments" :key="item.id" :class="$style.listItem")
div(:class="$style.content")
div(:class="$style.left")
img( :class="$style.avatar" :src="item.avatar || commentDefImg" @error="handleUserImg")
div(:class="$style.right")
div(:class="$style.info")
div(:class="$style.baseInfo")
div.select(:class="$style.name") {{item.userName}}
div.select(:class="$style.name") {{ item.userName }}
div(:class="$style.metaInfo")
time(:class="$style.label" v-if="item.timeStr") {{timeFormat(item.timeStr)}}
div(:class="$style.label" v-if="item.location") {{$t('comment__location', { location: item.location })}}
div(:class="$style.likes" v-if="item.likedCount != null")
svg(:class="$style.likesIcon" 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-thumbs-up')
time(v-if="item.timeStr" :class="$style.label") {{ timeFormat(item.timeStr) }}
div(v-if="item.location" :class="$style.label") {{$t('comment__location', { location: item.location })}}
div(v-if="item.likedCount != null" :class="$style.likes")
svg(:class="$style.likesIcon" 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-thumbs-up")
| {{item.likedCount}}
p.select(:class="$style.comment_text") {{item.text}}
p.select(:class="$style.comment_text") {{ item.text }}
div(v-if="item.images?.length" :class="$style.comment_images")
img(v-for="url in item.images" :src="url" loading="lazy" decoding="async")
img(v-for="(url, index) in item.images" :key="index" :src="url" loading="lazy" decoding="async")
comment-floor(v-if="item.reply && item.reply.length" :class="$style.reply_floor" :comments="item.reply")
</template>

View File

@@ -1,37 +1,37 @@
<template lang="pug">
div.comment(:class="$style.comment" ref="dom_container")
div.comment(ref="dom_container" :class="$style.comment")
div(:class="$style.commentHeader")
h3 {{$t('comment__title', { name: title })}}
h3 {{ $t('comment__title', { name: title }) }}
div(:class="$style.commentHeaderBtns")
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" :aria-label="$t('comment__refresh')" @click="handleShowComment")
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')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-close')
svg(version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" space="preserve")
use(xlink:href="#icon-close")
div(:class="$style.commentMain")
template(v-if="available")
header(:class="$style.tab_header")
button(type="button" @click="handleToggleTab('hot')" :class="[$style.commentType, { [$style.active]: tabActiveId == 'hot' }]") {{$t('comment__hot_title')}} ({{hotComment.total}})
button(type="button" @click="handleToggleTab('new')" :class="[$style.commentType, { [$style.active]: tabActiveId == 'new' }]") {{$t('comment__new_title')}} ({{newComment.total}})
main(:class="$style.tab_main" ref="dom_tabMain")
button(type="button" :class="[$style.commentType, { [$style.active]: tabActiveId == 'hot' }]" @click="handleToggleTab('hot')") {{ $t('comment__hot_title') }} ({{ hotComment.total }})
button(type="button" :class="[$style.commentType, { [$style.active]: tabActiveId == 'new' }]" @click="handleToggleTab('new')") {{ $t('comment__new_title') }} ({{ newComment.total }})
main(ref="dom_tabMain" :class="$style.tab_main")
div(:class="$style.tab_content")
div.scroll(:class="$style.tab_content_scroll" ref="dom_commentHot")
p(:class="$style.commentLabel" style="cursor: pointer;" v-if="hotComment.isLoadError" @click="handleGetHotComment(currentMusicInfo, hotComment.nextPage, hotComment.limit)") {{$t('comment__hot_load_error')}}
p(:class="$style.commentLabel" v-else-if="hotComment.isLoading && !hotComment.list.length") {{$t('comment__hot_loading')}}
div.scroll(ref="dom_commentHot" :class="$style.tab_content_scroll")
p(v-if="hotComment.isLoadError" :class="$style.commentLabel" style="cursor: pointer;" @click="handleGetHotComment(currentMusicInfo, hotComment.nextPage, hotComment.limit)") {{ $t('comment__hot_load_error') }}
p(v-else-if="hotComment.isLoading && !hotComment.list.length" :class="$style.commentLabel") {{ $t('comment__hot_loading') }}
comment-floor(v-if="!hotComment.isLoadError && hotComment.list.length" :class="[$style.commentFloor, hotComment.isLoading ? $style.loading : null]" :comments="hotComment.list")
p(:class="$style.commentLabel" v-else-if="!hotComment.isLoadError && !hotComment.isLoading") {{$t('comment__no_content')}}
p(v-else-if="!hotComment.isLoadError && !hotComment.isLoading" :class="$style.commentLabel") {{ $t('comment__no_content') }}
div(:class="$style.pagination")
material-pagination(:count="hotComment.total" :btnLength="5" :limit="hotComment.limit" :page="hotComment.page" @btn-click="handleToggleHotCommentPage")
material-pagination(:count="hotComment.total" :btn-length="5" :limit="hotComment.limit" :page="hotComment.page" @btn-click="handleToggleHotCommentPage")
div(:class="$style.tab_content")
div.scroll(:class="$style.tab_content_scroll" ref="dom_commentNew")
p(:class="$style.commentLabel" style="cursor: pointer;" v-if="newComment.isLoadError" @click="handleGetNewComment(currentMusicInfo, newComment.nextPage, newComment.limit)") {{$t('comment__new_load_error')}}
p(:class="$style.commentLabel" v-else-if="newComment.isLoading && !newComment.list.length") {{$t('comment__new_loading')}}
div.scroll(ref="dom_commentNew" :class="$style.tab_content_scroll")
p(v-if="newComment.isLoadError" :class="$style.commentLabel" style="cursor: pointer;" @click="handleGetNewComment(currentMusicInfo, newComment.nextPage, newComment.limit)") {{ $t('comment__new_load_error') }}
p(v-else-if="newComment.isLoading && !newComment.list.length" :class="$style.commentLabel") {{ $t('comment__new_loading') }}
comment-floor(v-if="!newComment.isLoadError && newComment.list.length" :class="[$style.commentFloor, newComment.isLoading ? $style.loading : null]" :comments="newComment.list")
p(:class="$style.commentLabel" v-else-if="!newComment.isLoadError && !newComment.isLoading") {{$t('comment__no_content')}}
p(v-else-if="!newComment.isLoadError && !newComment.isLoading" :class="$style.commentLabel") {{ $t('comment__no_content') }}
div(:class="$style.pagination")
material-pagination(:count="newComment.total" :btnLength="5" :limit="newComment.limit" :page="newComment.page" @btn-click="handleToggleCommentPage")
material-pagination(:count="newComment.total" :btn-length="5" :limit="newComment.limit" :page="newComment.page" @btn-click="handleToggleCommentPage")
div(v-else :class="$style.unavailable")
p {{$t('comment__unavailable')}}
</template>

View File

@@ -1,54 +1,54 @@
<template lang="pug">
transition(enter-active-class="animated slideInRight" leave-active-class="animated slideOutDown" @after-enter="handleAfterEnter" @after-leave="handleAfterLeave")
div(:class="[$style.container, { [$style.fullscreen]: isFullscreen }]" @contextmenu="handleContextMenu" v-if="isShowPlayerDetail")
div(v-if="isShowPlayerDetail" :class="[$style.container, { [$style.fullscreen]: isFullscreen }]" @contextmenu="handleContextMenu")
div(:class="$style.bg")
//- div(:class="$style.bg" :style="bgStyle")
//- div(:class="$style.bg2")
div(:class="[$style.header, $style.controlBtnLeft]" v-if="appSetting['common.controlBtnPosition'] == 'left'")
div(v-if="appSetting['common.controlBtnPosition'] == 'left'" :class="[$style.header, $style.controlBtnLeft]")
div(:class="$style.controBtn")
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')
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.fullscreenExit" :aria-label="$t('fullscreen_exit')" @click="fullscreenExit")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='100%')
use(xlink:href='#icon-fullscreen-exit')
svg(:class="$style.controBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%")
use(xlink:href="#icon-fullscreen-exit")
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')
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" :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)
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(v-else :class="[$style.header, $style.controlBtnRight]")
div(:class="$style.controBtn")
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')
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.fullscreenExit" :aria-label="$t('fullscreen_exit')" @click="fullscreenExit")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='60%')
use(xlink:href='#icon-fullscreen-exit')
svg(:class="$style.controBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="60%")
use(xlink:href="#icon-fullscreen-exit")
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')
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" :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')
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")
div(:class="[$style.main, {[$style.showComment]: isShowPlayComment}]")
div.left(:class="$style.left")
//- div(:class="$style.info")
div(:class="$style.info")
img(:class="$style.img" :src="musicInfo.pic" v-if="musicInfo.pic")
img(v-if="musicInfo.pic" :class="$style.img" :src="musicInfo.pic")
div.description(:class="$style.description")
p {{$t('player__music_name')}}{{musicInfo.name}}
p {{$t('player__music_singer')}}{{musicInfo.singer}}
p(v-if="musicInfo.album") {{$t('player__music_album')}}{{musicInfo.album}}
p {{ $t('player__music_name') }}{{ musicInfo.name }}
p {{ $t('player__music_singer') }}{{ musicInfo.singer }}
p(v-if="musicInfo.album") {{ $t('player__music_album') }}{{musicInfo.album}}
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
LyricPlayer(v-if="visibled")
music-comment(:class="$style.comment" :show="isShowPlayComment" :musicInfo="playMusicInfo.musicInfo" @close="hideComment" v-if="visibled")
music-comment(v-if="visibled" :class="$style.comment" :show="isShowPlayComment" :music-info="playMusicInfo.musicInfo" @close="hideComment")
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
play-bar(v-if="visibled")
transition(enter-active-class="animated-slow fadeIn" leave-active-class="animated-slow fadeOut")

View File

@@ -1,52 +1,52 @@
<template lang="pug">
material-modal(:show="versionInfo.showModal" @close="handleClose" max-width="60%")
main(:class="$style.main" v-if="versionInfo.isLatest")
material-modal(:show="versionInfo.showModal" max-width="60%" @close="handleClose")
main(v-if="versionInfo.isLatest" :class="$style.main")
h2 🎉 已是最新版本 🎉
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 最新版本{{versionInfo.newVersion?.version}}
h3 当前版本{{versionInfo.version}}
h3 最新版本{{ versionInfo.newVersion?.version }}
h3 当前版本{{ versionInfo.version }}
h3 版本变化
pre(:class="$style.desc" v-text="versionInfo.newVersion?.desc")
div(:class="$style.footer")
div(:class="$style.btns")
base-btn(v-if="versionInfo.status == 'checking'" :class="$style.btn" disabled) 检查更新中...
base-btn(v-else :class="$style.btn" @click="handleCheckUpdate") 重新检查更新
main(:class="$style.main" v-else-if="versionInfo.isUnknown")
main(v-else-if="versionInfo.isUnknown" :class="$style.main")
h2 获取最新版本信息失败
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 当前版本{{versionInfo.version}}
h3 当前版本{{ versionInfo.version }}
div(:class="$style.desc")
p 更新信息获取失败可能是无法访问Github导致的请手动检查更新
p
| 检查方法打开
base-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
base-btn(min aria-label="点击打开" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')") 软件发布页
|
base-btn(min @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
base-btn(min aria-label="点击打开" @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')") 网盘
| (密码
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
strong.hover(aria-label="点击复制" @click="handleCopy('glqw')") glqw
| )查看它们的
strong 版本号
| 与当前版本({{versionInfo.version}})对比是否一样
| 与当前版本({{ versionInfo.version }})对比是否一样
p 若一样则不必理会该弹窗直接关闭即可否则请手动下载新版本更新
div(:class="$style.footer")
div(:class="$style.btns")
base-btn(v-if="versionInfo.status == 'error'" :class="$style.btn" @click="handleCheckUpdate") 重新检查更新
base-btn(v-else :class="$style.btn" disabled) 检查更新中...
main(:class="$style.main" v-else-if="versionInfo.status == 'downloaded'")
main(v-else-if="versionInfo.status == 'downloaded'" :class="$style.main")
h2 🚀程序更新🚀
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 最新版本{{versionInfo.newVersion?.version}}
h3 当前版本{{versionInfo.version}}
h3 最新版本{{ versionInfo.newVersion?.version }}
h3 当前版本{{ versionInfo.version }}
h3 版本变化
pre(:class="$style.desc" v-text="versionInfo.newVersion?.desc")
div(:class="[$style.history, $style.desc]" v-if="history.length")
div(v-if="history.length" :class="[$style.history, $style.desc]")
h3 历史版本
div(:class="$style.item" v-for="ver in history")
h4 v{{ver.version}}
div(v-for="(ver, index) in history" :key="index" :class="$style.item")
h4 v{{ ver.version }}
pre(v-text="ver.desc")
div(:class="$style.footer")
div(:class="$style.desc")
@@ -58,37 +58,37 @@ material-modal(:show="versionInfo.showModal" @close="handleClose" max-width="60%
strong 关闭程序时
| 自动更新~
div(:class="$style.btns")
base-btn(:class="$style.btn" @click.onec="handleRestartClick") 立即重启更新
main(:class="$style.main" v-else)
base-btn(:class="$style.btn" @click="handleRestartClick") 立即重启更新
main(v-else :class="$style.main")
h2 🌟发现新版本🌟
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 最新版本{{versionInfo.newVersion?.version}}
h3 当前版本{{versionInfo.version}}
h3 最新版本{{ versionInfo.newVersion?.version }}
h3 当前版本{{ versionInfo.version }}
h3 版本变化
pre(:class="$style.desc" v-text="versionInfo.newVersion?.desc")
div(:class="[$style.history, $style.desc]" v-if="history.length")
div(v-if="history.length" :class="[$style.history, $style.desc]")
h3 历史版本
div(:class="$style.item" v-for="ver in history")
h4 v{{ver.version}}
div(v-for="(ver, index) in history" :key="index" :class="$style.item")
h4 v{{ ver.version }}
pre(v-text="ver.desc")
div(:class="$style.footer")
div(:class="$style.desc")
p 发现有新版本啦你可以尝试使用自动更新或手动更新
p 手动更新可以去&nbsp;
strong.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
strong.hover.underline(aria-label="点击打开" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')") 软件发布页
| &nbsp;&nbsp;
strong.hover.underline(@click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
strong.hover.underline(aria-label="点击打开" @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')") 网盘
| (密码
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
strong.hover(aria-label="点击复制" @click="handleCopy('glqw')") glqw
| )&nbsp;下载
p 国内Windows/MAC用户推荐到网盘下载若遇到问题可以看
strong.hover.underline(@click="handleOpenUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq')" aria-label="点击打开") 常见问题
p(v-if="progress") 当前下载进度{{progress}}
strong.hover.underline(aria-label="点击打开" @click="handleOpenUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq')") 常见问题
p(v-if="progress") 当前下载进度{{ progress }}
p(v-else) &nbsp;
div(:class="$style.btns")
base-btn(:class="$style.btn2" @click="handleIgnoreClick") {{ isIgnored ? '取消忽略' : '忽略更新该版本'}}
base-btn(:class="$style.btn2" @click="handleIgnoreClick") {{ isIgnored ? '取消忽略' : '忽略更新该版本' }}
base-btn(v-if="versionInfo.status == 'downloading'" :class="$style.btn2" disabled) 下载更新中...
base-btn(v-else :class="$style.btn2" @click="handleDonwloadClick") 下载更新
</template>

View File

@@ -8,7 +8,7 @@
</template>
<script setup lang="ts">
import { ref, defineExpose } from '@common/utils/vueTools'
import { ref } from '@common/utils/vueTools'
const visible = ref(false)
const dom_btn = ref<HTMLElement | null>(null)

View File

@@ -8,7 +8,7 @@
:placeholder="placeholder"
@focus="handleFocus"
@blur="handleBlur"
@input="$emit('update:modelValue', text)"
@input="$emit('update:model-value', text)"
@change="sendEvent('change')"
@keyup.enter="handleSearch"
@keydown.arrow-down.arrow-up.prevent
@@ -82,7 +82,7 @@ export default {
default: false,
},
},
emits: ['update:modelValue', 'event'],
emits: ['update:model-value', 'event'],
data() {
return {
isShow: false,
@@ -174,11 +174,11 @@ export default {
str = str.replace(/\s+/g, ' ')
let dom_input = this.$refs.dom_input
this.text = this.text.substring(0, dom_input.selectionStart) + str + this.text.substring(dom_input.selectionEnd, this.text.length)
this.$emit('update:modelValue', this.text)
this.$emit('update:model-value', this.text)
},
handleClearList() {
this.text = ''
this.$emit('update:modelValue', this.text)
this.$emit('update:model-value', this.text)
this.sendEvent('submit')
},
},

View File

@@ -1,5 +1,5 @@
import { onBeforeUnmount, watch } from '@common/utils/vueTools'
import { setPlaybackRate as setPlayerPlaybackRate } from '@renderer/plugins/player'
import { setPlaybackRate as setPlayerPlaybackRate, setPreservesPitch } from '@renderer/plugins/player'
import { debounce } from '@common/utils'
// import { HOTKEY_PLAYER } from '@common/hotKey'
@@ -11,6 +11,7 @@ export default () => {
setPlaybackRate(appSetting['player.playbackRate'])
setPlayerPlaybackRate(appSetting['player.playbackRate'])
setPreservesPitch(appSetting['player.preservesPitch'])
const handleSetPlaybackRate = (num: number) => {
@@ -41,6 +42,11 @@ export default () => {
})
watch(() => appSetting['player.preservesPitch'], preservesPitch => {
setPreservesPitch(preservesPitch)
})
// window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)
// window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)
window.app_event.on('setPlaybackRate', handleSetPlaybackRate)

View File

@@ -10,6 +10,7 @@ import {
stopPanner,
setConvolverMainGain,
setConvolverSendGain,
setPitchShifter,
} from '@renderer/plugins/player'
import { appSetting } from '@renderer/store/setting'
@@ -51,7 +52,7 @@ const loadBuffer = async(name: string) => new Promise<AudioBuffer>((resolve, rej
})
export default () => {
console.log(appSetting['player.soundEffect.panner.enable'])
// console.log(appSetting['player.soundEffect.panner.enable'])
if (appSetting['player.soundEffect.panner.enable']) startPanner()
setPannerSoundR(appSetting['player.soundEffect.panner.soundR'] / 10)
setPannerSpeed(2 * (appSetting['player.soundEffect.panner.speed'] / 10))
@@ -66,6 +67,9 @@ export default () => {
setConvolver(buffer, appSetting['player.soundEffect.convolution.mainGain'] / 10, appSetting['player.soundEffect.convolution.sendGain'] / 10)
})
}
if (appSetting['player.soundEffect.pitchShifter.playbackRate'] != 1) {
setPitchShifter(appSetting['player.soundEffect.pitchShifter.playbackRate'])
}
watch(() => appSetting['player.soundEffect.panner.enable'], (enable) => {
@@ -139,6 +143,10 @@ export default () => {
bfs.get('hz16000')!.gain.value = hz16000
})
watch(() => appSetting['player.soundEffect.pitchShifter.playbackRate'], (playbackRate) => {
setPitchShifter(playbackRate)
})
// window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)
// window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)

View File

@@ -84,6 +84,7 @@ export default {
pointer-events: none;
// text-align: justify;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.3);
white-space: pre;
}
:global(.tips-fade-enter-active), :global(.tips-fade-leave-active) {

View File

@@ -38,12 +38,19 @@ export const convolutions = [
{ name: 'feedback_spring', mainGain: 1.8, sendGain: 0.8, source: 'feedback-spring.wav' },
// { name: 'tim_omni_rear_blend', mainGain: 1.8, sendGain: 0.8, source: 'tim-omni-rear-blend.wav' },
] as const
// 半音
// export const semitones = [-1.5, -1, -0.5, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] as const
let convolver: ConvolverNode
let convolverSourceGainNode: GainNode
let convolverOutputGainNode: GainNode
let convolverDynamicsCompressor: DynamicsCompressorNode
let gainNode: GainNode
let panner: PannerNode
let pitchShifterNode: AudioWorkletNode
let pitchShifterNodePitchFactor: AudioParam
let pitchShifterNodeLoadStatus: 'none' | 'loading' | 'unconnect' | 'connected' = 'none'
let pitchShifterNodeTempValue = 1
export const soundR = 0.5
@@ -53,6 +60,7 @@ export const createAudio = () => {
audio.controls = false
audio.autoplay = true
audio.preload = 'auto'
audio.crossOrigin = 'anonymous'
}
const initAnalyser = () => {
@@ -100,26 +108,21 @@ const initAdvancedAudioFeatures = () => {
if (audioContext) return
if (!audio) throw new Error('audio not defined')
audioContext = new window.AudioContext()
mediaSource = audioContext.createMediaElementSource(audio)
initAnalyser()
mediaSource.connect(analyser)
// analyser.connect(audioContext.destination)
initBiquadFilter()
analyser.connect(biquads.get(`hz${freqs[0]}`) as BiquadFilterNode)
initConvolver()
initPanner()
initGain()
// source -> analyser -> biquadFilter -> [(convolver & convolverSource)->convolverDynamicsCompressor] -> panner -> gain
mediaSource = audioContext.createMediaElementSource(audio)
mediaSource.connect(analyser)
analyser.connect(biquads.get(`hz${freqs[0]}`) as BiquadFilterNode)
const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1) as Freqs}`) as BiquadFilterNode)
lastBiquadFilter.connect(convolverSourceGainNode)
lastBiquadFilter.connect(convolver)
initPanner()
convolverDynamicsCompressor.connect(panner)
initGain()
panner.connect(gainNode)
gainNode.connect(audioContext.destination)
}
@@ -216,6 +219,101 @@ export const startPanner = () => {
}, pannerInfo.speed * 10)
}
let isConnected = true
const connectNode = () => {
if (isConnected) return
console.log('connect Node')
analyser?.connect(biquads.get(`hz${freqs[0]}`) as BiquadFilterNode)
isConnected = true
if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') {
disconnectPitchShifterNode()
}
}
const disconnectNode = () => {
if (!isConnected) return
console.log('disconnect Node')
analyser?.disconnect()
isConnected = false
if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') {
disconnectPitchShifterNode()
}
}
const connectPitchShifterNode = () => {
console.log('connect Pitch Shifter Node')
audio!.addEventListener('playing', connectNode)
audio!.addEventListener('pause', disconnectNode)
audio!.addEventListener('waiting', disconnectNode)
audio!.addEventListener('emptied', disconnectNode)
if (audio!.paused) disconnectNode()
const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1) as Freqs}`) as BiquadFilterNode)
lastBiquadFilter.disconnect()
lastBiquadFilter.connect(pitchShifterNode)
pitchShifterNode.connect(convolver)
pitchShifterNode.connect(convolverSourceGainNode)
// convolverDynamicsCompressor.disconnect(panner)
// convolverDynamicsCompressor.connect(pitchShifterNode)
// pitchShifterNode.connect(panner)
pitchShifterNodeLoadStatus = 'connected'
pitchShifterNodePitchFactor.value = pitchShifterNodeTempValue
}
const disconnectPitchShifterNode = () => {
console.log('disconnect Pitch Shifter Node')
const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1) as Freqs}`) as BiquadFilterNode)
lastBiquadFilter.disconnect()
lastBiquadFilter.connect(convolver)
lastBiquadFilter.connect(convolverSourceGainNode)
pitchShifterNodeLoadStatus = 'unconnect'
audio!.removeEventListener('playing', connectNode)
audio!.removeEventListener('pause', disconnectNode)
audio!.removeEventListener('waiting', disconnectNode)
audio!.removeEventListener('emptied', disconnectNode)
connectNode()
}
const loadPitchShifterNode = () => {
pitchShifterNodeLoadStatus = 'loading'
initAdvancedAudioFeatures()
// source -> analyser -> biquadFilter -> audioWorklet(pitch shifter) -> [(convolver & convolverSource)->convolverDynamicsCompressor] -> panner -> gain
void audioContext.audioWorklet.addModule(new URL(
/* webpackChunkName: 'pitch_shifter.audioWorklet' */
'./pitch-shifter/phase-vocoder.js',
import.meta.url,
)).then(() => {
console.log('pitch shifter audio worklet loaded')
// https://github.com/olvb/phaze/issues/26#issuecomment-1574629971
pitchShifterNode = new AudioWorkletNode(audioContext, 'phase-vocoder-processor', { outputChannelCount: [2] })
let pitchFactorParam = pitchShifterNode.parameters.get('pitchFactor')
if (!pitchFactorParam) return
pitchShifterNodePitchFactor = pitchFactorParam
pitchShifterNodeLoadStatus = 'unconnect'
if (pitchShifterNodeTempValue == 1) return
connectPitchShifterNode()
})
}
export const setPitchShifter = (val: number) => {
// console.log('setPitchShifter', val)
pitchShifterNodeTempValue = val
switch (pitchShifterNodeLoadStatus) {
case 'loading':
break
case 'none':
loadPitchShifterNode()
break
case 'connected':
// a: 1 = 半音
// value = 2 ** (a / 12)
pitchShifterNodePitchFactor.value = val
break
case 'unconnect':
connectPitchShifterNode()
break
}
}
export const hasInitedAdvancedAudioFeatures = (): boolean => audioContext != null
export const setResource = (src: string) => {
@@ -253,6 +351,11 @@ export const setPlaybackRate = (rate: number) => {
audio.playbackRate = rate
}
export const setPreservesPitch = (preservesPitch: boolean) => {
if (!audio) return
audio.preservesPitch = preservesPitch
}
export const getMute = (): boolean => {
return audio?.muted ?? false
}

View File

@@ -0,0 +1,500 @@
// https://github.com/indutny/fft.js
function FFT(size) {
this.size = size | 0
if (this.size <= 1 || (this.size & (this.size - 1)) !== 0) { throw new Error('FFT size must be a power of two and bigger than 1') }
this._csize = size << 1
// NOTE: Use of `var` is intentional for old V8 versions
let table = new Array(this.size * 2)
for (let i = 0; i < table.length; i += 2) {
const angle = Math.PI * i / this.size
table[i] = Math.cos(angle)
table[i + 1] = -Math.sin(angle)
}
this.table = table
// Find size's power of two
let power = 0
for (let t = 1; this.size > t; t <<= 1) { power++ }
// Calculate initial step's width:
// * If we are full radix-4 - it is 2x smaller to give inital len=8
// * Otherwise it is the same as `power` to give len=4
this._width = power % 2 === 0 ? power - 1 : power
// Pre-compute bit-reversal patterns
this._bitrev = new Array(1 << this._width)
for (let j = 0; j < this._bitrev.length; j++) {
this._bitrev[j] = 0
for (let shift = 0; shift < this._width; shift += 2) {
let revShift = this._width - shift - 2
this._bitrev[j] |= ((j >>> shift) & 3) << revShift
}
}
this._out = null
this._data = null
this._inv = 0
}
FFT.prototype.fromComplexArray = function fromComplexArray(complex, storage) {
let res = storage || new Array(complex.length >>> 1)
for (let i = 0; i < complex.length; i += 2) { res[i >>> 1] = complex[i] }
return res
}
FFT.prototype.createComplexArray = function createComplexArray() {
const res = new Array(this._csize)
for (let i = 0; i < res.length; i++) { res[i] = 0 }
return res
}
FFT.prototype.toComplexArray = function toComplexArray(input, storage) {
let res = storage || this.createComplexArray()
for (let i = 0; i < res.length; i += 2) {
res[i] = input[i >>> 1]
res[i + 1] = 0
}
return res
}
FFT.prototype.completeSpectrum = function completeSpectrum(spectrum) {
let size = this._csize
let half = size >>> 1
for (let i = 2; i < half; i += 2) {
spectrum[size - i] = spectrum[i]
spectrum[size - i + 1] = -spectrum[i + 1]
}
}
FFT.prototype.transform = function transform(out, data) {
if (out === data) { throw new Error('Input and output buffers must be different') }
this._out = out
this._data = data
this._inv = 0
this._transform4()
this._out = null
this._data = null
}
FFT.prototype.realTransform = function realTransform(out, data) {
if (out === data) { throw new Error('Input and output buffers must be different') }
this._out = out
this._data = data
this._inv = 0
this._realTransform4()
this._out = null
this._data = null
}
FFT.prototype.inverseTransform = function inverseTransform(out, data) {
if (out === data) { throw new Error('Input and output buffers must be different') }
this._out = out
this._data = data
this._inv = 1
this._transform4()
for (let i = 0; i < out.length; i++) { out[i] /= this.size }
this._out = null
this._data = null
}
// radix-4 implementation
//
// NOTE: Uses of `var` are intentional for older V8 version that do not
// support both `let compound assignments` and `const phi`
FFT.prototype._transform4 = function _transform4() {
let out = this._out
let size = this._csize
// Initial step (permute and transform)
let width = this._width
let step = 1 << width
let len = (size / step) << 1
let outOff
let t
let bitrev = this._bitrev
if (len === 4) {
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
const off = bitrev[t]
this._singleTransform2(outOff, off, step)
}
} else {
// len === 8
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
const off = bitrev[t]
this._singleTransform4(outOff, off, step)
}
}
// Loop through steps in decreasing order
let inv = this._inv ? -1 : 1
let table = this.table
for (step >>= 2; step >= 2; step >>= 2) {
len = (size / step) << 1
let quarterLen = len >>> 2
// Loop through offsets in the data
for (outOff = 0; outOff < size; outOff += len) {
// Full case
let limit = outOff + quarterLen
for (let i = outOff, k = 0; i < limit; i += 2, k += step) {
const A = i
const B = A + quarterLen
const C = B + quarterLen
const D = C + quarterLen
// Original values
const Ar = out[A]
const Ai = out[A + 1]
const Br = out[B]
const Bi = out[B + 1]
const Cr = out[C]
const Ci = out[C + 1]
const Dr = out[D]
const Di = out[D + 1]
// Middle values
const MAr = Ar
const MAi = Ai
const tableBr = table[k]
const tableBi = inv * table[k + 1]
const MBr = Br * tableBr - Bi * tableBi
const MBi = Br * tableBi + Bi * tableBr
const tableCr = table[2 * k]
const tableCi = inv * table[2 * k + 1]
const MCr = Cr * tableCr - Ci * tableCi
const MCi = Cr * tableCi + Ci * tableCr
const tableDr = table[3 * k]
const tableDi = inv * table[3 * k + 1]
const MDr = Dr * tableDr - Di * tableDi
const MDi = Dr * tableDi + Di * tableDr
// Pre-Final values
const T0r = MAr + MCr
const T0i = MAi + MCi
const T1r = MAr - MCr
const T1i = MAi - MCi
const T2r = MBr + MDr
const T2i = MBi + MDi
const T3r = inv * (MBr - MDr)
const T3i = inv * (MBi - MDi)
// Final values
const FAr = T0r + T2r
const FAi = T0i + T2i
const FCr = T0r - T2r
const FCi = T0i - T2i
const FBr = T1r + T3i
const FBi = T1i - T3r
const FDr = T1r - T3i
const FDi = T1i + T3r
out[A] = FAr
out[A + 1] = FAi
out[B] = FBr
out[B + 1] = FBi
out[C] = FCr
out[C + 1] = FCi
out[D] = FDr
out[D + 1] = FDi
}
}
}
}
// radix-2 implementation
//
// NOTE: Only called for len=4
FFT.prototype._singleTransform2 = function _singleTransform2(outOff, off,
step) {
const out = this._out
const data = this._data
const evenR = data[off]
const evenI = data[off + 1]
const oddR = data[off + step]
const oddI = data[off + step + 1]
const leftR = evenR + oddR
const leftI = evenI + oddI
const rightR = evenR - oddR
const rightI = evenI - oddI
out[outOff] = leftR
out[outOff + 1] = leftI
out[outOff + 2] = rightR
out[outOff + 3] = rightI
}
// radix-4
//
// NOTE: Only called for len=8
FFT.prototype._singleTransform4 = function _singleTransform4(outOff, off,
step) {
const out = this._out
const data = this._data
const inv = this._inv ? -1 : 1
const step2 = step * 2
const step3 = step * 3
// Original values
const Ar = data[off]
const Ai = data[off + 1]
const Br = data[off + step]
const Bi = data[off + step + 1]
const Cr = data[off + step2]
const Ci = data[off + step2 + 1]
const Dr = data[off + step3]
const Di = data[off + step3 + 1]
// Pre-Final values
const T0r = Ar + Cr
const T0i = Ai + Ci
const T1r = Ar - Cr
const T1i = Ai - Ci
const T2r = Br + Dr
const T2i = Bi + Di
const T3r = inv * (Br - Dr)
const T3i = inv * (Bi - Di)
// Final values
const FAr = T0r + T2r
const FAi = T0i + T2i
const FBr = T1r + T3i
const FBi = T1i - T3r
const FCr = T0r - T2r
const FCi = T0i - T2i
const FDr = T1r - T3i
const FDi = T1i + T3r
out[outOff] = FAr
out[outOff + 1] = FAi
out[outOff + 2] = FBr
out[outOff + 3] = FBi
out[outOff + 4] = FCr
out[outOff + 5] = FCi
out[outOff + 6] = FDr
out[outOff + 7] = FDi
}
// Real input radix-4 implementation
FFT.prototype._realTransform4 = function _realTransform4() {
let out = this._out
let size = this._csize
// Initial step (permute and transform)
let width = this._width
let step = 1 << width
let len = (size / step) << 1
let outOff
let t
let bitrev = this._bitrev
if (len === 4) {
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
const off = bitrev[t]
this._singleRealTransform2(outOff, off >>> 1, step >>> 1)
}
} else {
// len === 8
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
const off = bitrev[t]
this._singleRealTransform4(outOff, off >>> 1, step >>> 1)
}
}
// Loop through steps in decreasing order
let inv = this._inv ? -1 : 1
let table = this.table
for (step >>= 2; step >= 2; step >>= 2) {
len = (size / step) << 1
let halfLen = len >>> 1
let quarterLen = halfLen >>> 1
let hquarterLen = quarterLen >>> 1
// Loop through offsets in the data
for (outOff = 0; outOff < size; outOff += len) {
for (let i = 0, k = 0; i <= hquarterLen; i += 2, k += step) {
let A = outOff + i
let B = A + quarterLen
let C = B + quarterLen
let D = C + quarterLen
// Original values
let Ar = out[A]
let Ai = out[A + 1]
let Br = out[B]
let Bi = out[B + 1]
let Cr = out[C]
let Ci = out[C + 1]
let Dr = out[D]
let Di = out[D + 1]
// Middle values
let MAr = Ar
let MAi = Ai
let tableBr = table[k]
let tableBi = inv * table[k + 1]
let MBr = Br * tableBr - Bi * tableBi
let MBi = Br * tableBi + Bi * tableBr
let tableCr = table[2 * k]
let tableCi = inv * table[2 * k + 1]
let MCr = Cr * tableCr - Ci * tableCi
let MCi = Cr * tableCi + Ci * tableCr
let tableDr = table[3 * k]
let tableDi = inv * table[3 * k + 1]
let MDr = Dr * tableDr - Di * tableDi
let MDi = Dr * tableDi + Di * tableDr
// Pre-Final values
let T0r = MAr + MCr
let T0i = MAi + MCi
let T1r = MAr - MCr
let T1i = MAi - MCi
let T2r = MBr + MDr
let T2i = MBi + MDi
let T3r = inv * (MBr - MDr)
let T3i = inv * (MBi - MDi)
// Final values
let FAr = T0r + T2r
let FAi = T0i + T2i
let FBr = T1r + T3i
let FBi = T1i - T3r
out[A] = FAr
out[A + 1] = FAi
out[B] = FBr
out[B + 1] = FBi
// Output final middle point
if (i === 0) {
let FCr = T0r - T2r
let FCi = T0i - T2i
out[C] = FCr
out[C + 1] = FCi
continue
}
// Do not overwrite ourselves
if (i === hquarterLen) { continue }
// In the flipped case:
// MAi = -MAi
// MBr=-MBi, MBi=-MBr
// MCr=-MCr
// MDr=MDi, MDi=MDr
let ST0r = T1r
let ST0i = -T1i
let ST1r = T0r
let ST1i = -T0i
let ST2r = -inv * T3i
let ST2i = -inv * T3r
let ST3r = -inv * T2i
let ST3i = -inv * T2r
let SFAr = ST0r + ST2r
let SFAi = ST0i + ST2i
let SFBr = ST1r + ST3i
let SFBi = ST1i - ST3r
let SA = outOff + quarterLen - i
let SB = outOff + halfLen - i
out[SA] = SFAr
out[SA + 1] = SFAi
out[SB] = SFBr
out[SB + 1] = SFBi
}
}
}
}
// radix-2 implementation
//
// NOTE: Only called for len=4
FFT.prototype._singleRealTransform2 = function _singleRealTransform2(outOff,
off,
step) {
const out = this._out
const data = this._data
const evenR = data[off]
const oddR = data[off + step]
const leftR = evenR + oddR
const rightR = evenR - oddR
out[outOff] = leftR
out[outOff + 1] = 0
out[outOff + 2] = rightR
out[outOff + 3] = 0
}
// radix-4
//
// NOTE: Only called for len=8
FFT.prototype._singleRealTransform4 = function _singleRealTransform4(outOff,
off,
step) {
const out = this._out
const data = this._data
const inv = this._inv ? -1 : 1
const step2 = step * 2
const step3 = step * 3
// Original values
const Ar = data[off]
const Br = data[off + step]
const Cr = data[off + step2]
const Dr = data[off + step3]
// Pre-Final values
const T0r = Ar + Cr
const T1r = Ar - Cr
const T2r = Br + Dr
const T3r = inv * (Br - Dr)
// Final values
const FAr = T0r + T2r
const FBr = T1r
const FBi = -T3r
const FCr = T0r - T2r
const FDr = T1r
const FDi = T3r
out[outOff] = FAr
out[outOff + 1] = 0
out[outOff + 2] = FBr
out[outOff + 3] = FBi
out[outOff + 4] = FCr
out[outOff + 5] = 0
out[outOff + 6] = FDr
out[outOff + 7] = FDi
}
export default FFT

View File

@@ -0,0 +1,224 @@
/* eslint-disable no-var */
const WEBAUDIO_BLOCK_SIZE = 128
/** Overlap-Add Node */
class OLAProcessor extends globalThis.AudioWorkletProcessor {
constructor(options) {
super(options)
this.keepReturnTrue = true
this.processNow = false
this.nbInputs = options.numberOfInputs
this.nbOutputs = options.numberOfOutputs
this.blockSize = options.processorOptions.blockSize
// TODO for now, the only support hop size is the size of a web audio block
this.hopSize = WEBAUDIO_BLOCK_SIZE
this.nbOverlaps = this.blockSize / this.hopSize
this.lastSilencedHopCount = 0
this.nbOverlaps2x = this.nbOverlaps * 2
this.fakeEmptyInputs = [new Array(2).fill(new Float32Array(WEBAUDIO_BLOCK_SIZE))]
// pre-allocate input buffers (will be reallocated if needed)
this.inputBuffers = new Array(this.nbInputs)
this.inputBuffersHead = new Array(this.nbInputs)
this.inputBuffersToSend = new Array(this.nbInputs)
// assume 2 channels per input
for (var i = 0; i < this.nbInputs; i++) {
this.allocateInputChannels(i, 2)
}
// pre-allocate input buffers (will be reallocated if needed)
this.outputBuffers = new Array(this.nbOutputs)
this.outputBuffersToRetrieve = new Array(this.nbOutputs)
// assume 2 channels per output
for (i = 0; i < this.nbOutputs; i++) {
this.allocateOutputChannels(i, 2)
}
this.port.onmessage = (e) => this.keepReturnTrue = false
}
/** Handles dynamic reallocation of input/output channels buffer
(channel numbers may vary during lifecycle) **/
reallocateChannelsIfNeeded(inputs, outputs, force) {
for (var i = 0; i < this.nbInputs; i++) {
let nbChannels = inputs[i].length
if (force || (nbChannels != this.inputBuffers[i].length)) {
this.allocateInputChannels(i, nbChannels)
// console.log("reallocateChannelsIfNeeded");
}
}
for (i = 0; i < this.nbOutputs; i++) {
let nbChannels = outputs[i].length
if (force || (nbChannels != this.outputBuffers[i].length)) {
this.allocateOutputChannels(i, nbChannels)
// console.log("reallocateChannelsIfNeeded");
}
}
}
allocateInputChannels(inputIndex, nbChannels) {
// allocate input buffers
// console.log("allocateInputChannels");
this.inputBuffers[inputIndex] = new Array(nbChannels)
for (var i = 0; i < nbChannels; i++) {
this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE)
this.inputBuffers[inputIndex][i].fill(0)
}
// allocate input buffers to send and head pointers to copy from
// (cannot directly send a pointer/subarray because input may be modified)
this.inputBuffersHead[inputIndex] = new Array(nbChannels)
this.inputBuffersToSend[inputIndex] = new Array(nbChannels)
for (i = 0; i < nbChannels; i++) {
this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i].subarray(0, this.blockSize)
this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize)
}
}
allocateOutputChannels(outputIndex, nbChannels) {
// allocate output buffers
this.outputBuffers[outputIndex] = new Array(nbChannels)
for (var i = 0; i < nbChannels; i++) {
this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize)
this.outputBuffers[outputIndex][i].fill(0)
}
// allocate output buffers to retrieve
// (cannot send a pointer/subarray because new output has to be add to existing output)
this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels)
for (i = 0; i < nbChannels; i++) {
this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize)
this.outputBuffersToRetrieve[outputIndex][i].fill(0)
}
}
checkForNotSilence(value) {
return value !== 0
}
/** Read next web audio block to input buffers **/
readInputs(inputs) {
// when playback is paused, we may stop receiving new samples
/* if (inputs[0].length && inputs[0][0].length == 0) {
for (var i = 0; i < this.nbInputs; i++) {
for (var j = 0; j < this.inputBuffers[i].length; j++) {
this.inputBuffers[i][j].fill(0, this.blockSize);
}
}
return;
} */
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
let webAudioBlock = inputs[i][j]
this.inputBuffers[i][j]?.set(webAudioBlock, this.blockSize)
}
}
}
/** Shift left content of input buffers to receive new web audio block **/
shiftInputBuffers() {
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE)
}
}
}
/** Copy contents of input buffers to buffer actually sent to process **/
prepareInputBuffersToSend() {
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j])
}
}
}
/** Add contents of output buffers just processed to output buffers **/
handleOutputBuffersToRetrieve() {
for (let i = 0; i < this.nbOutputs; i++) {
for (let j = 0; j < this.outputBuffers[i].length; j++) {
for (let k = 0; k < this.blockSize; k++) {
this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps
}
}
}
}
/** Write next web audio block from output buffers **/
writeOutputs(outputs) {
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE)
outputs[i][j]?.set(webAudioBlock)
}
}
}
/** Shift left content of output buffers to receive new web audio block **/
shiftOutputBuffers() {
for (let i = 0; i < this.nbOutputs; i++) {
for (let j = 0; j < this.outputBuffers[i].length; j++) {
this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE)
this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0)
}
}
}
process(inputs, outputs, params) {
// console.log(inputs[0].length ? "active" : "inactive");
// this.reallocateChannelsIfNeeded(inputs, outputs);
// if (inputs[0][0].some(this.checkForNotSilence) || inputs[0][1].some(this.checkForNotSilence))
// console.log(inputs[0].length)
if (inputs[0].length < 2) {
// DUE TO CHROME BUG/INCONSISTENCY, WHEN INACTIVE SILENT NODE IS CONNECTED, inputs[0] IS EITHER EMPTY OR CONTAINS 1 CHANNEL OF SILENT AUDIO DATA, REQUIRES SPECIAL HANDLING
// if (inputs[0][0].some(this.checkForNotSilence)) console.warn("single channel not silence exception!");
if (this.lastSilencedHopCount < this.nbOverlaps2x) {
// ALLOW nbOverlaps2x BLOCKS OF SILENCE TO COME THROUGH TO ACCOMODATE LATENCY TAIL
this.lastSilencedHopCount++
inputs = this.fakeEmptyInputs
this.processNow = true
} else {
// console.warn("skipping processing");
if (this.lastSilencedHopCount === this.nbOverlaps2x) {
this.lastSilencedHopCount++
this.reallocateChannelsIfNeeded(this.fakeEmptyInputs, outputs, true)
// console.warn("reallocateChannels");
}
this.processNow = false // ENABLES SKIPPING UNNEEDED PROCESSING OF SILENT INPUT
}
} else {
if (this.lastSilencedHopCount) {
this.lastSilencedHopCount = 0
// this.reallocateChannelsIfNeeded(inputs, outputs, true);
// console.warn("reallocateChannels");
}
this.processNow = true
}
if (this.processNow) {
this.readInputs(inputs)
this.shiftInputBuffers()
this.prepareInputBuffersToSend()
this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params)
this.handleOutputBuffersToRetrieve()
this.writeOutputs(outputs)
this.shiftOutputBuffers()
}
return this.keepReturnTrue
}
/* processOLA(inputs, outputs, params) {
console.assert(false, "Not overriden");
} */
}
export default OLAProcessor

View File

@@ -0,0 +1,176 @@
// https://github.com/olvb/phaze/issues/26#issuecomment-1573938170
// https://github.com/olvb/phaze
import FFT from './fft'
import OLAProcessor from './ola-processor'
const DEFAULT_BUFFERED_BLOCK_SIZE = 4096
function genHannWindow(length) {
let win = new Float32Array(length)
for (let i = 0; i < length; i++) {
win[i] = 0.8 * (1 - Math.cos(2 * Math.PI * i / length))
}
return win
}
class PhaseVocoderProcessor extends OLAProcessor {
static get parameterDescriptors() {
return [{
name: 'pitchFactor',
defaultValue: 1.0,
automationRate: 'k-rate',
}, /* ,
{
name: 'pitchCents',
defaultValue: 0.0,
automationRate: 'k-rate'
} */]
}
constructor(options) {
(options.processorOptions ??= {}).blockSize ??= DEFAULT_BUFFERED_BLOCK_SIZE
super(options)
this.fftSize = this.blockSize
this.timeCursor = 0
this.hannWindow = genHannWindow(this.blockSize)
// prepare FFT and pre-allocate buffers
this.fft = new FFT(this.fftSize)
this.freqComplexBuffer = this.fft.createComplexArray()
this.freqComplexBufferShifted = this.fft.createComplexArray()
this.timeComplexBuffer = this.fft.createComplexArray()
this.magnitudes = new Float32Array(this.fftSize / 2 + 1)
this.peakIndexes = new Int32Array(this.magnitudes.length)
this.nbPeaks = 0
}
processOLA(inputs, outputs, parameters) {
// k-rate automation, param arrays only have single value
const pitchFactor = parameters.pitchFactor[0]/* || Math.pow(2, (parameters.pitchCents[0]/12)) */
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < inputs[i].length; j++) {
// big assumption here: output is symetric to input
let input = inputs[i][j]
let output = outputs[i][j]
this.applyHannWindow(input)
this.fft.realTransform(this.freqComplexBuffer, input)
this.computeMagnitudes()
this.findPeaks()
this.shiftPeaks(pitchFactor)
this.fft.completeSpectrum(this.freqComplexBufferShifted)
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted)
this.fft.fromComplexArray(this.timeComplexBuffer, output)
this.applyHannWindow(output)
}
}
this.timeCursor += this.hopSize
}
/** Apply Hann window in-place */
applyHannWindow(input) {
for (let i = 0; i < this.blockSize; i++) {
input[i] = input[i] * this.hannWindow[i]
}
}
/** Compute squared magnitudes for peak finding **/
computeMagnitudes() {
let i = 0; let j = 0
while (i < this.magnitudes.length) {
let real = this.freqComplexBuffer[j]
let imag = this.freqComplexBuffer[j + 1]
// no need to sqrt for peak finding
this.magnitudes[i] = real ** 2 + imag ** 2
i += 1
j += 2
}
}
/** Find peaks in spectrum magnitudes **/
findPeaks() {
this.nbPeaks = 0
let i = 2
let end = this.magnitudes.length - 2
while (i < end) {
let mag = this.magnitudes[i]
if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
i++
continue
}
if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) {
i++
continue
}
this.peakIndexes[this.nbPeaks] = i
this.nbPeaks++
i += 2
}
}
/** Shift peaks and regions of influence by pitchFactor into new specturm */
shiftPeaks(pitchFactor) {
// zero-fill new spectrum
this.freqComplexBufferShifted.fill(0)
for (let i = 0; i < this.nbPeaks; i++) {
let peakIndex = this.peakIndexes[i]
let peakIndexShifted = Math.round(peakIndex * pitchFactor)
if (peakIndexShifted > this.magnitudes.length) break
// find region of influence
let startIndex = 0
let endIndex = this.fftSize
if (i > 0) {
startIndex = peakIndex - Math.floor((peakIndex - this.peakIndexes[i - 1]) / 2)
}
if (i < this.nbPeaks - 1) {
endIndex = peakIndex + Math.ceil((this.peakIndexes[i + 1] - peakIndex) / 2)
}
// shift whole region of influence around peak to shifted peak
let startOffset = startIndex - peakIndex
let endOffset = endIndex - peakIndex
for (let j = startOffset; j < endOffset; j++) {
let binIndex = peakIndex + j
let binIndexShifted = peakIndexShifted + j
if (binIndexShifted >= this.magnitudes.length) break
// apply phase correction
let omegaDelta = 2 * Math.PI * (binIndexShifted - binIndex) / this.fftSize
let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor)
let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor)
let indexReal = binIndex * 2
let indexImag = indexReal + 1
let valueReal = this.freqComplexBuffer[indexReal]
let valueImag = this.freqComplexBuffer[indexImag]
let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag
let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal
let indexShiftedReal = binIndexShifted * 2
let indexShiftedImag = indexShiftedReal + 1
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag
}
}
}
}
globalThis.registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor)

View File

@@ -148,19 +148,22 @@ const saveMeta = (downloadInfo: LX.Download.ListItem) => {
: Promise.resolve(null),
]
void Promise.all(tasks).then(([imgUrl, lyrics]) => {
let lyric: null | string = null
if (lyrics?.lyric) {
lyric = fixKgLyric(lyrics.lyric)
if (appSetting['download.isEmbedLyricT'] && lyrics.tlyric) lyric += '\n' + lyrics.tlyric + '\n'
if (appSetting['download.isEmbedLyricR'] && lyrics.rlyric) lyric += '\n' + lyrics.rlyric + '\n'
const lrcData = {
lrc: '',
tlrc: null as string | null,
rlrc: null as string | null,
}
if (lyrics) {
lrcData.lrc = lyrics.lyric
if (appSetting['download.isEmbedLyricT'] && lyrics.tlyric) lrcData.tlrc = lyrics.tlyric
if (appSetting['download.isEmbedLyricR'] && lyrics.rlyric) lrcData.rlrc = lyrics.rlyric
}
void window.lx.worker.download.writeMeta(downloadInfo.metadata.filePath, {
title: downloadInfo.metadata.musicInfo.name,
artist: downloadInfo.metadata.musicInfo.singer,
album: downloadInfo.metadata.musicInfo.meta.albumName,
APIC: imgUrl,
lyrics: lyric,
})
}, lrcData)
})
}
@@ -177,11 +180,13 @@ const downloadLyric = (downloadInfo: LX.Download.ListItem) => {
}).then(lrcs => {
if (lrcs.lyric) {
lrcs.lyric = fixKgLyric(lrcs.lyric)
let lyric = lrcs.lyric
if (appSetting['download.isDownloadTLrc'] && lrcs.tlyric) lyric += '\n' + lrcs.tlyric + '\n'
if (appSetting['download.isDownloadRLrc'] && lrcs.rlyric) lyric += '\n' + lrcs.rlyric + '\n'
void window.lx.worker.download.saveLrc(downloadInfo.metadata.filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'),
lyric, appSetting['download.lrcFormat'])
const lrcData = {
lrc: lrcs.lyric,
tlrc: appSetting['download.isDownloadTLrc'] && lrcs.tlyric ? lrcs.tlyric : null,
rlrc: appSetting['download.isDownloadRLrc'] && lrcs.rlyric ? lrcs.rlyric : null,
}
void window.lx.worker.download.saveLrc(lrcData, downloadInfo.metadata.filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'),
appSetting['download.lrcFormat'])
}
})
}

View File

@@ -1,7 +1,7 @@
import { reactive, markRaw, shallowReactive } from '@common/utils/vueTools'
import music from '@renderer/utils/musicSdk'
export declare type Source = LX.OnlineSource
export type Source = LX.OnlineSource
export const sources: LX.OnlineSource[] = markRaw([])
@@ -10,12 +10,12 @@ for (const source of music.sources) {
sources.push(source.id as LX.OnlineSource)
}
export declare interface BoardItem {
export interface BoardItem {
id: string
name: string
bangid: string
}
export declare interface Board {
export interface Board {
list: BoardItem[]
source: LX.OnlineSource
}
@@ -23,7 +23,7 @@ type Boards = Partial<Record<LX.OnlineSource, Board>>
export const boards = shallowReactive<Boards>({})
export declare interface ListDetailInfo {
export interface ListDetailInfo {
list: LX.Music.MusicInfoOnline[]
total: number
page: number

View File

@@ -3,7 +3,7 @@ import { ref, shallowReactive } from '@common/utils/vueTools'
export const searchText = ref('')
export declare type onlineSource = LX.OnlineSource
export type onlineSource = LX.OnlineSource
export const historyList = shallowReactive<string[]>([])

View File

@@ -1,7 +1,7 @@
import { reactive, markRaw, ref, shallowReactive } from '@common/utils/vueTools'
import music from '@renderer/utils/musicSdk'
export declare interface SortInfo {
export interface SortInfo {
name: string
id: string
}
@@ -16,18 +16,18 @@ for (const source of music.sources) {
sortList[source.id as LX.OnlineSource] = songList.sortList as SortInfo[]
}
export declare interface TagInfoItem<T extends LX.OnlineSource = LX.OnlineSource> {
export interface TagInfoItem<T extends LX.OnlineSource = LX.OnlineSource> {
parent_id: string
parent_name: string
id: string
name: string
source: T
}
export declare interface TagInfoTypeItem<T extends LX.OnlineSource = LX.OnlineSource> {
export interface TagInfoTypeItem<T extends LX.OnlineSource = LX.OnlineSource> {
name: string
list: Array<TagInfoItem<T>>
}
export declare interface TagInfo<Source extends LX.OnlineSource = LX.OnlineSource> {
export interface TagInfo<Source extends LX.OnlineSource = LX.OnlineSource> {
tags: Array<TagInfoTypeItem<Source>>
hotTag: Array<TagInfoItem<Source>>
source: Source
@@ -38,7 +38,7 @@ type Tags = Partial<Record<LX.OnlineSource, TagInfo>>
export const tags = shallowReactive<Tags>({})
export declare interface ListInfoItem {
export interface ListInfoItem {
play_count: string
id: string
author: string
@@ -50,7 +50,7 @@ export declare interface ListInfoItem {
source: LX.OnlineSource
total?: string
}
export declare interface ListInfo {
export interface ListInfo {
list: ListInfoItem[]
total: number
page: number
@@ -62,7 +62,7 @@ export declare interface ListInfo {
sortId: string
}
export declare interface ListDetailInfo {
export interface ListDetailInfo {
list: LX.Music.MusicInfoOnline[]
source: LX.OnlineSource
desc: string | null

View File

@@ -1,5 +1,12 @@
import { reactive, toRaw } from '@common/utils/vueTools'
import { getUserSoundEffectConvolutionPresetList, getUserSoundEffectEQPresetList, saveUserSoundEffectConvolutionPresetList, saveUserSoundEffectEQPresetList } from '@renderer/utils/ipc'
import {
getUserSoundEffectConvolutionPresetList,
getUserSoundEffectEQPresetList,
// getUserSoundEffectPitchShifterPresetList,
saveUserSoundEffectConvolutionPresetList,
saveUserSoundEffectEQPresetList,
// saveUserSoundEffectPitchShifterPresetList,
} from '@renderer/utils/ipc'
let userEqPresetList: LX.SoundEffect.EQPreset[] | null = null
@@ -54,3 +61,30 @@ export const removeUserConvolutionPreset = async(id: string) => {
userConvolutionPresetList.splice(index, 1)
saveUserSoundEffectConvolutionPresetList(toRaw(userConvolutionPresetList))
}
// let userPitchShifterPresetList: LX.SoundEffect.PitchShifterPreset[] | null = null
// export const getUserPitchShifterPresetList = async() => {
// if (userEqPresetList == null) {
// userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList())
// }
// return userPitchShifterPresetList
// }
// export const saveUserPitchShifterPreset = async(preset: LX.SoundEffect.PitchShifterPreset) => {
// if (userPitchShifterPresetList == null) {
// userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList())
// }
// const target = userPitchShifterPresetList.find(p => p.id == preset.id)
// if (target) Object.assign(target, preset)
// else userPitchShifterPresetList.push(preset)
// saveUserSoundEffectPitchShifterPresetList(toRaw(userPitchShifterPresetList))
// }
// export const removeUserPitchShifterPreset = async(id: string) => {
// if (userPitchShifterPresetList == null) {
// userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList())
// }
// const index = userPitchShifterPresetList.findIndex(p => p.id == id)
// if (index < 0) return
// userPitchShifterPresetList.splice(index, 1)
// saveUserSoundEffectPitchShifterPresetList(toRaw(userPitchShifterPresetList))
// }

View File

@@ -1,9 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "nodenext",
"isolatedModules": true,
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
"@common/*": ["common/*"],
"@renderer/*": ["renderer/*"],
@@ -12,14 +10,14 @@
"@/*": ["./*"],
},
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
"./types"
"renderer/types"
],
},
"vueCompilerOptions": {
"plugins": [
"@volar/vue-language-plugin-pug"
]
}
// "vueCompilerOptions": {
// "plugins": [
// "@volar/vue-language-plugin-pug"
// ]
// }
// "include": [
// "./**/*.ts",
// // "./**/*.js",

View File

@@ -307,6 +307,13 @@ export const saveUserSoundEffectConvolutionPresetList = (list: LX.SoundEffect.Co
rendererSend<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, list)
}
// export const getUserSoundEffectPitchShifterPresetList = async() => {
// return await rendererInvoke<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_pitch_shifter_preset)
// }
// export const saveUserSoundEffectPitchShifterPresetList = (list: LX.SoundEffect.PitchShifterPreset[]) => {
// rendererSend<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_pitch_shifter_preset, list)
// }
export const allHotKeys = markRaw({
local: [

View File

@@ -2,11 +2,11 @@ import { isMac } from '@common/utils'
const downKeys = new Set<string>()
export declare type KeyActionType = LX.KeyDownEevent['type']
export declare type Keys = LX.KeyDownEevent['keys']
export declare type Key = LX.KeyDownEevent['key']
export declare type EventKey = LX.KeyDownEevent['eventKey']
export declare type Event = LX.KeyDownEevent['event']
export type KeyActionType = LX.KeyDownEevent['type']
export type Keys = LX.KeyDownEevent['keys']
export type Key = LX.KeyDownEevent['key']
export type EventKey = LX.KeyDownEevent['eventKey']
export type Event = LX.KeyDownEevent['event']
const handleEvent = (type: KeyActionType, event: LX.KeyEvent, keys: Keys, isEditing: boolean) => {
let eventKey = event.key

View File

@@ -12,10 +12,10 @@ const api_test = {
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
default: return Promise.reject(new Error(requestMsg.fail))
}
})

View File

@@ -174,7 +174,7 @@ export default {
filterList(rawData) {
return rawData.map(item => ({
play_count: this.formatPlayCount(item.listen_num),
id: item.list_id,
id: String(item.list_id),
author: item.username,
name: item.title,
// time: item.publish_time,

View File

@@ -12,10 +12,10 @@ const api_test = {
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
default: return Promise.reject(new Error(requestMsg.fail))
}
})

View File

@@ -10,7 +10,7 @@ export default {
let timestamp = Date.now()
const params = `appid=1005&clienttime=${timestamp}&clienttoken=0&clientver=11409&code=fc4be23b4e972707f36b8a828a93ba8a&dfid=0&extdata=${hash}&kugouid=0&mid=16249512204336365674023395779019&mixsongid=0&p=${page}&pagesize=${limit}&uuid=0&ver=10`
const _requestObj = httpFetch(`http://m.comment.service.kugou.com/v1/cmtlist?${params}&signature=${signatureParams(params)}`, {
const _requestObj = httpFetch(`http://m.comment.service.kugou.com/r/v1/rank/newest?${params}&signature=${signatureParams(params)}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.24',
},

View File

@@ -0,0 +1,85 @@
import { getMusicInfosByList } from './musicInfo'
import { createHttpFetch } from './util'
export default {
/**
* 获取歌手信息
* @param {*} id
*/
getInfo(id) {
if (id == 0) throw new Error('歌手不存在') // kg源某些歌曲在歌手没被kg收录时返回的歌手id为0
return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/info?singerid=${id}`).then(body => {
if (!body) throw new Error('get singer info faild.')
return {
source: 'kg',
id: body.singerid,
info: {
name: body.singername,
desc: body.intro,
avatar: body.imgurl.replace('{size}', 480),
gender: body.grade === 1 ? 'man' : 'woman',
},
count: {
music: body.songcount,
album: body.albumcount,
},
}
})
},
/**
* 获取歌手专辑列表
* @param {*} id
* @param {*} page
* @param {*} limit
*/
getAlbumList(id, page = 1, limit = 10) {
if (id == 0) throw new Error('歌手不存在')
return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/album?singerid=${id}&page=${page}&pagesize=${limit}`).then(body => {
if (!body.info) throw new Error('get singer album list faild.')
const list = this.filterAlbumList(body.info)
return {
source: 'kg',
list,
limit,
page,
total: body.total,
}
})
},
/**
* 获取歌手歌曲列表
* @param {*} id
* @param {*} page
* @param {*} limit
*/
async getSongList(id, page = 1, limit = 100) {
if (id == 0) throw new Error('歌手不存在')
const body = await createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/song?singerid=${id}&page=${page}&pagesize=${limit}`)
if (!body.info) throw new Error('get singer song list faild.')
const list = await getMusicInfosByList(body.info)
return {
source: 'kg',
list,
limit,
page,
total: body.total,
}
},
filterAlbumList(raw) {
return raw.map(item => {
return {
id: item.albumid,
count: item.songcount,
info: {
name: item.albumname,
author: item.singername,
img: item.replaceAll('{size}', '480'),
desc: item.intro,
},
}
})
},
}

View File

@@ -12,10 +12,10 @@ const api_temp = {
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
default: return Promise.reject(new Error(body.msg))
}
})

View File

@@ -23,10 +23,10 @@ const api_test = {
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
default: return Promise.reject(new Error(requestMsg.fail))
}
})

View File

@@ -467,7 +467,7 @@ export default {
list: body.abslist.map(item => {
return {
play_count: this.formatPlayCount(item.playcnt),
id: item.playlistid,
id: String(item.playlistid),
author: decodeName(item.nickname),
name: decodeName(item.name),
total: item.songnum,

View File

@@ -12,10 +12,10 @@ const api_test = {
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
default: return Promise.reject(new Error(requestMsg.fail))
}
})

View File

@@ -2,7 +2,7 @@ import { httpFetch } from '../../request'
import { sizeFormate, formatPlayTime } from '../../index'
import { toMD5, formatSingerName } from '../utils'
const sign = (time, str) => {
export const createSignature = (time, str) => {
const deviceId = '963B7AA0D21511ED807EE5846EC87D20'
const signatureMd5 = '6cdc72a439cef99a3418d2a78aa28c73'
const sign = toMD5(`${str}${signatureMd5}yyapp2d16148780a1dcc7408e06336b98cfd50${deviceId}${time}`)
@@ -111,7 +111,7 @@ export default {
musicSearch(str, page, limit) {
const time = Date.now().toString()
const signData = sign(time, str)
const signData = createSignature(time, str)
const searchRequest = httpFetch(`https://jadeite.migu.cn/music_search/v3/search/searchAll?isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, {
headers: {
uiVersion: 'A_music_3.6.1',

File diff suppressed because one or more lines are too long

View File

@@ -12,10 +12,10 @@ const api_messoer = {
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
default: return Promise.reject(new Error(requestMsg.fail))
}
})

View File

@@ -241,7 +241,7 @@ export default {
text: item.Content ? this.replaceEmoji(item.Content).replace(/\\n/g, '\n') : '',
time: item.PubTime ? this.formatTime(item.PubTime) : null,
timeStr: item.PubTime ? dateFormat2(this.formatTime(item.PubTime)) : null,
userName: item.Nick ? item.Nick.substring(1) : '',
userName: item.Nick ?? '',
images: item.Pic ? [item.Pic] : [],
avatar: item.Avatar,
location: item.Location ? item.Location : '',
@@ -254,7 +254,7 @@ export default {
text: this.replaceEmoji(c.Content).replace(/\\n/g, '\n'),
time: c.PubTime ? this.formatTime(c.PubTime) : null,
timeStr: c.PubTime ? dateFormat2(this.formatTime(c.PubTime)) : null,
userName: c.Nick ? c.Nick.substring(1) : '',
userName: c.Nick ?? '',
avatar: c.Avatar,
images: c.Pic ? [c.Pic] : [],
userId: c.EncryptUin,

View File

@@ -100,8 +100,10 @@ const parseTools = {
return str.replace(/^[\S\s]*?LyricContent="/, '').replace(/"\/>[\S\s]*?$/, '')
},
getIntv(interval) {
let [m, s, ms] = interval.split(/:|\./)
if (!interval.includes('.')) interval += '.0'
let arr = interval.split(/:|\./)
while (arr.length < 3) arr.unshift('0')
const [m, s, ms] = arr
return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms)
},
fixRlrcTimeTag(rlrc, lrc) {
@@ -123,7 +125,7 @@ const parseTools = {
const lrcLineResult = this.rxps.lineTime2.exec(lrcLine)
if (!lrcLineResult) continue
const t2 = this.getIntv(lrcLineResult[1])
if (Math.abs(t1 - t2) < 10) {
if (Math.abs(t1 - t2) < 100) {
newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0]))
break
}
@@ -140,21 +142,25 @@ const parseTools = {
const tlrcLines = tlrc.split('\n')
let lrcLines = lrc.split('\n')
// let temp = []
const timeTagRxp = /^\[[\d:.]+\]/
let newLrc = []
tlrcLines.forEach((line) => {
const result = timeTagRxp.exec(line)
const result = this.rxps.lineTime2.exec(line)
if (!result) return
const words = line.replace(timeTagRxp, '')
const words = line.replace(this.rxps.lineTime2, '')
if (!words.trim()) return
const tag = result[0].replace(/\d]/, '').replace(this.rxps.timeLabelFixRxp, '')
let time = result[1]
if (time.includes('.')) {
time += ''.padStart(3 - time.split('.')[1].length, '0')
}
const t1 = this.getIntv(time)
while (lrcLines.length) {
const lrcLine = lrcLines.shift()
const lrcLineResult = timeTagRxp.exec(lrcLine)
const lrcLineResult = this.rxps.lineTime2.exec(lrcLine)
if (!lrcLineResult) continue
if (lrcLineResult[0].includes(tag)) {
newLrc.push(line.replace(timeTagRxp, lrcLineResult[0]))
const t2 = this.getIntv(lrcLineResult[1])
if (Math.abs(t1 - t2) < 100) {
newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0]))
break
}
// temp.push(line)

View File

@@ -0,0 +1,249 @@
import { httpFetch } from '../../request'
import { formatPlayTime, sizeFormate } from '../../index'
import { formatSingerName } from '../utils'
export const filterMusicInfoItem = item => {
const types = []
const _types = {}
if (item.file.size_128mp3 != 0) {
let size = sizeFormate(item.file.size_128mp3)
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
}
if (item.file.size_320mp3 !== 0) {
let size = sizeFormate(item.file.size_320mp3)
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
}
if (item.file.size_flac !== 0) {
let size = sizeFormate(item.file.size_flac)
types.push({ type: 'flac', size })
_types.flac = {
size,
}
}
if (item.file.size_hires !== 0) {
let size = sizeFormate(item.file.size_hires)
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
}
const albumId = item.album.id ?? ''
const albumMid = item.album.mid ?? ''
const albumName = item.album.name ?? ''
return {
source: 'tx',
singer: formatSingerName(item.singer, 'name'),
name: item.name,
albumName,
albumId,
albumMid,
interval: formatPlayTime(item.interval),
songId: item.id,
songmid: item.mid,
strMediaMid: item.file.media_mid,
img: (albumId === '' || albumId === '空')
? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : ''
: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumMid}.jpg`,
types,
_types,
typeUrl: {},
}
}
/**
* 创建一个适用于TX的Http请求
* @param {*} url
* @param {*} options
* @param {*} retryNum
*/
const createMusicuFetch = async(data, options, retryNum = 0) => {
if (retryNum > 2) throw new Error('try max num')
let result
try {
result = await httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {
method: 'POST',
body: {
comm: {
cv: 4747474,
ct: 24,
format: 'json',
inCharset: 'utf-8',
outCharset: 'utf-8',
uin: 0,
},
...data,
},
headers: {
'User-Angent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
},
}).promise
} catch (err) {
console.log(err)
return createMusicuFetch(data, options, ++retryNum)
}
if (result.statusCode !== 200 || result.body.code != 0) return createMusicuFetch(data, options, ++retryNum)
return result.body
}
export default {
/**
* 获取歌手信息
* @param {*} id
*/
getInfo(id) {
return createMusicuFetch({
req_1: {
module: 'music.musichallSinger.SingerInfoInter',
method: 'GetSingerDetail',
param: {
singer_mid: [id],
ex_singer: 1,
wiki_singer: 1,
group_singer: 0,
pic: 1,
photos: 0,
},
},
req_2: {
module: 'music.musichallAlbum.AlbumListServer',
method: 'GetAlbumList',
param: {
singerMid: id,
order: 0,
begin: 0,
num: 1,
songNumTag: 0,
singerID: 0,
},
},
req_3: {
module: 'musichall.song_list_server',
method: 'GetSingerSongList',
param: {
singerMid: id,
order: 1,
begin: 0,
num: 1,
},
},
}).then(body => {
if (body.req_1.code != 0 || body.req_2 != 0 || body.req_3 != 0) throw new Error('get singer info faild.')
const info = body.req_1.data.singer_list[0]
const music = body.req_3.data
const album = body.req_3.data
return {
source: 'tx',
id: info.basic_info.singer_mid,
info: {
name: info.basic_info.name,
desc: info.ex_info.desc,
avatar: info.pic.pic,
gender: info.ex_info.genre === 1 ? 'man' : 'woman',
},
count: {
music: music.totalNum,
album: album.total,
},
}
})
},
/**
* 获取歌手专辑列表
* @param {*} id
* @param {*} page
* @param {*} limit
*/
getAlbumList(id, page = 1, limit = 10) {
if (page === 1) page = 0
return createMusicuFetch({
req: {
module: 'music.musichallAlbum.AlbumListServer',
method: 'GetAlbumList',
param: {
singerMid: id,
order: 0,
begin: page * limit,
num: limit,
songNumTag: 0,
singerID: 0,
},
},
}).then(body => {
if (body.req.code != 0) throw new Error('get singer album faild.')
const list = this.filterAlbumList(body.req.data.albumList)
return {
source: 'tx',
list,
limit,
page,
total: body.req.data.total,
}
})
},
/**
* 获取歌手歌曲列表
* @param {*} id
* @param {*} page
* @param {*} limit
*/
async getSongList(id, page = 1, limit = 100) {
if (page === 1) page = 0
return createMusicuFetch({
req: {
module: 'musichall.song_list_server',
method: 'GetSingerSongList',
param: {
singerMid: id,
order: 1,
begin: page * limit,
num: limit,
},
},
}).then(body => {
if (body.req.code != 0) throw new Error('get singer song list faild.')
const list = this.filterSongList(body.req.data.songList)
return {
source: 'tx',
list,
limit,
page,
total: body.req.data.totalNum,
}
})
},
filterAlbumList(raw) {
return raw.map(item => {
return {
id: item.albumID,
mid: item.albumMid,
count: item.totalNum,
info: {
name: item.albumName,
author: item.singerName,
img: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.albumMid}.jpg`,
desc: null,
},
}
})
},
filterSongList(raw) {
raw.map(item => {
return filterMusicInfoItem(item.songInfo)
})
},
}

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