pull/1050/head
lyswhut 2022-10-29 11:36:35 +08:00
parent 66b965d9f7
commit e45cee6596
715 changed files with 57096 additions and 31771 deletions

View File

@ -1,5 +1,6 @@
{
"presets": [
"@babel/preset-typescript",
[
"@babel/preset-env",
{

View File

@ -1,6 +1,6 @@
{
"root": true,
"extends": [
// "plugin:vue/vue3-recommended",
"standard"
],
"plugins": [
@ -8,7 +8,7 @@
],
"parser": "@babel/eslint-parser",
"parserOptions": {
"requireConfigFile": false
// "requireConfigFile": false
},
"rules": {
"no-new": "off",
@ -24,11 +24,94 @@
"standard/no-callback-literal": "off",
"prefer-const": "off",
"no-labels": "off",
"node/no-callback-literal": "off",
"vue/multi-word-component-names": "off"
"node/no-callback-literal": "off"
},
"settings": {
"html/html-extensions": [".html", ".vue"]
},
"ignorePatterns": ["vendors", "*.min.js", "dist", "node_modules"]
"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",
"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",
"multiline-ternary": "off",
"@typescript-eslint/comma-dangle": "off",
},
"parser": "@typescript-eslint/parser",
"extends": [
"standard-with-typescript"
],
"parserOptions": {
"project": "./src/**/tsconfig.json"
}
}
]
}

View File

@ -11,15 +11,15 @@ jobs:
runs-on: windows-latest
steps:
- name: Check out git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache file
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
node_modules
@ -32,7 +32,6 @@ jobs:
- name: Install Dependencies
run: |
npm install npm@8.5 -g
npm install
- name: Build src code
@ -41,7 +40,7 @@ jobs:
- name: Build Package Setup x64
run: npm run pack:win:setup:x64
- name: Upload Artifact Setup x64
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-x64-Setup
path: build/* x64 Setup.exe
@ -49,7 +48,7 @@ jobs:
- name: Build Package Setup x86
run: npm run pack:win:setup:x86
- name: Upload Artifact Setup x86
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-x86-Setup
path: build/* x86 Setup.exe
@ -57,7 +56,7 @@ jobs:
- name: Build Package Setup arm64
run: npm run pack:win:setup:arm64
- name: Upload Artifact Setup arm64
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-arm64-Setup
path: build/* arm64 Setup.exe
@ -65,7 +64,7 @@ jobs:
- name: Build Package Setup x86_64
run: npm run pack:win:setup:x86_64
- name: Upload Artifact Setup x86_64
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-x86_64-Setup
path: build/*x86_64 Setup.exe
@ -73,7 +72,7 @@ jobs:
- name: Build Package 7z x64
run: npm run pack:win:7z:x64
- name: Upload Artifact 7z x64
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-win_x64-green
path: build/*win_x64 green.7z
@ -81,7 +80,7 @@ jobs:
- name: Build Package 7z x86
run: npm run pack:win:7z:x86
- name: Upload Artifact 7z x86
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-win_x86-green
path: build/*win_x86 green.7z
@ -89,7 +88,7 @@ jobs:
- name: Build Package 7z arm64
run: npm run pack:win:7z:arm64
- name: Upload Artifact 7z arm64
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-win_arm64-green
path: build/*win_arm64 green.7z
@ -104,15 +103,15 @@ jobs:
runs-on: macos-latest
steps:
- name: Check out git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache file
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
node_modules
@ -125,7 +124,6 @@ jobs:
- name: Install Dependencies
run: |
npm install npm@8.5 -g
npm install
- name: Build src code
@ -140,14 +138,14 @@ jobs:
ELECTRON_BUILDERCACHE: $HOME/.cache/electron-builder
- name: Upload Artifact dmg
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-mac-dmg
path: |
build/*.dmg
!build/*-arm64.dmg
- name: Upload Artifact dmg
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-mac-dmg-arm64
path: build/*-arm64.dmg
@ -165,15 +163,15 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y rpm libarchive-tools
- name: Check out git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache file
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
node_modules
@ -186,7 +184,6 @@ jobs:
- name: Install Dependencies
run: |
npm install npm@8.5 -g
npm install
- name: Build src code
@ -195,23 +192,15 @@ jobs:
- name: Build Package deb x64
run: npm run pack:linux:deb:x64
- name: Upload Artifact deb x64
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-deb-x64
path: build/* x64.deb
- name: Build Package deb x86
run: npm run pack:linux:deb:x86
- name: Upload Artifact deb x86
uses: actions/upload-artifact@v2
with:
name: lx-music-desktop-deb-x86
path: build/* x86.deb
- name: Build Package deb arm64
run: npm run pack:linux:deb:arm64
- name: Upload Artifact deb arm64
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-deb-arm64
path: build/* arm64.deb
@ -219,7 +208,7 @@ jobs:
- name: Build Package deb armv7l
run: npm run pack:linux:deb:armv7l
- name: Upload Artifact deb armv7l
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-deb-armv7l
path: build/* armv7l.deb
@ -227,7 +216,7 @@ jobs:
- name: Build Package x64 appImage
run: npm run pack:linux:appImage
- name: Upload Artifact x64 appImage
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-x64-appImage
path: build/* x64.AppImage
@ -235,7 +224,7 @@ jobs:
- name: Build Package x64 rpm
run: npm run pack:linux:rpm
- name: Upload Artifact x64 rpm
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-x64-rpm
path: build/* x64.rpm
@ -243,7 +232,7 @@ jobs:
- name: Build Package x64 pacman
run: npm run pack:linux:pacman
- name: Upload Artifact x64 pacman
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: lx-music-desktop-x64-pacman
path: build/* x64.pacman

View File

@ -11,15 +11,15 @@ jobs:
runs-on: windows-latest
steps:
- name: Check out git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache file
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
node_modules
@ -32,7 +32,6 @@ jobs:
- name: Install Dependencies
run: |
npm install npm@8.5 -g
npm install
- name: Build src code
@ -61,15 +60,15 @@ jobs:
runs-on: macos-latest
steps:
- name: Check out git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache file
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
node_modules
@ -82,7 +81,6 @@ jobs:
- name: Install Dependencies
run: |
npm install npm@8.5 -g
npm install
- name: Build src code
@ -111,15 +109,15 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y rpm libarchive-tools
- name: Check out git repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache file
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
node_modules
@ -132,7 +130,6 @@ jobs:
- name: Install Dependencies
run: |
npm install npm@8.5 -g
npm install
- name: Build src code
@ -141,7 +138,6 @@ jobs:
- name: Release package
run: |
npm run publish:linux:deb:x64:always
npm run publish:linux:deb:x86
npm run publish:linux:deb:arm64
npm run publish:linux:deb:armv7l
npm run publish:linux:appImage

View File

@ -11,4 +11,9 @@ module.exports = {
// 'electron-builder',
// 'electron-updater',
// ],
// target: 'minor',
// filter: [
// 'electron',
// ],
}

29
.vscode/i18n-ally-custom-framework.yml vendored Normal file
View File

@ -0,0 +1,29 @@
# .vscode/i18n-ally-custom-framework.yml
# An array of strings which contain Language Ids defined by VS Code
# You can check avaliable language ids here: https://code.visualstudio.com/docs/languages/overview#_language-id
languageIds:
- javascript
- typescript
- vue
# An array of RegExes to find the key usage. **The key should be captured in the first match group**.
# You should unescape RegEx strings in order to fit in the YAML file
# To help with this, you can use https://www.freeformatter.com/json-escape.html
usageMatchRegex:
# The following example shows how to detect `t("your.i18n.keys")`
# the `{key}` will be placed by a proper keypath matching regex,
# you can ignore it and use your own matching rules as well
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
# An array of strings containing refactor templates.
# The "$1" will be replaced by the keypath specified.
# Optional: uncomment the following two lines to use
# refactorTemplates:
# - i18n.get("$1")
# If set to true, only enables this custom framework (will disable all built-in frameworks)
monopoly: true

37
.vscode/javascript.code-snippets vendored Normal file
View File

@ -0,0 +1,37 @@
{
// Place your lx-music-desktop-new 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"use i18n": {
"prefix": "ui18n",
"body": [
"import { useI18n } from '@renderer/plugins/i18n'",
"${1:const t = useI18n()}"
]
},
"list action": {
"prefix": "listacion",
"body": [
"import { $1 } from '@renderer/store/list/action'",
]
},
"import vue tools": {
"prefix": "imvt",
"body": [
"import { $1 } from '@common/utils/vueTools'",
]
}
}

View File

@ -10,5 +10,7 @@
"google-cn",
"google"
],
"i18n-ally.sortKeys": true
"i18n-ally.sortKeys": true,
"javascript.preferences.importModuleSpecifier": "non-relative",
"typescript.tsdk": "node_modules/typescript/lib"
}

31
.vscode/typescript.code-snippets vendored Normal file
View File

@ -0,0 +1,31 @@
{
// Place your lx-music-desktop-new 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"use i18n": {
"prefix": "ui18n",
"body": [
"import { useI18n } from '@renderer/plugins/i18n'",
"${1:const t = useI18n()}"
]
},
"import vue tools": {
"prefix": "imvt",
"body": [
"import { $1 } from '@common/utils/vueTools'",
]
}
}

8
FAQ.md
View File

@ -369,7 +369,7 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看上面的
- URL统一以`lxmusic://`开头
- 若无特别说明,源的可用值为:`kw/kg/tx/wy/mg`
- 若无特别说明,音质的可用值为:`128k/320k/flac/flac32bit`
- 若无特别说明,音质的可用值为:`128k/320k/flac/flac24bit`
目前支持两种传参方式:
@ -464,7 +464,7 @@ send(EVENT_NAMES.inited, {
name: '酷我音乐',
type: 'music', // 目前固定为 music
actions: ['musicUrl'], // 目前固定为 ['musicUrl']
qualitys: ['128k', '320k', 'flac'], // 当前脚本的该源所支持获取的Url音质有效的值有['128k', '320k', 'flac']
qualitys: ['128k', '320k', 'flac', 'flac24bit'], // 当前脚本的该源所支持获取的Url音质有效的值有['128k', '320k', 'flac', 'flac24bit']
},
},
})
@ -506,8 +506,8 @@ send(EVENT_NAMES.inited, {
| 事件名 | 描述
| --- | ---
| `inited` | 脚本初始化完成后发送给应用的事件名,发送该事件时需要传入以下信息:`{status, sources, openDevTools}`<br>`status`:初始化结果(`true`成功,`false`失败)<br>`openDevTools`是否打开DevTools此选项可用于开发脚本时的调试<br>`sources`:支持的源信息对象,<br>`sources[kw/kg/tx/wy/mg].name`:源的名字(目前非必须)<br>`sources[kw/kg/tx/wy/mg].type`:源类型,目前固定值需为`music`<br>`sources[kw/kg/tx/wy/mg].actions`支持的actions由于目前只支持`musicUrl`,所以固定传`['musicUrl']`即可<br>`sources[kw/kg/tx/wy/mg].qualitys`:该源支持的音质列表,有效的值为`['128k', '320k', 'flac']`,该字段用于控制应用可用的音质类型
| `request` | 应用API请求事件名回调入参`handler({ source, action, info})`,回调必须返回`Promise`对象<br>`source`:音乐源,可能的值取决于初始化时传入的`sources`对象的源key值<br>`info`:请求附加信息,内容根据`action`变化<br>`action`:请求操作类型,目前只有`musicUrl`即获取音乐URL链接需要在 Promise 返回歌曲 url`info`的结构:`{type, musicInfo}``info.type`:音乐质量,可能的值有`128k` / `320k` / `flac`(取决于初始化时对应源传入的`qualitys`值中的一个),`info.musicInfo`音乐信息对象里面有音乐ID、名字等信息
| `inited` | 脚本初始化完成后发送给应用的事件名,发送该事件时需要传入以下信息:`{status, sources, openDevTools}`<br>`status`:初始化结果(`true`成功,`false`失败)<br>`openDevTools`是否打开DevTools此选项可用于开发脚本时的调试<br>`sources`:支持的源信息对象,<br>`sources[kw/kg/tx/wy/mg].name`:源的名字(目前非必须)<br>`sources[kw/kg/tx/wy/mg].type`:源类型,目前固定值需为`music`<br>`sources[kw/kg/tx/wy/mg].actions`支持的actions由于目前只支持`musicUrl`,所以固定传`['musicUrl']`即可<br>`sources[kw/kg/tx/wy/mg].qualitys`:该源支持的音质列表,有效的值为`['128k', '320k', 'flac', 'flac24bit']`,该字段用于控制应用可用的音质类型
| `request` | 应用API请求事件名回调入参`handler({ source, action, info})`,回调必须返回`Promise`对象<br>`source`:音乐源,可能的值取决于初始化时传入的`sources`对象的源key值<br>`info`:请求附加信息,内容根据`action`变化<br>`action`:请求操作类型,目前只有`musicUrl`即获取音乐URL链接需要在 Promise 返回歌曲 url`info`的结构:`{type, musicInfo}``info.type`:音乐质量,可能的值有`128k` / `320k` / `flac` / `flac24bit`(取决于初始化时对应源传入的`qualitys`值中的一个),`info.musicInfo`音乐信息对象里面有音乐ID、名字等信息
| `updateAlert` | 显示源更新弹窗,发送该事件时的参数:`{log, updateUrl}`<br>`log`:更新日志,必传,字符串类型,内容可以使用`\n`换行最大长度1024超过此长度后将被截取超出的部分<br>`updateUrl`更新地址用于引导用户去该地址更新源选传需为http协议的url地址最大长度1024<br>此事件每次运行脚本只能调用一次源版本v1.2.0新增)<br>例子:`lx.send(lx.EVENT_NAMES.updateAlert, { log: 'hello world', updateUrl: 'https://xxx.com' })`

View File

@ -36,7 +36,7 @@
所用技术栈:
- Electron 17
- Electron 15+
- Vue 3
已支持的平台:
@ -48,8 +48,7 @@
软件变化请查看:[更新日志](https://github.com/lyswhut/lx-music-desktop/blob/master/CHANGELOG.md)<br>
软件下载请转到:[发布页面](https://github.com/lyswhut/lx-music-desktop/releases)<br>
或者到网盘下载网盘内有MAC、windows版`https://www.lanzoui.com/b0bf2cfa/` 密码:`glqw`(若链接无法打开请百度:蓝奏云链接打不开)<br>
使用常见问题请转至:[常见问题](https://lyswhut.github.io/lx-music-doc/desktop/faq)<br>
移动版项目地址:<https://github.com/lyswhut/lx-music-mobile>
使用常见问题请转至:[常见问题](https://lyswhut.github.io/lx-music-doc/desktop/faq)
#### Scheme URL支持

View File

@ -0,0 +1,25 @@
const fs = require('fs').promises
// https://github.com/electron-userland/electron-builder/issues/4630
// https://github.com/electron-userland/electron-builder/issues/4630#issuecomment-782020139
module.exports = async(context) => {
const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== 'darwin') return
const {
productFilename,
info: {
_metadata: { macLanguagesInfoPlistStrings },
},
} = context.packager.appInfo
const resPath = `${appOutDir}/${productFilename}.app/Contents/Resources`
// 创建APP语言包文件
return await Promise.all(
Object.entries(macLanguagesInfoPlistStrings).map(([lang, config]) => {
let infos = Object.entries(config).map(([k, v]) => `"${k}" = "${v}";`).join('\n')
return fs.writeFile(`${resPath}/${lang}.lproj/InfoPlist.strings`, infos)
}),
)
}

View File

@ -5,9 +5,17 @@ module.exports = {
target: 'electron-main',
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
library: {
type: 'commonjs2',
},
path: path.join(__dirname, '../../dist'),
},
externals: [
'font-list',
'better-sqlite3',
'bufferutil',
'utf-8-validate',
],
resolve: {
alias: {
'@main': path.join(__dirname, '../../src/main'),
@ -15,7 +23,7 @@ module.exports = {
'@lyric': path.join(__dirname, '../../src/renderer-lyric'),
'@common': path.join(__dirname, '../../src/common'),
},
extensions: ['*', '.js', '.json', '.node'],
extensions: ['.tsx', '.ts', '.js', '.mjs', '.json', '.node'],
},
module: {
rules: [
@ -23,6 +31,11 @@ module.exports = {
test: /\.node$/,
use: 'node-loader',
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
plugins: [

View File

@ -8,15 +8,17 @@ const baseConfig = require('./webpack.config.base')
module.exports = merge(baseConfig, {
mode: 'development',
entry: {
main: path.join(__dirname, '../../src/main/index.dev.js'),
main: path.join(__dirname, '../../src/main/index-dev.ts'),
// 'dbService.worker': path.join(__dirname, '../../src/main/worker/dbService/index.ts'),
},
devtool: 'eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"development"',
},
__static: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`,
__userApi: `"${path.join(__dirname, '../../src/main/modules/userApi').replace(/\\/g, '\\\\')}"`,
webpackStaticPath: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`,
webpackUserApiPath: `"${path.join(__dirname, '../../src/main/modules/userApi').replace(/\\/g, '\\\\')}"`,
}),
],
performance: {

View File

@ -5,18 +5,17 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
const baseConfig = require('./webpack.config.base')
const { dependencies } = require('../../package.json')
// const { dependencies } = require('../../package.json')
const buildConfig = require('../webpack-build-config')
module.exports = merge(baseConfig, {
mode: 'production',
entry: {
main: path.join(__dirname, '../../src/main/index.js'),
main: path.join(__dirname, '../../src/main/index.ts'),
// 'dbService.worker': path.join(__dirname, '../../src/main/worker/dbService/index.ts'),
},
externals: [
...Object.keys(dependencies || {}),
// 'font-list',
],
node: {
__dirname: false,
__filename: false,
@ -25,12 +24,12 @@ module.exports = merge(baseConfig, {
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../../src/main/modules/userApi/renderer'),
to: path.join(__dirname, '../../dist/userApi/renderer'),
from: path.join(__dirname, '../../src/main/modules/userApi/renderer/user-api.html'),
to: path.join(__dirname, '../../dist/userApi/renderer/user-api.html'),
},
{
from: path.join(__dirname, '../../src/main/modules/userApi/rendererEvent/name.js'),
to: path.join(__dirname, '../../dist/userApi/rendererEvent/name.js'),
from: path.join(__dirname, '../../src/common/theme/images/*').replace(/\\/g, '/'),
to: path.join(__dirname, '../../dist/theme_images/[name][ext]'),
},
],
}),
@ -45,6 +44,6 @@ module.exports = merge(baseConfig, {
maxAssetSize: 1024 * 1024 * 20,
},
optimization: {
minimize: false,
minimize: buildConfig.minimize,
},
})

View File

@ -5,13 +5,16 @@ const del = require('del')
const webpack = require('webpack')
const Spinnies = require('spinnies')
const mainConfig = require('./main/webpack.config.prod')
const rendererConfig = require('./renderer/webpack.config.prod')
const rendererLyricConfig = require('./renderer-lyric/webpack.config.prod')
const mainConfig = './main/webpack.config.prod'
const rendererConfig = './renderer/webpack.config.prod'
const rendererLyricConfig = './renderer-lyric/webpack.config.prod'
const rendererScriptConfig = './renderer-scripts/webpack.config.prod'
const errorLog = chalk.bgRed.white(' ERROR ') + ' '
const okayLog = chalk.bgGreen.white(' OKAY ') + ' '
const { Worker, isMainThread, parentPort } = require('worker_threads')
function build() {
del.sync(['dist/**', 'build/**'])
@ -20,6 +23,7 @@ function build() {
spinners.add('main', { text: 'main building' })
spinners.add('renderer', { text: 'renderer building' })
spinners.add('renderer-lyric', { text: 'renderer-lyric building' })
spinners.add('renderer-scripts', { text: 'renderer-scripts building' })
let results = ''
// m.on('success', () => {
@ -63,11 +67,35 @@ function build() {
console.error(`\n${err}\n`)
process.exit(1)
}),
pack(rendererScriptConfig).then(result => {
results += result + '\n\n'
spinners.succeed('renderer-scripts', { text: 'renderer-scripts build success!' })
}).catch(err => {
spinners.fail('renderer-scripts', { text: 'renderer-scripts build fail :(' })
console.log(`\n ${errorLog}failed to build renderer-scripts process`)
console.error(`\n${err}\n`)
process.exit(1)
}),
]).then(handleSuccess)
}
function pack(config) {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename)
const subChannel = new MessageChannel()
worker.postMessage({ port: subChannel.port1, config }, [subChannel.port1])
subChannel.port2.on('message', ({ status, message }) => {
switch (status) {
case 'success': return resolve(message)
case 'error': return reject(message)
}
})
})
}
function runPack(config) {
return new Promise((resolve, reject) => {
config = require(config)
config.mode = 'production'
webpack(config, (err, stats) => {
if (err) reject(err.stack || err)
@ -95,5 +123,22 @@ function pack(config) {
})
}
build()
if (isMainThread) build()
else {
parentPort.once('message', ({ port, config }) => {
// assert(port instanceof MessagePort)
runPack(config).then((result) => {
port.postMessage({
status: 'success',
message: result,
})
}).catch((err) => {
port.postMessage({
status: 'error',
message: err,
})
}).finally(() => {
port.close()
})
})
}

View File

@ -12,11 +12,13 @@ const isDev = process.env.NODE_ENV === 'development'
module.exports = {
target: 'electron-renderer',
entry: {
'renderer-lyric': path.join(__dirname, '../../src/renderer-lyric/main.js'),
'renderer-lyric': path.join(__dirname, '../../src/renderer-lyric/main.ts'),
},
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
library: {
type: 'commonjs2',
},
path: path.join(__dirname, '../../dist'),
publicPath: 'auto',
},
@ -29,10 +31,25 @@ module.exports = {
'@static': path.join(__dirname, '../../src/static'),
'@common': path.join(__dirname, '../../src/common'),
},
extensions: ['*', '.js', '.json', '.vue', '.node'],
extensions: ['.tsx', '.ts', '.js', '.json', '.vue', '.node'],
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
},
},
},
{
test: /\.node$/,
use: 'node-loader',
@ -43,9 +60,8 @@ module.exports = {
options: vueLoaderConfig,
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
test: /\.pug$/,
loader: 'pug-plain-loader',
},
{
test: /\.css$/,
@ -60,20 +76,6 @@ module.exports = {
},
}),
},
{
test: /\.pug$/,
oneOf: [
// Use pug-plain-loader handle .vue file
{
resourceQuery: /vue/,
use: ['pug-plain-loader'],
},
// Use pug-loader handle .pug file
{
use: ['pug-loader'],
},
],
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
exclude: path.join(__dirname, '../../src/renderer/assets/svgs'),
@ -130,7 +132,7 @@ module.exports = {
plugins: [
new HTMLPlugin({
filename: 'lyric.html',
template: path.join(__dirname, '../../src/renderer-lyric/index.pug'),
template: path.join(__dirname, '../../src/renderer-lyric/index.html'),
isProd: process.env.NODE_ENV == 'production',
browser: process.browser,
__dirname,

View File

@ -16,7 +16,7 @@ module.exports = merge(baseConfig, {
},
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
__static: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`,
staticPath: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`,
}),
],
performance: {

View File

@ -5,18 +5,19 @@ const TerserPlugin = require('terser-webpack-plugin')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.config.base')
const buildConfig = require('../webpack-build-config')
const { dependencies } = require('../../package.json')
// const { dependencies } = require('../../package.json')
// let whiteListedModules = ['vue']
let whiteListedModules = ['vue', 'vue-router', 'vuex', 'vue-i18n']
// let whiteListedModules = ['vue', 'vue-router', 'vuex', 'vue-i18n']
module.exports = merge(baseConfig, {
mode: 'production',
devtool: false,
externals: [
...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
// ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
],
plugins: [
new webpack.DefinePlugin({
@ -28,7 +29,7 @@ module.exports = merge(baseConfig, {
}),
],
optimization: {
minimize: false,
minimize: buildConfig.minimize,
minimizer: [
new TerserPlugin(),
new CssMinimizerPlugin(),

View File

@ -0,0 +1,57 @@
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
module.exports = {
target: 'electron-renderer',
entry: {
'user-api-preload': path.join(__dirname, '../../src/main/modules/userApi/renderer/preload.js'),
},
output: {
filename: '[name].js',
library: {
type: 'commonjs2',
},
path: path.join(__dirname, '../../dist'),
publicPath: 'auto',
},
resolve: {
alias: {
'@': path.join(__dirname, '../../src'),
'@main': path.join(__dirname, '../../src/main'),
'@renderer': path.join(__dirname, '../../src/renderer'),
'@lyric': path.join(__dirname, '../../src/renderer-lyric'),
'@static': path.join(__dirname, '../../src/static'),
'@common': path.join(__dirname, '../../src/common'),
},
extensions: ['.tsx', '.ts', '.js', '.json', '.vue', '.node'],
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
},
},
},
{
test: /\.node$/,
use: 'node-loader',
},
],
},
plugins: [
new ESLintPlugin({
extensions: ['js'],
formatter: require('eslint-formatter-friendly'),
}),
],
}

View File

@ -0,0 +1,24 @@
const path = require('path')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.config.base')
module.exports = merge(baseConfig, {
mode: 'development',
devtool: 'eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"development"',
ELECTRON_DISABLE_SECURITY_WARNINGS: 'true',
},
staticPath: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`,
}),
],
performance: {
hints: false,
},
})

View File

@ -0,0 +1,45 @@
// const path = require('path')
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.config.base')
const buildConfig = require('../webpack-build-config')
// const { dependencies } = require('../../package.json')
// let whiteListedModules = ['vue']
// let whiteListedModules = ['vue', 'vue-router', 'vuex', 'vue-i18n']
module.exports = merge(baseConfig, {
mode: 'production',
devtool: false,
externals: [
// ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
],
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"',
},
}),
],
optimization: {
minimize: buildConfig.minimize,
minimizer: [
new TerserPlugin(),
],
},
performance: {
maxEntrypointSize: 1024 * 1024 * 10,
maxAssetSize: 1024 * 1024 * 20,
hints: 'warning',
},
node: {
__dirname: false,
__filename: false,
},
})

View File

@ -12,11 +12,13 @@ const isDev = process.env.NODE_ENV === 'development'
module.exports = {
target: 'electron-renderer',
entry: {
renderer: path.join(__dirname, '../../src/renderer/main.js'),
renderer: path.join(__dirname, '../../src/renderer/main.ts'),
},
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
library: {
type: 'commonjs2',
},
path: path.join(__dirname, '../../dist'),
publicPath: 'auto',
},
@ -29,10 +31,25 @@ module.exports = {
'@static': path.join(__dirname, '../../src/static'),
'@common': path.join(__dirname, '../../src/common'),
},
extensions: ['*', '.js', '.json', '.vue', '.node'],
extensions: ['.tsx', '.ts', '.js', '.json', '.vue', '.node'],
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
},
},
},
{
test: /\.node$/,
use: 'node-loader',
@ -43,9 +60,8 @@ module.exports = {
options: vueLoaderConfig,
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
test: /\.pug$/,
loader: 'pug-plain-loader',
},
{
test: /\.css$/,
@ -60,20 +76,6 @@ module.exports = {
},
}),
},
{
test: /\.pug$/,
oneOf: [
// Use pug-plain-loader handle .vue file
{
resourceQuery: /vue/,
use: ['pug-plain-loader'],
},
// Use pug-loader handle .pug file
{
use: ['pug-loader'],
},
],
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
exclude: path.join(__dirname, '../../src/renderer/assets/svgs'),

View File

@ -14,10 +14,10 @@ module.exports = merge(baseConfig, {
NODE_ENV: '"development"',
ELECTRON_DISABLE_SECURITY_WARNINGS: 'true',
},
ENVIRONMENT: 'process.env',
// ENVIRONMENT: 'process.env',
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
__static: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`,
staticPath: `"${path.join(__dirname, '../../src/static').replace(/\\/g, '\\\\')}"`,
}),
],
performance: {

View File

@ -6,17 +6,18 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.config.base')
const buildConfig = require('../webpack-build-config')
const { dependencies } = require('../../package.json')
// const { dependencies } = require('../../package.json')
let whiteListedModules = ['vue', 'vue-router', 'vuex', 'vue-i18n']
// let whiteListedModules = ['vue', 'vue-router', 'vuex', 'vue-i18n']
module.exports = merge(baseConfig, {
mode: 'production',
devtool: false,
externals: [
...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
// ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
],
plugins: [
new CopyWebpackPlugin({
@ -31,17 +32,21 @@ module.exports = merge(baseConfig, {
'process.env': {
NODE_ENV: '"production"',
},
ENVIRONMENT: 'process.env',
// ENVIRONMENT: 'process.env',
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
}),
],
optimization: {
minimize: false,
minimize: buildConfig.minimize,
minimizer: [
new TerserPlugin(),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'initial',
minChunks: 2,
},
},
performance: {
maxEntrypointSize: 1024 * 1024 * 10,

View File

@ -13,6 +13,7 @@ const webpackHotMiddleware = require('webpack-hot-middleware')
const mainConfig = require('./main/webpack.config.dev')
const rendererConfig = require('./renderer/webpack.config.dev')
const rendererLyricConfig = require('./renderer-lyric/webpack.config.dev')
const rendererScriptConfig = require('./renderer-scripts/webpack.config.dev')
let electronProcess = null
let manualRestart = false
@ -47,9 +48,10 @@ function startRenderer() {
port: 9080,
hot: true,
historyApiFallback: true,
// static: {
// directory: path.join(__dirname, '../'),
// },
static: {
directory: path.join(__dirname, '../src/common/theme/images'),
publicPath: '/theme_images',
},
client: {
logging: 'warn',
overlay: true,
@ -111,6 +113,22 @@ function startRendererLyric() {
})
}
function startRendererScripts() {
return new Promise((resolve, reject) => {
// mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)
// mainConfig.mode = 'development'
const compiler = webpack(rendererScriptConfig)
compiler.watch({}, (err, stats) => {
if (err) {
console.log(err)
return
}
resolve()
})
})
}
function startMain() {
return new Promise((resolve, reject) => {
// mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)
@ -175,11 +193,19 @@ function startElectron() {
})
}
const logs = [
'Manifest version 2 is deprecated, and support will be removed in 2023',
'"Extension server error: Operation failed: Permission denied", source: devtools://devtools/bundled',
// https://github.com/electron/electron/issues/32133
'"Electron sandbox_bundle.js script failed to run"',
'"TypeError: object null is not iterable (cannot read property Symbol(Symbol.iterator))",',
]
function electronLog(data, color) {
let log = data.toString()
if (/[0-9A-z]+/.test(log)) {
// 抑制 user api 窗口使用 data url 加载页面时 vue扩展 的报错日志刷屏的问题
if (color == 'red' && typeof log === 'string' && log.includes('"Extension server error: Operation failed: Permission denied", source: devtools://devtools/bundled/extensions/extensions.js')) return
// 抑制某些无关的报错日志
if (color == 'red' && typeof log === 'string' && logs.some(l => log.includes(l))) return
console.log(chalk[color](log))
}
@ -191,6 +217,7 @@ function init() {
spinners.add('main', { text: 'main compiling' })
spinners.add('renderer', { text: 'renderer compiling' })
spinners.add('renderer-lyric', { text: 'renderer-lyric compiling' })
spinners.add('renderer-scripts', { text: 'renderer-scripts compiling' })
function handleSuccess(name) {
spinners.succeed(name, { text: name + ' compile success!' })
}
@ -207,6 +234,10 @@ function init() {
console.error(err.message)
return handleFail('renderer-lyric')
}),
startRendererScripts().then(() => handleSuccess('renderer-scripts')).catch((err) => {
console.error(err.message)
return handleFail('renderer-scripts')
}),
startMain().then(() => handleSuccess('main')).catch(() => handleFail('main')),
]).then(startElectron).catch(err => {
console.error(err)

View File

@ -0,0 +1,3 @@
module.exports = {
minimize: false,
}

View File

@ -2,16 +2,12 @@
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"@main/*": ["src/main/*"],
"@renderer/*": ["src/renderer/*"],
"@lyric/*": ["src/renderer-lyric/*"],
"@static/*": ["src/static/*"],
"@common/*": ["src/common/*"],
},
},
"vueCompilerOptions": {
"experimentalDisableTemplateSupport": true
}
},
"exclude": ["node_modules", "build", "dist"]
}

15999
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "lx-music-desktop",
"version": "1.22.3",
"version": "2.0.0-beta.1",
"description": "一个免费的音乐查找助手",
"main": "./dist/main.js",
"productName": "lx-music-desktop",
@ -21,9 +21,8 @@
"pack:win:7z:arm64": "cross-env TARGET=green ARCH=win_arm64 electron-builder -w=7z --arm64 -p never",
"pack:linux": "node build-config/pack.js && npm run pack:linux:deb && npm run pack:linux:appImage && npm run pack:linux:rpm && npm run pack:linux:pacman",
"pack:linux:appImage": "cross-env ARCH=x64 electron-builder -l=AppImage -p never",
"pack:linux:deb": "npm run pack:linux:deb:x64 && npm run pack:linux:deb:x86 && npm run pack:linux:deb:arm64 && npm run pack:linux:deb:armv7l",
"pack:linux:deb": "npm run pack:linux:deb:x64 && npm run pack:linux:deb:arm64 && npm run pack:linux:deb:armv7l",
"pack:linux:deb:x64": "cross-env ARCH=x64 electron-builder -l=deb --x64 -p never",
"pack:linux:deb:x86": "cross-env ARCH=x86 electron-builder -l=deb --ia32 -p never",
"pack:linux:deb:arm64": "cross-env ARCH=arm64 electron-builder -l=deb --arm64 -p never",
"pack:linux:deb:armv7l": "cross-env ARCH=armv7l electron-builder -l=deb --armv7l -p never",
"pack:linux:rpm": "cross-env ARCH=x64 electron-builder -l=rpm --x64 -p never",
@ -50,27 +49,29 @@
"publish:mac:dmg:arm64": "electron-builder -m=dmg --arm64 -p onTagOrDraft",
"publish:linux:deb:x64:always": "cross-env ARCH=x64 electron-builder -l=deb --x64 -p always",
"publish:linux:deb:x64": "cross-env ARCH=x64 electron-builder -l=deb --x64 -p onTagOrDraft",
"publish:linux:deb:x86": "cross-env ARCH=x86 electron-builder -l=deb --ia32 -p onTagOrDraft",
"publish:linux:deb:arm64": "cross-env ARCH=arm64 electron-builder -l=deb --arm64 -p onTagOrDraft",
"publish:linux:deb:armv7l": "cross-env ARCH=armv7l electron-builder -l=deb --armv7l -p onTagOrDraft",
"publish:linux:appImage": "cross-env ARCH=x64 electron-builder -l=AppImage -p onTagOrDraft",
"publish:linux:rpm": "cross-env ARCH=x64 electron-builder -l=rpm --x64 -p onTagOrDraft",
"publish:linux:pacman": "cross-env ARCH=x64 electron-builder -l=pacman --x64 -p onTagOrDraft",
"dev": "node build-config/runner-dev.js",
"dev": "cross-env NODE_OPTIONS=--max-http-header-size=200000 node build-config/runner-dev.js",
"clean:electron": "rimraf dist",
"clean": "rimraf dist && rimraf build",
"build:theme": "node src/common/theme/createThemes.js",
"build:src": "node build-config/pack.js",
"build:main": "cross-env NODE_ENV=production webpack --config build-config/main/webpack.config.prod.js --progress",
"build:renderer": "cross-env NODE_ENV=production webpack --config build-config/renderer/webpack.config.prod.js --progress",
"build:renderer-lyric": "cross-env NODE_ENV=production webpack --config build-config/renderer-lyric/webpack.config.prod.js --progress",
"build": "npm run clean:electron && npm run build:main && npm run build:renderer && npm run build:renderer-lyric",
"build:renderer-scripts": "cross-env NODE_ENV=production webpack --config build-config/renderer-scripts/webpack.config.prod.js --progress",
"build": "npm run clean:electron && npm run build:main && npm run build:renderer && npm run build:renderer-lyric && npm run build:renderer-scripts",
"lint": "eslint --ext .js,.vue -f node_modules/eslint-formatter-friendly src",
"postinstall": "electron-builder install-app-deps",
"lint:fix": "eslint --ext .js,.vue -f node_modules/eslint-formatter-friendly --fix src",
"dp": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://localhost:1081 npm run pack",
"up": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://localhost:1081 npm i"
},
"browserslist": [
"Electron 15.5.7"
"Electron 19.1.0"
],
"engines": {
"node": ">= 16",
@ -78,6 +79,7 @@
},
"build": {
"appId": "cn.toside.music.desktop",
"afterPack": "./build-config/build-after-pack.js",
"protocols": {
"name": "lx-music-protocol",
"schemes": [
@ -89,6 +91,14 @@
"output": "./build"
},
"files": [
"!node_modules/**/*",
"node_modules/font-list",
"node_modules/better-sqlite3/lib",
"node_modules/better-sqlite3/package.json",
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
"node_modules/node-gyp-build",
"node_modules/bufferutil",
"node_modules/utf-8-validate",
"dist/**/*"
],
"asar": {
@ -104,11 +114,7 @@
},
"mac": {
"icon": "./resources/icons/icon.icns",
"category": "public.app-category.music",
"extendInfo": {
"CFBundleName": "lx-music-desktop",
"CFBundleDisplayName": "LX Music"
}
"category": "public.app-category.music"
},
"linux": {
"maintainer": "lyswhut <lyswhut@qq.com>",
@ -116,7 +122,12 @@
"icon": "./resources/icons",
"category": "Utility;AudioVideo;Audio;Player;Music;",
"desktop": {
"Name[zh_CN]": "洛雪音乐助手"
"Name": "LX Music",
"Name[zh_CN]": "LX Music",
"Name[zh_TW]": "LX Music",
"Encoding": "UTF-8",
"MimeType": "x-scheme-handler/lxmusic",
"StartupNotify": "false"
}
},
"nsis": {
@ -159,6 +170,20 @@
}
]
},
"macLanguagesInfoPlistStrings": {
"en": {
"CFBundleDisplayName": "LX Music",
"CFBundleName": "LX Music"
},
"zh_CN": {
"CFBundleDisplayName": "LX Music",
"CFBundleName": "LX Music"
},
"zh_TW": {
"CFBundleDisplayName": "LX Music",
"CFBundleName": "LX Music"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/lyswhut/lx-music-desktop.git"
@ -166,7 +191,7 @@
"keywords": [
"music-player",
"electron-app",
"vuejs2"
"vuejs3"
],
"author": {
"name": "lyswhut",
@ -178,96 +203,104 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.18.10",
"@babel/eslint-parser": "^7.18.9",
"@babel/core": "^7.19.6",
"@babel/eslint-parser": "^7.19.1",
"@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.18.10",
"@babel/preset-env": "^7.18.10",
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.19.4",
"@babel/preset-typescript": "^7.18.6",
"@types/better-sqlite3": "^7.6.2",
"@types/needle": "^2.5.3",
"@types/tunnel": "^0.0.3",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"@typescript-eslint/parser": "^5.41.0",
"@volar/vue-language-plugin-pug": "^1.0.9",
"babel-loader": "^8.2.5",
"babel-preset-minify": "^0.5.2",
"browserslist": "^4.21.3",
"browserslist": "^4.21.4",
"chalk": "^4.1.2",
"changelog-parser": "^2.8.1",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.24.1",
"core-js": "^3.26.0",
"cross-env": "^7.0.3",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^4.0.0",
"css-minimizer-webpack-plugin": "^4.2.2",
"del": "^6.1.1",
"electron": "^15.5.7",
"electron-builder": "^23.3.3",
"electron": "^19.1.3",
"electron-builder": "^24.0.0-alpha.2",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"electron-to-chromium": "^1.4.224",
"electron-updater": "^5.2.1",
"eslint": "^8.22.0",
"electron-to-chromium": "^1.4.284",
"electron-updater": "^6.0.0-alpha.1",
"eslint": "^8.26.0",
"eslint-config-standard": "^17.0.0",
"eslint-formatter-friendly": "git+https://github.com/lyswhut/eslint-friendly-formatter.git#2170d1320e2fad13615a9dcf229669f0bb473a53",
"eslint-config-standard-with-typescript": "^23.0.0",
"eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-n": "^15.3.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.6.0",
"eslint-webpack-plugin": "^3.2.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"less": "^4.1.3",
"less-loader": "^11.0.0",
"less-loader": "^11.1.0",
"mini-css-extract-plugin": "^2.6.1",
"node-loader": "^2.0.0",
"postcss": "^8.4.16",
"postcss": "^8.4.18",
"postcss-loader": "^7.0.1",
"postcss-pxtorem": "^6.0.0",
"pug": "^3.0.2",
"pug-loader": "^2.4.0",
"pug-plain-loader": "^1.1.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"spinnies": "git+https://github.com/lyswhut/spinnies.git#233305c58694aa3b053e3ab9af9049993f918b9d",
"spinnies": "github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d",
"svg-sprite-loader": "^6.0.11",
"svg-transform-loader": "^2.0.13",
"svgo-loader": "^3.0.1",
"terser": "^5.14.2",
"terser-webpack-plugin": "^5.3.5",
"url-loader": "^4.1.1",
"terser": "^5.15.1",
"terser-webpack-plugin": "^5.3.6",
"ts-loader": "^9.4.1",
"typescript": "^4.8.4",
"vue-eslint-parser": "^9.1.0",
"vue-loader": "^17.0.0",
"vue-template-compiler": "^2.7.8",
"vue-template-compiler": "^2.7.13",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.10.0",
"webpack-hot-middleware": "git+https://github.com/lyswhut/webpack-hot-middleware.git#329c4375134b89d39da23a56a94db651247c74a1",
"webpack-dev-server": "^4.11.1",
"webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"bufferutil": "^4.0.6",
"@simonwep/pickr": "^1.8.2",
"better-sqlite3": "^7.6.2",
"bufferutil": "^4.0.7",
"comlink": "^4.3.1",
"crypto-js": "^4.1.1",
"electron-log": "^4.4.8",
"electron-store": "^8.1.0",
"font-list": "git+https://github.com/lyswhut/node-font-list.git#4edbb1933b49a9bac1eedd63a31da16b487fe57d",
"font-list": "github:lyswhut/node-font-list#4edbb1933b49a9bac1eedd63a31da16b487fe57d",
"http-terminator": "^3.2.0",
"iconv-lite": "^0.6.3",
"image-size": "^1.0.2",
"jschardet": "^3.0.0",
"koa": "^2.13.4",
"long": "^5.2.0",
"mitt": "^3.0.0",
"needle": "^3.1.0",
"music-metadata": "^8.1.0",
"needle": "github:lyswhut/needle#95cd7135818824a90d1ed4bb5aa4f8610304ae34",
"node-id3": "^0.2.3",
"request": "^2.88.2",
"socket.io": "^4.5.1",
"socket.io": "^4.5.3",
"sortablejs": "^1.15.0",
"tunnel": "^0.0.6",
"utf-8-validate": "^5.0.9",
"vue": "^3.2.37",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.3",
"vuex": "^4.0.2"
"utf-8-validate": "^5.0.10",
"vue": "^3.2.41",
"vue-router": "^4.1.6"
},
"overrides": {
"async": "^2.3.0",
"got": "^11.8.5",
"svg-sprite-loader": {
"postcss": "8.2.13"
}
"got": "^11",
"svg-baker": {
"postcss": "latest"
},
"minimatch": "latest"
}
}

View File

@ -1,3 +1,56 @@
### 不兼容性变更说明
- 数据迁移,升级此版本时,会使用旧版本的我的列表、下载设置、快捷键设置、自定义源等数据会自动迁移到新的数据格式版本,旧的数据仍然会保留,但下载列表的数据不做迁移
- 备份文件v2.0.0及以后版本导出的列表、配置不支持导入v2.0.0之前版本但v2.0.0之前版本导出的列表、配置支持导入v2.0.0以及以后版本
- 同步功能由于v2.0.0支持本地歌曲,所以未兼容现有移动端版本的同步,需要以后更新移动端
### 新增
- 新增自定义主题功能
- 新增歌单搜索功能
- 新增将本地歌曲添加到我的列表的支持,此功能可以在列表的右击菜单中使用(本地歌曲的歌词优先尝试读取相同路径下的同名歌词文件,若文件不存在则尝试读取歌曲文件内的歌词,若还是找不到歌词则尝试利用换源功能获取在线歌词,歌曲封面则是尝试读取歌曲文件内的封面,若不存在则利用换源功能获取在线封面)
- 启动软件时自动回到上次的界面,例如上次退出软件时在我的列表,下次启动软件时会自动进入我的列表
- 新增启动软件时自动播放音乐设置,默认关闭,可去设置-播放设置开启
- 新增“蛋雅深藍”皮肤
- 新增歌词时是否歌词翻译、罗马音设置,默认关闭,可以去设置-下载设置开启(#344
- 新增界面字体大小设置
- 桌面歌词新增竖排歌词显示功能(#971
- 桌面歌词新增歌词对齐方式、是否不允许歌词换行、歌词颜色设置
- 桌面歌词新增歌曲频谱显示得益于主窗口与桌面歌词进程通信的改进可以将此功能以CPU使用率“相对较低”的方式带到桌面歌词中
- 添加kg源罗马音歌词的支持
### 优化(界面/交互/功能)
- 调整软件界面及配色,使其更加清爽
- 处于单曲循环、顺序播放、禁用切歌模式时,手动切歌将会按列表循环模式的逻辑处理切歌(#864
- 歌单右键菜单的“重复歌曲”扫描功能现在会将歌曲名字内的括号内容移除再对比,这可以有效找出歌曲的变体,例如:`突然的自我`、`突然的自我(Live)`、`突然的自我(女生版)`、`突然的自我(DJ版)`等都会被找出来(#987
- 播放栏的心形按钮点击时,将会收藏/取消收藏当前播放的歌曲,右击将打开歌曲添加弹窗(原来的行为),然后可以将此歌曲添加到其他列表
- 允许更小的桌面歌词窗口高度,可以取消“不允许拖动到主屏幕之外”设置后,再启用“不允许歌词换行”、“置顶歌词”与“自动刷新置顶”设置,把它拖动到任务栏上,当做任务栏歌词使用
### 优化(程序)
- 优化程序启动性能,优化与程序交互的流畅度
- 重构整个程序重新梳理了程序逻辑使其更容易扩展及维护将大部分代码从JavaScript迁移到TypeScript
- 重写配置管理、列表管理功能列表、歌词数据从json文件迁移到sqlite3存储这应该能解决因为意外的字符编码导致的数据文件损坏问题
### 变更
- 列表右侧的操作按钮栏默认不再显示,歌曲的操作可以使用右键菜单代替,若想恢复它们的显示,可以去设置-列表设置-启用操作按钮栏开启
- 窗口大小设置时不再自动调整字体大小,想要调整字体大小可以使用新增的字体大小设置调整
- v2.0.0及以后版本导出的列表、配置不支持导入v2.0.0之前版本但v2.0.0之前版本导出的列表、配置支持导入v2.0.0以及以后版本
### 修复
- 修复因音源的域名到期导致的音源失效的问题
- 修复Linux、macOS下若程序路径存在百分号时会导致软件无法启动的问题#963
- 支持单行多时间标签歌词解析,修复某些歌词会出现时间标签的问题
### 移除
- 移除“信口雌黄”皮肤(由于该皮肤的配色有点刺眼),若你正在使用该皮肤,可以使用自定义主题功能恢复它
- 移除Linux deb x86包构建Electron/Chromium已不再支持 32-bit Linuxelectron/electron#34787
- 移除桌面歌词主题设置,改用桌面歌词字体颜色设置功能代替
### 其他
- 更新Electron到v19.1.3

File diff suppressed because one or more lines are too long

View File

@ -1,130 +0,0 @@
module.exports = {
windowSizeList: [
{
id: 0,
name: 'smaller',
width: 828,
height: 530,
fontSize: '14px',
},
{
id: 1,
name: 'small',
width: 920,
height: 590,
fontSize: '16px',
},
{
id: 2,
name: 'medium',
width: 1018,
height: 650,
fontSize: '16px',
},
{
id: 3,
name: 'big',
width: 1114,
height: 708,
fontSize: '17px',
},
{
id: 4,
name: 'larger',
width: 1202,
height: 766,
fontSize: '17px',
},
{
id: 5,
name: 'oversized',
width: 1382,
height: 886,
fontSize: '18px',
},
{
id: 6,
name: 'huge',
width: 1686,
height: 1062,
fontSize: '19px',
},
],
navigationUrlWhiteList: [
],
themes: [
{
id: 0,
name: '绿意盎然',
className: 'green',
},
{
id: 1,
name: '蓝田生玉',
className: 'blue',
},
{
id: 2,
name: '信口雌黄',
className: 'yellow',
},
{
id: 3,
name: '橙黄橘绿',
className: 'orange',
},
{
id: 4,
name: '热情似火',
className: 'red',
},
{
id: 10,
name: '粉装玉琢',
className: 'pink',
},
{
id: 5,
name: '重斤球紫',
className: 'purple',
},
{
id: 6,
name: '灰常美丽',
className: 'grey',
},
{
id: 11,
name: '青出于黑',
className: 'ming',
},
{
id: 12,
name: '青出于黑',
className: 'blue2',
},
{
id: 13,
name: '黑灯瞎火',
className: 'black',
},
{
id: 7,
name: '月里嫦娥',
className: 'mid_autumn',
},
{
id: 8,
name: '木叶之村',
className: 'naruto',
},
{
id: 9,
name: '新年快乐',
className: 'happy_new_year',
},
],
themeLights: [0, 1, 2, 3, 4, 10, 5, 6, 11, 12, 7, 8, 9],
themeDarks: [13, 7],
}

86
src/common/config.ts Normal file
View File

@ -0,0 +1,86 @@
export interface WindowSize {
id: number
name: string
width: number
height: number
}
export const windowSizeList: WindowSize[] = [
{
id: 0,
name: 'smaller',
width: 828,
height: 540,
},
{
id: 1,
name: 'small',
width: 920,
height: 600,
},
{
id: 2,
name: 'medium',
width: 1020,
height: 660,
},
{
id: 3,
name: 'big',
width: 1114,
height: 718,
},
{
id: 4,
name: 'larger',
width: 1202,
height: 776,
},
{
id: 5,
name: 'oversized',
width: 1385,
height: 896,
},
{
id: 6,
name: 'huge',
width: 1700,
height: 1070,
},
]
export const navigationUrlWhiteList: RegExp[] = []
// 基础黑白色
export const commonColorNames = [
'--color-000', '--color-050', '--color-100', '--color-200', '--color-300', '--color-400',
'--color-500', '--color-600', '--color-700', '--color-800', '--color-900',
] as const
export const commonLightColorValues = [
'rgb(255, 255, 255)',
'rgb(217,217,217)',
'rgb(184,184,184)',
'rgb(156,156,156)',
'rgb(133,133,133)',
'rgb(113,113,113)',
'rgb(96,96,96)',
'rgb(82,82,82)',
'rgb(70,70,70)',
'rgb(60,60,60)',
'rgb(51,51,51)',
] as const
export const commonDarkColorValues = [
'rgb(11, 11, 11)',
'rgb(60,60,60)',
'rgb(99,99,99)',
'rgb(130,130,130)',
'rgb(155,155,155)',
'rgb(175,175,175)',
'rgb(191,191,191)',
'rgb(204,204,204)',
'rgb(214,214,214)',
'rgb(222,222,222)',
'rgb(229,229,229)',
] as const

74
src/common/constants.ts Normal file
View File

@ -0,0 +1,74 @@
export const URL_SCHEME_RXP = /^lxmusic:\/\//
export const STORE_NAMES = {
APP_SETTINGS: 'config_v2',
DATA: 'data',
SYNC: 'sync',
HOTKEY: 'hot_key',
USER_API: 'user_api',
LRC_RAW: 'lyrics',
LRC_EDITED: 'lyrics_edited',
THEME: 'theme',
} as const
export const APP_EVENT_NAMES = {
winMainName: 'win_main',
winLyricName: 'win_lyric',
trayName: 'tray',
} as const
export const LIST_IDS = {
DEFAULT: 'default',
LOVE: 'love',
TEMP: 'temp',
DOWNLOAD: 'download',
} as const
export const DATA_KEYS = {
viewPrevState: 'viewPrevState',
playInfo: 'playInfo',
searchHistoryList: 'searchHistoryList',
listScrollPosition: 'listScrollPosition',
listPrevSelectId: 'listPrevSelectId',
listUpdateInfo: 'listUpdateInfo',
ignoreVersion: 'ignoreVersion',
leaderboardSetting: 'leaderboardSetting',
songListSetting: 'songListSetting',
searchSetting: 'searchSetting',
} as const
export const DEFAULT_SETTING = {
leaderboard: {
source: 'kw',
boardId: 'kw__16',
},
songList: {
source: 'kg',
sortId: '5',
tagId: '',
},
search: {
temp_source: 'kw',
source: 'all',
type: 'music',
},
viewPrevState: {
url: '/search',
query: {},
},
}
export const DOWNLOAD_STATUS = {
RUN: 'run',
WAITING: 'waiting',
PAUSE: 'pause',
ERROR: 'error',
COMPLETED: 'completed',
} as const
export const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k'] as const

View File

@ -1,89 +0,0 @@
const { player: hotKeyPlayer, common: hotKeyCommon, desktop_lyric: hotKeyDesktopLyric } = require('./hotKey')
module.exports = {
local: {
enable: true,
keys: {
'mod+f5': {
type: hotKeyPlayer.toggle_play.type,
name: hotKeyPlayer.toggle_play.name,
action: hotKeyPlayer.toggle_play.action,
},
'mod+arrowleft': {
type: hotKeyPlayer.prev.type,
name: hotKeyPlayer.prev.name,
action: hotKeyPlayer.prev.action,
},
'mod+arrowright': {
type: hotKeyPlayer.next.type,
name: hotKeyPlayer.next.name,
action: hotKeyPlayer.next.action,
},
f1: {
type: hotKeyCommon.focusSearchInput.type,
name: hotKeyCommon.focusSearchInput.name,
action: hotKeyCommon.focusSearchInput.action,
},
},
},
global: {
enable: false,
keys: {
MediaPlayPause: {
type: hotKeyPlayer.toggle_play.type,
name: null,
action: hotKeyPlayer.toggle_play.action,
},
MediaPreviousTrack: {
type: hotKeyPlayer.prev.type,
name: null,
action: hotKeyPlayer.prev.action,
},
MediaNextTrack: {
type: hotKeyPlayer.next.type,
name: null,
action: hotKeyPlayer.next.action,
},
'mod+alt+f5': {
type: hotKeyPlayer.toggle_play.type,
name: hotKeyPlayer.toggle_play.name,
action: hotKeyPlayer.toggle_play.action,
},
'mod+alt+arrowleft': {
type: hotKeyPlayer.prev.type,
name: hotKeyPlayer.prev.name,
action: hotKeyPlayer.prev.action,
},
'mod+alt+arrowright': {
type: hotKeyPlayer.next.type,
name: hotKeyPlayer.next.name,
action: hotKeyPlayer.next.action,
},
'mod+alt+arrowup': {
type: hotKeyPlayer.volume_up.type,
name: hotKeyPlayer.volume_up.name,
action: hotKeyPlayer.volume_up.action,
},
'mod+alt+arrowdown': {
type: hotKeyPlayer.volume_down.type,
name: hotKeyPlayer.volume_down.name,
action: hotKeyPlayer.volume_down.action,
},
'mod+alt+0': {
type: hotKeyDesktopLyric.toggle_visible.type,
name: hotKeyDesktopLyric.toggle_visible.name,
action: hotKeyDesktopLyric.toggle_visible.action,
},
'mod+alt+-': {
type: hotKeyDesktopLyric.toggle_lock.type,
name: hotKeyDesktopLyric.toggle_lock.name,
action: hotKeyDesktopLyric.toggle_lock.action,
},
'mod+alt+=': {
type: hotKeyDesktopLyric.toggle_always_top.type,
name: hotKeyDesktopLyric.toggle_always_top.name,
action: hotKeyDesktopLyric.toggle_always_top.action,
},
},
},
}

View File

@ -0,0 +1,93 @@
import { HOTKEY_PLAYER, HOTKEY_COMMON, HOTKEY_DESKTOP_LYRIC } from './hotKey'
const local: LX.HotKeyConfig = {
enable: true,
keys: {
'mod+f5': {
type: HOTKEY_PLAYER.toggle_play.type,
name: HOTKEY_PLAYER.toggle_play.name,
action: HOTKEY_PLAYER.toggle_play.action,
},
'mod+arrowleft': {
type: HOTKEY_PLAYER.prev.type,
name: HOTKEY_PLAYER.prev.name,
action: HOTKEY_PLAYER.prev.action,
},
'mod+arrowright': {
type: HOTKEY_PLAYER.next.type,
name: HOTKEY_PLAYER.next.name,
action: HOTKEY_PLAYER.next.action,
},
f1: {
type: HOTKEY_COMMON.focusSearchInput.type,
name: HOTKEY_COMMON.focusSearchInput.name,
action: HOTKEY_COMMON.focusSearchInput.action,
},
},
}
const global: LX.HotKeyConfig = {
enable: false,
keys: {
MediaPlayPause: {
type: HOTKEY_PLAYER.toggle_play.type,
name: '',
action: HOTKEY_PLAYER.toggle_play.action,
},
MediaPreviousTrack: {
type: HOTKEY_PLAYER.prev.type,
name: '',
action: HOTKEY_PLAYER.prev.action,
},
MediaNextTrack: {
type: HOTKEY_PLAYER.next.type,
name: '',
action: HOTKEY_PLAYER.next.action,
},
'mod+alt+f5': {
type: HOTKEY_PLAYER.toggle_play.type,
name: HOTKEY_PLAYER.toggle_play.name,
action: HOTKEY_PLAYER.toggle_play.action,
},
'mod+alt+arrowleft': {
type: HOTKEY_PLAYER.prev.type,
name: HOTKEY_PLAYER.prev.name,
action: HOTKEY_PLAYER.prev.action,
},
'mod+alt+arrowright': {
type: HOTKEY_PLAYER.next.type,
name: HOTKEY_PLAYER.next.name,
action: HOTKEY_PLAYER.next.action,
},
'mod+alt+arrowup': {
type: HOTKEY_PLAYER.volume_up.type,
name: HOTKEY_PLAYER.volume_up.name,
action: HOTKEY_PLAYER.volume_up.action,
},
'mod+alt+arrowdown': {
type: HOTKEY_PLAYER.volume_down.type,
name: HOTKEY_PLAYER.volume_down.name,
action: HOTKEY_PLAYER.volume_down.action,
},
'mod+alt+0': {
type: HOTKEY_DESKTOP_LYRIC.toggle_visible.type,
name: HOTKEY_DESKTOP_LYRIC.toggle_visible.name,
action: HOTKEY_DESKTOP_LYRIC.toggle_visible.action,
},
'mod+alt+-': {
type: HOTKEY_DESKTOP_LYRIC.toggle_lock.type,
name: HOTKEY_DESKTOP_LYRIC.toggle_lock.name,
action: HOTKEY_DESKTOP_LYRIC.toggle_lock.action,
},
'mod+alt+=': {
type: HOTKEY_DESKTOP_LYRIC.toggle_always_top.type,
name: HOTKEY_DESKTOP_LYRIC.toggle_always_top.name,
action: HOTKEY_DESKTOP_LYRIC.toggle_always_top.action,
},
},
}
export default {
local,
global,
}

View File

@ -1,142 +0,0 @@
const path = require('path')
const os = require('os')
const defaultSetting = {
version: '1.0.59',
player: {
togglePlayMethod: 'listLoop',
highQuality: false,
isShowTaskProgess: true,
volume: 1,
isMute: false,
mediaDeviceId: 'default',
isMediaDeviceRemovedStopPlay: false,
isShowLyricTranslation: false,
isShowLyricRoma: false,
isS2t: false, // 是否将歌词从简体转换为繁体
isPlayLxlrc: true,
isSavePlayTime: false,
audioVisualization: false,
waitPlayEndStop: true,
waitPlayEndStopTime: '',
autoSkipOnError: true,
},
playDetail: {
isZoomActiveLrc: true,
isShowLyricProgressSetting: false,
style: {
fontSize: 100,
align: 'center',
},
},
desktopLyric: {
enable: false,
isLock: false,
isAlwaysOnTop: false,
isAlwaysOnTopLoop: false,
width: 450,
height: 300,
x: null,
y: null,
theme: 0,
isLockScreen: true,
isDelayScroll: true,
isHoverHide: false,
style: {
font: '',
fontSize: 120,
opacity: 95,
isZoomActiveLrc: true,
},
},
list: {
isClickPlayList: false,
isShowAlbumName: true,
isShowSource: true,
isSaveScrollLocation: true,
addMusicLocationType: 'top',
},
download: {
enable: false,
savePath: path.join(os.homedir(), 'Desktop'),
fileName: '歌名 - 歌手',
maxDownloadNum: 3,
isDownloadLrc: false,
lrcFormat: 'utf8',
isEmbedPic: true,
isEmbedLyric: false,
isUseOtherSource: false,
},
leaderboard: {
source: 'kw',
tabId: 'kw__16',
},
songList: {
source: 'kg',
sortId: '5',
tagInfo: {
name: '默认',
id: null,
},
},
odc: {
isAutoClearSearchInput: false,
isAutoClearSearchList: false,
},
search: {
searchSource: 'all',
tempSearchSource: 'kw',
isShowHotSearch: false,
isShowHistorySearch: false,
isFocusSearchBox: false,
},
network: {
proxy: {
enable: false,
host: '',
port: '',
username: '',
password: '',
},
},
tray: {
isShow: false,
isToTray: false,
themeId: 0,
},
sync: {
enable: false,
port: '23332',
},
windowSizeId: 2,
startInFullscreen: false,
theme: {
id: 0,
lightId: 0,
darkId: 13,
},
langId: null,
sourceId: 'kw',
apiSource: 'temp',
sourceNameType: 'alias',
font: '',
isShowAnimation: true,
randomAnimate: true,
ignoreVersion: null,
isAgreePact: false,
controlBtnPosition: process.platform === 'darwin' ? 'left' : 'right',
}
const overwriteSetting = {
}
// 使用新年皮肤
if (new Date().getMonth() < 2) {
defaultSetting.theme.id = 9
defaultSetting.desktopLyric.theme = 3
}
exports.defaultSetting = defaultSetting
exports.overwriteSetting = overwriteSetting

View File

@ -0,0 +1,120 @@
import { join } from 'path'
import { homedir } from 'os'
const defaultSetting: LX.AppSetting = {
version: '2.0.0',
'common.windowSizeId': 2,
'common.fontSize': 16,
'common.startInFullscreen': false,
'common.langId': null,
'common.apiSource': 'temp',
'common.sourceNameType': 'alias',
'common.font': '',
'common.isShowAnimation': true,
'common.randomAnimate': true,
'common.isAgreePact': false,
'common.controlBtnPosition': process.platform === 'darwin' ? 'left' : 'right',
'player.startupAutoPlay': false,
'player.togglePlayMethod': 'listLoop',
'player.highQuality': false,
'player.isShowTaskProgess': true,
'player.volume': 1,
'player.isMute': false,
'player.mediaDeviceId': 'default',
'player.isMediaDeviceRemovedStopPlay': false,
'player.isShowLyricTranslation': false,
'player.isShowLyricRoma': false,
'player.isS2t': false,
'player.isPlayLxlrc': false,
'player.isSavePlayTime': false,
'player.audioVisualization': false,
'player.waitPlayEndStop': true,
'player.waitPlayEndStopTime': '',
'player.autoSkipOnError': true,
'playDetail.isZoomActiveLrc': false,
'playDetail.isShowLyricProgressSetting': false,
'playDetail.style.fontSize': 100,
'playDetail.style.align': 'center',
'desktopLyric.enable': false,
'desktopLyric.isLock': false,
'desktopLyric.isAlwaysOnTop': false,
'desktopLyric.isAlwaysOnTopLoop': false,
'desktopLyric.audioVisualization': false,
'desktopLyric.width': 450,
'desktopLyric.height': 300,
'desktopLyric.x': null,
'desktopLyric.y': null,
'desktopLyric.isLockScreen': true,
'desktopLyric.isDelayScroll': true,
'desktopLyric.isHoverHide': false,
'desktopLyric.direction': 'horizontal',
'desktopLyric.style.align': 'center',
'desktopLyric.style.font': '',
'desktopLyric.style.fontSize': 20,
'desktopLyric.style.lyricUnplayColor': 'rgba(255, 255, 255, 1)',
'desktopLyric.style.lyricPlayedColor': 'rgba(7, 197, 86, 1)',
'desktopLyric.style.lyricShadowColor': 'rgba(0, 0, 0, 0.14)',
// 'desktopLyric.style.fontWeight': false,
'desktopLyric.style.opacity': 95,
'desktopLyric.style.ellipsis': false,
'desktopLyric.style.isZoomActiveLrc': true,
'list.isClickPlayList': false,
'list.isShowSource': true,
'list.isSaveScrollLocation': true,
'list.addMusicLocationType': 'top',
'list.actionButtonsVisible': false,
'download.enable': false,
'download.savePath': join(homedir(), 'Desktop'),
'download.fileName': '歌名 - 歌手',
'download.maxDownloadNum': 3,
'download.isDownloadLrc': false,
'download.isDownloadTLrc': false,
'download.isDownloadRLrc': false,
'download.lrcFormat': 'utf8',
'download.isEmbedPic': true,
'download.isEmbedLyric': false,
'download.isUseOtherSource': false,
'search.isShowHotSearch': false,
'search.isShowHistorySearch': false,
'search.isFocusSearchBox': false,
'network.proxy.enable': false,
'network.proxy.host': '',
'network.proxy.port': '',
'network.proxy.username': '',
'network.proxy.password': '',
'tray.enable': false,
// 'tray.isToTray': false,
'tray.themeId': 0,
'sync.enable': false,
'sync.port': '23332',
'theme.id': 'blue_plus',
// 'theme.id': 'green',
'theme.lightId': 'green',
'theme.darkId': 'black',
'odc.isAutoClearSearchInput': false,
'odc.isAutoClearSearchList': false,
}
// 使用新年皮肤
if (new Date().getMonth() < 2) {
defaultSetting['theme.id'] = 'happy_new_year'
defaultSetting['desktopLyric.style.lyricPlayedColor'] = 'rgba(255, 18, 34, 1)'
}
export default defaultSetting

View File

@ -1,6 +1,12 @@
const { log } = require('./utils')
import { log } from './utils'
process.on('uncaughtException', function(err) {
const ignoreErrorMessage = [
'Possible side-effect in debug-evaluate',
'Unexpected end of input',
]
process.on('uncaughtException', err => {
if (ignoreErrorMessage.includes(err.message)) return
console.error('An uncaught error occurred!')
console.error(err)
log.error(err)

View File

@ -1,84 +1,101 @@
const names = require('@main/events/_name')
import { APP_EVENT_NAMES } from './constants'
const keyName = {
common: APP_EVENT_NAMES.winMainName,
player: APP_EVENT_NAMES.winMainName,
desktop_lyric: APP_EVENT_NAMES.winLyricName,
}
const hotKey = {
common: {
min: {
name: 'min',
action: 'min',
type: '',
},
min_toggle: {
name: 'toggle_min',
action: 'toggle_min',
type: '',
},
hide_toggle: {
name: 'toggle_hide',
action: 'toggle_hide',
type: '',
},
close: {
name: 'toggle_close',
action: 'toggle_close',
type: '',
},
focusSearchInput: {
name: 'focus_search_input',
action: 'focus_search_input',
type: '',
},
},
player: {
toggle_play: {
name: 'toggle_play',
action: 'toggle_play',
type: '',
},
next: {
name: 'next',
action: 'next',
type: '',
},
prev: {
name: 'prev',
action: 'prev',
type: '',
},
volume_up: {
name: 'volume_up',
action: 'volume_up',
type: '',
},
volume_down: {
name: 'volume_down',
action: 'volume_down',
type: '',
},
volume_mute: {
name: 'volume_mute',
action: 'volume_mute',
type: '',
},
},
desktop_lyric: {
toggle_visible: {
name: 'toggle_visible',
action: 'toggle_visible',
type: '',
},
toggle_lock: {
name: 'toggle_lock',
action: 'toggle_lock',
type: '',
},
toggle_always_top: {
name: 'toggle_always_top',
action: 'toggle_always_top',
type: '',
},
},
}
const keyName = {
common: names.mainWindow.name,
player: names.mainWindow.name,
desktop_lyric: names.winLyric.name,
}
for (const type of Object.keys(hotKey)) {
let item = hotKey[type]
for (const key of Object.keys(item)) {
item[key].action = `${type}_${item[key].action}`
item[key].name = `${type}_${item[key].name}`
item[key].type = keyName[type]
for (const type of Object.keys(hotKey) as Array<keyof typeof hotKey>) {
let keys = hotKey[type]
for (const key of Object.keys(keys) as Array<keyof typeof keys>) {
const keyInfo: LX.HotKey = keys[key]
keyInfo.action = `${type}_${keyInfo.action}`
keyInfo.name = `${type}_${keyInfo.name}`
keyInfo.type = keyName[type] as keyof typeof hotKey
}
}
exports.common = hotKey.common
exports.player = hotKey.player
exports.desktop_lyric = hotKey.desktop_lyric
export const HOTKEY_COMMON = hotKey.common
export const HOTKEY_PLAYER = hotKey.player
export const HOTKEY_DESKTOP_LYRIC = hotKey.desktop_lyric

View File

@ -1,52 +0,0 @@
const { ipcMain, ipcRenderer } = require('electron')
const names = require('./ipcNames')
exports.mainOn = (name, callback) => {
ipcMain.on(name, callback)
}
exports.mainOnce = (name, callback) => {
ipcMain.once(name, callback)
}
exports.mainOff = (name, callback) => {
ipcMain.removeListener(name, callback)
}
exports.mainOffAll = name => {
ipcMain.removeAllListeners(name)
}
exports.mainHandle = (name, callback) => {
ipcMain.handle(name, callback)
}
exports.mainHandleOnce = (name, callback) => {
ipcMain.handleOnce(name, callback)
}
exports.mainHandleRemove = name => {
ipcMain.removeListener(name)
}
exports.mainSend = (window, name, params) => {
window.webContents.send(name, params)
}
exports.rendererSend = (name, params) => {
ipcRenderer.send(name, params)
}
exports.rendererSendSync = (name, params) => ipcRenderer.sendSync(name, params)
exports.rendererInvoke = (name, params) => ipcRenderer.invoke(name, params)
exports.rendererOn = (name, callback) => {
ipcRenderer.on(name, callback)
}
exports.rendererOnce = (name, callback) => {
ipcRenderer.once(name, callback)
}
exports.rendererOff = (name, callback) => {
ipcRenderer.removeListener(name, callback)
}
exports.rendererOffAll = name => {
ipcRenderer.removeAllListeners(name)
}
exports.NAMES = names

View File

@ -1,126 +0,0 @@
const names = {
mainWindow: {
focus: 'focus',
close: 'close',
min: 'min',
max: 'max',
fullscreen: 'fullscreen',
set_app_name: 'set_app_name',
clear_cache: 'clear_cache',
get_cache_size: 'get_cache_size',
get_env_params: 'get_env_params',
clear_env_params_deeplink: 'clear_env_params_deeplink',
wait: 'wait',
wait_cancel: 'wait_cancel',
interval: 'interval',
interval_callback: 'interval_callback',
interval_cancel: 'interval_cancel',
open_dev_tools: 'open_dev_tools',
system_theme_change: 'system_theme_change',
set_music_meta: 'set_music_meta',
progress: 'progress',
change_tray: 'change_tray',
quit_update: 'quit_update',
update_available: 'update_available',
update_error: 'update_error',
update_progress: 'update_progress',
update_downloaded: 'update_downloaded',
update_not_available: 'update_not_available',
set_ignore_mouse_events: 'set_ignore_mouse_events',
set_app_setting: 'set_app_setting',
set_window_size: 'set_window_size',
show_save_dialog: 'show_save_dialog',
get_system_fonts: 'get_system_fonts',
handle_request: 'handle_request',
cancel_request: 'cancel_request',
handle_xm_verify_open: 'handle_xm_verify_open',
handle_xm_verify_close: 'handle_xm_verify_close',
select_dir: 'select_dir',
restart_window: 'restart_window',
lang_s2t: 'lang_s2t',
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
get_lyric_info: 'get_lyric_info',
set_lyric_info: 'set_lyric_info',
set_config: 'set_config',
set_hot_key_config: 'set_hot_key_config',
key_down: 'key_down',
quit: 'quit',
min_toggle: 'min_toggle',
hide_toggle: 'hide_toggle',
get_data_path: 'get_data_path',
show_dialog: 'show_dialog',
get_setting: 'get_setting',
get_playlist: 'get_playlist',
save_playlist: 'save_playlist',
get_data: 'get_data',
save_data: 'save_data',
get_hot_key: 'get_hot_key',
import_user_api: 'import_user_api',
remove_user_api: 'remove_user_api',
set_user_api: 'set_user_api',
get_user_api_list: 'get_user_api_list',
request_user_api: 'request_user_api',
request_user_api_cancel: 'request_user_api_cancel',
get_user_api_status: 'get_user_api_status',
user_api_status: 'user_api_status',
user_api_show_update_alert: 'user_api_show_update_alert',
user_api_set_allow_update_alert: 'user_api_set_allow_update_alert',
get_lyric: 'get_lyric',
save_lyric: 'save_lyric',
clear_lyric: 'clear_lyric',
get_lyric_raw: 'get_lyric_raw',
save_lyric_raw: 'save_lyric_raw',
clear_lyric_raw: 'clear_lyric_raw',
get_lyric_edited: 'get_lyric_edited',
save_lyric_edited: 'save_lyric_edited',
remove_lyric_edited: 'remove_lyric_edited',
get_music_url: 'get_music_url',
save_music_url: 'save_music_url',
clear_music_url: 'clear_music_url',
sync_enable: 'sync_enable',
sync_status: 'sync_status',
sync_get_status: 'sync_get_status',
sync_generate_code: 'sync_generate_code',
sync_action_list: 'sync_action_list',
sync_list: 'sync_list',
taskbar_set_thumbar_buttons: 'taskbar_set_thumbar_buttons',
taskbar_set_thumbnail_clip: 'taskbar_set_thumbnail_clip',
taskbar_on_thumbar_button_click: 'taskbar_on_thumbar_button_click',
},
winLyric: {
close: 'close',
set_lyric_info: 'set_lyric_info',
get_lyric_info: 'get_lyric_info',
set_lyric_config: 'set_lyric_config',
get_lyric_config: 'get_lyric_config',
set_win_bounds: 'set_win_bounds',
key_down: 'key_down',
},
hotKey: {
enable: 'enable',
status: 'status',
set_config: 'set_config',
},
}
for (const item of Object.keys(names)) {
let name = names[item]
for (const key of Object.keys(name)) {
name[key] = `${item}_${name[key]}`
}
}
exports.mainWindow = names.mainWindow
exports.winLyric = names.winLyric
exports.hotKey = names.hotKey

177
src/common/ipcNames.ts Normal file
View File

@ -0,0 +1,177 @@
const modules = {
common: {
get_env_params: 'get_env_params',
deeplink: 'deeplink',
clear_env_params_deeplink: 'clear_env_params_deeplink',
system_theme_change: 'system_theme_change',
theme_change: 'theme_change',
get_system_fonts: 'get_system_fonts',
get_app_setting: 'get_app_setting',
set_app_setting: 'set_app_setting',
},
player: {
invoke_play_music: 'play_music',
invoke_play_next: 'play_next',
invoke_play_prev: 'play_prev',
invoke_toggle_play: 'toggle_play',
player_play: 'player_play',
player_pause: 'player_pause',
player_stop: 'player_stop',
player_error: 'player_error',
list_data_overwire: 'list_data_overwire',
list_get: 'list_get',
list_add: 'list_add',
list_remove: 'list_remove',
list_update: 'list_update',
list_update_position: 'list_update_position',
list_music_get: 'list_music_get',
list_music_add: 'list_music_add',
list_music_move: 'list_music_move',
list_music_remove: 'list_music_remove',
list_music_update: 'list_music_update',
list_music_update_position: 'list_music_update_position',
list_music_overwrite: 'list_music_overwrite',
list_music_clear: 'list_music_clear',
list_music_check_exist: 'list_music_check_exist',
list_music_get_list_ids: 'list_music_get_list_ids',
},
winMain: {
focus: 'focus',
close: 'close',
min: 'min',
max: 'max',
fullscreen: 'fullscreen',
set_app_name: 'set_app_name',
clear_cache: 'clear_cache',
get_cache_size: 'get_cache_size',
inited: 'inited',
show_save_dialog: 'show_save_dialog',
show_select_dialog: 'show_select_dialog',
show_dialog: 'show_dialog',
open_dev_tools: 'open_dev_tools',
progress: 'progress',
change_tray: 'change_tray',
quit_update: 'quit_update',
update_check: 'update_check',
update_available: 'update_available',
update_error: 'update_error',
update_progress: 'update_progress',
update_downloaded: 'update_downloaded',
update_not_available: 'update_not_available',
set_ignore_mouse_events: 'set_ignore_mouse_events',
set_window_size: 'set_window_size',
handle_request: 'handle_request',
cancel_request: 'cancel_request',
restart_window: 'restart_window',
// lang_s2t: 'lang_s2t',
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
get_lyric_info: 'get_lyric_info',
set_lyric_info: 'set_lyric_info',
set_config: 'set_config',
set_hot_key_config: 'set_hot_key_config',
on_config_change: 'on_config_change',
key_down: 'key_down',
quit: 'quit',
min_toggle: 'min_toggle',
hide_toggle: 'hide_toggle',
get_other_source: 'get_other_source',
save_other_source: 'save_other_source',
clear_other_source: 'clear_other_source',
get_other_source_count: 'get_other_source_count',
get_data: 'get_data',
save_data: 'save_data',
get_hot_key: 'get_hot_key',
import_user_api: 'import_user_api',
remove_user_api: 'remove_user_api',
set_user_api: 'set_user_api',
get_user_api_list: 'get_user_api_list',
request_user_api: 'request_user_api',
request_user_api_cancel: 'request_user_api_cancel',
get_user_api_status: 'get_user_api_status',
user_api_status: 'user_api_status',
user_api_show_update_alert: 'user_api_show_update_alert',
user_api_set_allow_update_alert: 'user_api_set_allow_update_alert',
get_palyer_lyric: 'get_lyric',
// save_lyric: 'save_lyric',
// clear_lyric: 'clear_lyric',
get_lyric_raw: 'get_lyric_raw',
save_lyric_raw: 'save_lyric_raw',
clear_lyric_raw: 'clear_lyric_raw',
get_lyric_raw_count: 'get_lyric_raw_count',
get_lyric_edited: 'get_lyric_edited',
save_lyric_edited: 'save_lyric_edited',
remove_lyric_edited: 'remove_lyric_edited',
clear_lyric_edited: 'clear_lyric_edited',
get_lyric_edited_count: 'get_lyric_edited_count',
get_music_url: 'get_music_url',
save_music_url: 'save_music_url',
clear_music_url: 'clear_music_url',
get_music_url_count: 'get_music_url_count',
sync_action: 'sync_action',
process_new_desktop_lyric_client: 'process_new_desktop_lyric_client',
player_action_set_buttons: 'player_action_set_buttons',
// player_action_set_thumbnail_clip: 'player_action_set_thumbnail_clip',
player_action_on_button_click: 'player_action_on_button_click',
get_themes: 'get_themes',
save_theme: 'save_theme',
remove_theme: 'remove_theme',
download_list_get: 'download_list_get',
download_list_add: 'download_list_add',
download_list_update: 'download_list_update',
download_list_remove: 'download_list_remove',
download_list_clear: 'download_list_clear',
},
winLyric: {
close: 'close',
set_config: 'set_config',
get_config: 'get_config',
on_config_change: 'on_config_change',
main_window_inited: 'main_window_inited',
set_win_bounds: 'set_win_bounds',
key_down: 'key_down',
request_main_window_channel: 'request_main_window_channel',
provide_main_window_channel: 'provide_main_window_channel',
},
hotKey: {
enable: 'enable',
status: 'status',
set_config: 'set_config',
},
}
for (const moduleName of Object.keys(modules) as Array<keyof typeof modules>) {
let eventNames = modules[moduleName]
for (const eventName of Object.keys(eventNames) as Array<keyof typeof eventNames>) {
eventNames[eventName] = `${moduleName}_${eventName as string}` as never
}
}
// for (const moduleName of Object.keys(modules) as Array<keyof typeof modules>) {
// let eventNames = modules[moduleName]
// for (const eventName of Object.keys(eventNames)) {
// eventNames[eventName] = `${moduleName}_${eventName}`
// }
// }
export const CMMON_EVENT_NAME = modules.common
export const PLAYER_EVENT_NAME = modules.player
export const WIN_MAIN_RENDERER_EVENT_NAME = modules.winMain
export const WIN_LYRIC_RENDERER_EVENT_NAME = modules.winLyric
export const HOTKEY_RENDERER_EVENT_NAME = modules.hotKey

54
src/common/mainIpc.ts Normal file
View File

@ -0,0 +1,54 @@
import { ipcMain } from 'electron'
export function mainOn(name: string, listener: LX.IpcMainEventListener): void
export function mainOn<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void
export function mainOn<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void {
ipcMain.on(name, (event, params) => {
listener({ event, params })
})
}
export function mainOnce(name: string, listener: LX.IpcMainEventListener): void
export function mainOnce<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void
export function mainOnce<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void {
ipcMain.once(name, (event, params) => {
listener({ event, params })
})
}
export const mainOff = (name: string, listener: (...args: any[]) => void) => {
ipcMain.removeListener(name, listener)
}
export const mainOffAll = (name: string) => {
ipcMain.removeAllListeners(name)
}
export function mainHandle(name: string, listener: LX.IpcMainInvokeEventListener): void
export function mainHandle<T>(name: string, listener: LX.IpcMainInvokeEventListenerParams<T>): void
export function mainHandle<V>(name: string, listener: LX.IpcMainInvokeEventListenerValue<V>): void
export function mainHandle<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void
export function mainHandle<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void {
ipcMain.handle(name, async(event, params) => {
return await listener({ event, params })
})
}
export function mainHandleOnce(name: string, listener: LX.IpcMainInvokeEventListener): void
export function mainHandleOnce<T>(name: string, listener: LX.IpcMainInvokeEventListenerParams<T>): void
export function mainHandleOnce<V>(name: string, listener: LX.IpcMainInvokeEventListenerValue<V>): void
export function mainHandleOnce<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void
export function mainHandleOnce<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void {
ipcMain.handleOnce(name, async(event, params) => {
return await listener({ event, params })
})
}
export const mainHandleRemove = (name: string) => {
ipcMain.removeHandler(name)
}
export function mainSend(window: Electron.BrowserWindow, name: string): void
export function mainSend<T>(window: Electron.BrowserWindow, name: string, params: T): void
export function mainSend<T>(window: Electron.BrowserWindow, name: string, params?: T): void {
window.webContents.send(name, params)
}

45
src/common/rendererIpc.ts Normal file
View File

@ -0,0 +1,45 @@
import { ipcRenderer } from 'electron'
export function rendererSend(name: string): void
export function rendererSend<T>(name: string, params: T): void
export function rendererSend<T>(name: string, params?: T): void {
ipcRenderer.send(name, params)
}
export function rendererSendSync(name: string): void
export function rendererSendSync<T>(name: string, params: T): void
export function rendererSendSync<T>(name: string, params?: T): void {
ipcRenderer.sendSync(name, params)
}
export async function rendererInvoke(name: string): Promise<void>
export async function rendererInvoke<V>(name: string): Promise<V>
export async function rendererInvoke<T>(name: string, params: T): Promise<void>
export async function rendererInvoke<T, V>(name: string, params: T): Promise<V>
export async function rendererInvoke <T, V>(name: string, params?: T): Promise<V> {
return await ipcRenderer.invoke(name, params)
}
export function rendererOn(name: string, listener: LX.IpcRendererEventListener): void
export function rendererOn<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void
export function rendererOn<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void {
ipcRenderer.on(name, (event, params) => {
listener({ event, params })
})
}
export function rendererOnce(name: string, listener: LX.IpcRendererEventListener): void
export function rendererOnce<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void
export function rendererOnce<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void {
ipcRenderer.once(name, (event, params) => {
listener({ event, params })
})
}
export const rendererOff = (name: string, listener: (...args: any[]) => any) => {
ipcRenderer.removeListener(name, listener)
}
export const rendererOffAll = (name: string) => {
ipcRenderer.removeAllListeners(name)
}

View File

@ -1,42 +0,0 @@
const Store = require('electron-store')
const { dialog, app, shell } = require('electron')
const path = require('path')
const fs = require('fs')
const log = require('electron-log')
const stores = {}
/**
* 获取 Store 对象
* @param {*} name store
* @param {*} isIgnoredError 是否忽略错误
* @param {*} isShowErrorAlert 是否显示错误弹窗
* @returns Store
*/
module.exports = (name, isIgnoredError = true, isShowErrorAlert = true) => {
if (stores[name]) return stores[name]
let store
try {
store = stores[name] = new Store({ name, clearInvalidConfig: false })
} catch (error) {
log.error(error)
if (!isIgnoredError) throw error
const backPath = path.join(app.getPath('userData'), name + '.json.bak')
fs.copyFileSync(path.join(app.getPath('userData'), name + '.json'), backPath)
if (isShowErrorAlert) {
dialog.showMessageBoxSync({
type: 'error',
message: name + ' data load error',
detail: `We have helped you back up the old ${name} file to: ${backPath}\nYou can try to repair and restore it manually\n\nError detail: ${error.message}`,
})
shell.showItemInFolder(backPath)
}
store = new Store({ name, clearInvalidConfig: true })
}
return store
}

View File

@ -0,0 +1,73 @@
/* eslint-disable */
// https://github.com/PimpTrizkit/PJs/wiki/12.-Shade,-Blend-and-Convert-a-Web-Color-(pSBC.js)#micro-functions-version-4
/**
* Blend color (Lighten or Darken)
* @param {number} p 混合百分比 范围 0.0 - 1.0
* @param {string} c0 rgb(a) color1
* @param {string} c1 rgb(a) color2
* @returns color
*/
exports.RGB_Linear_Blend=(p,c0,c1)=>{
var i=parseInt,r=Math.round,P=1-p,[a,b,c,d]=c0.split(","),[e,f,g,h]=c1.split(","),x=d||h,j=x?","+(!d?h:!h?d:r((parseFloat(d)*P+parseFloat(h)*p)*1000)/1000+")"):")";
return"rgb"+(x?"a(":"(")+r(i(a[3]=="a"?a.slice(5):a.slice(4))*P+i(e[3]=="a"?e.slice(5):e.slice(4))*p)+","+r(i(b)*P+i(f)*p)+","+r(i(c)*P+i(g)*p)+j;
}
/**
* Blend color (Lighten or Darken)
* @param {number} p 混合百分比 范围 0.0 - 1.0
* @param {string} c0 rgb(a) color1
* @param {string} c1 rgb(a) color2
* @returns color
*/
exports.RGB_Log_Blend=(p,c0,c1)=>{
var i=parseInt,r=Math.round,P=1-p,[a,b,c,d]=c0.split(","),[e,f,g,h]=c1.split(","),x=d||h,j=x?","+(!d?h:!h?d:r((parseFloat(d)*P+parseFloat(h)*p)*1000)/1000+")"):")";
return"rgb"+(x?"a(":"(")+r((P*i(a[3]=="a"?a.slice(5):a.slice(4))**2+p*i(e[3]=="a"?e.slice(5):e.slice(4))**2)**0.5)+","+r((P*i(b)**2+p*i(f)**2)**0.5)+","+r((P*i(c)**2+p*i(g)**2)**0.5)+j;
}
/**
* Shade color (Lighten or Darken)
* @param {number} p Shade 百分比范围为 -1.0 - 1.0 负为黑色正为白色
* @param {string} c0 rgb(a) color
* @returns color
*/
exports.RGB_Linear_Shade=(p,c0)=>{
var i=parseInt,r=Math.round,[a,b,c,d]=c0.split(","),n=p<0,t=n?0:255*p,P=n?1+p:1-p;
return"rgb"+(d?"a(":"(")+r(i(a[3]=="a"?a.slice(5):a.slice(4))*P+t)+","+r(i(b)*P+t)+","+r(i(c)*P+t)+(d?","+d:")");
}
/**
* Shade color (Lighten or Darken)
* @param {number} p Shade 百分比范围为 -1.0 - 1.0 负为黑色正为白色
* @param {string} c0 rgb(a) color
* @returns color
*/
exports.RGB_Log_Shade=(p,c0)=>{
var i=parseInt,r=Math.round,[a,b,c,d]=c0.split(","),n=p<0,t=n?0:p*255**2,P=n?1+p:1-p;
return"rgb"+(d?"a(":"(")+r((P*i(a[3]=="a"?a.slice(5):a.slice(4))**2+t)**0.5)+","+r((P*i(b)**2+t)**0.5)+","+r((P*i(c)**2+t)**0.5)+(d?","+d:")");
}
/**
* 修改透明度
* @param {number} p 透明度 -1.0 - 1.0
* @param {string} color
* @returns color
*/
exports.RGB_Alpha_Shade = (p, color) => {
var i = parseInt
var n = p < 0
var [r, g, b, a] = color.split(",")
r = r[3] == 'a' ? r.slice(5) : r.slice(4)
if (a) {
a = parseFloat(a)
a = a - (n ? (1 - a) * p : a * p)
a = n ? Math.max(0, a) : Math.min(1, a)
} else {
a = 1 - p
a = Math.min(1, a)
}
return `rgba(${i(r)}, ${i(g)}, ${i(b)}, ${a.toFixed(2)})`
}

View File

@ -0,0 +1,331 @@
//! 更新默认主题配置后,需要执行 npm run build:theme 重新构建index.json
const fs = require('fs')
const path = require('path')
const { createThemeColors } = require('./utils')
const defaultThemes = [
{
id: 'green',
name: '绿意盎然',
isDark: false,
config: {
primary: 'rgb(77, 175, 124)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#4baed5',
'--color-badge-tertiary': '#e7aa36',
},
},
{
id: 'blue',
name: '蓝田生玉',
isDark: false,
config: {
primary: 'rgb(52, 152, 219)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#5cbf9b',
'--color-badge-tertiary': '#5cbf9b',
},
},
{
id: 'blue_plus',
name: '蛋雅深蓝',
isDark: false,
config: {
primary: 'rgb(77, 131, 175)',
'--color-app-background': 'var(--color-primary-light-600-alpha-600)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': 'rgba(66.6, 150.7, 171, 1)',
'--color-badge-tertiary': 'rgba(54, 196, 231, 1)',
},
},
{
id: 'orange',
name: '橙黄橘绿',
isDark: false,
config: {
primary: 'rgb(245, 171, 53)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#9ed458',
'--color-badge-tertiary': '#9ed458',
},
},
{
id: 'red',
name: '热情似火',
isDark: false,
config: {
primary: 'rgb(214, 69, 65)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#dfbb6b',
'--color-badge-tertiary': '#dfbb6b',
},
},
{
id: 'pink',
name: '粉装玉琢',
isDark: false,
config: {
primary: 'rgb(241, 130, 141)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#f5b684',
'--color-badge-tertiary': '#f5b684',
},
},
{
id: 'purple',
name: '重斤球紫',
isDark: false,
config: {
primary: 'rgb(155, 89, 182)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#e5a39f',
'--color-badge-tertiary': '#e5a39f',
},
},
{
id: 'grey',
name: '灰常美丽',
isDark: false,
config: {
primary: 'rgb(108, 122, 137)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#b19b9f',
'--color-badge-tertiary': '#b19b9f',
},
},
{
id: 'ming',
name: '青出于黑',
isDark: false,
config: {
primary: 'rgb(51, 110, 123)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#6376a2',
'--color-badge-tertiary': '#6376a2',
},
},
{
id: 'blue2',
name: '清热板蓝',
isDark: false,
config: {
primary: 'rgb(79, 98, 208)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#b080db',
'--color-badge-tertiary': '#b080db',
},
},
{
id: 'black',
name: '黑灯瞎火',
isDark: true,
config: {
primary: 'rgb(150, 150, 150)',
'--color-app-background': 'rgba(0, 0, 0, 0)',
'--color-main-background': 'rgba(19, 19, 19, 0.9)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'url(./theme_images/landingMoon.png)',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary-dark-200)',
'--color-badge-secondary': 'var(--color-primary)',
'--color-badge-tertiary': 'var(--color-primary-dark-300)',
},
},
{
id: 'mid_autumn',
name: '月里嫦娥',
isDark: false,
config: {
primary: 'rgb(74, 55, 82)',
'--color-app-background': 'rgba(255, 255, 255, 0)',
'--color-main-background': 'rgba(255, 255, 255, 0.9)',
'--color-nav-font': 'var(--color-primary-light-600)',
'--background-image': 'url(./theme_images/jqbg.jpg)',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#af9479',
'--color-badge-tertiary': '#af9479',
},
},
{
id: 'naruto',
name: '木叶之村',
isDark: false,
config: {
primary: 'rgb(87, 144, 167)',
'--color-app-background': 'rgba(255, 255, 255, 0.15)',
'--color-main-background': 'rgba(255, 255, 255, 0.8)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'url(./theme_images/myzcbg.jpg)',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': 'var(--color-primary-light-100)',
'--color-badge-tertiary': 'var(--color-primary-light-100)',
},
},
{
id: 'happy_new_year',
name: '新年快乐',
isDark: false,
config: {
primary: 'rgb(192, 57, 43)',
'--color-app-background': 'rgba(255, 255, 255, 0.15)',
'--color-main-background': 'rgba(255, 255, 255, 0.8)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'url(./theme_images/xnkl.png)',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': '#7fb575',
'--color-badge-secondary': '#dfbb6b',
'--color-badge-tertiary': 'var(--color-primary-light-100)',
},
},
]
const themes = defaultThemes.map(({ config: { primary, ...extInfo }, ...themeInfo }) => {
return {
...themeInfo,
isCustom: false,
config: {
themeColors: createThemeColors(primary, themeInfo.isDark),
extInfo,
},
}
})
fs.writeFileSync(path.join(__dirname, 'index.json'), JSON.stringify(themes, null, 2))

View File

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 997 KiB

After

Width:  |  Height:  |  Size: 997 KiB

3292
src/common/theme/index.json Normal file

File diff suppressed because it is too large Load Diff

42
src/common/theme/utils.js Normal file
View File

@ -0,0 +1,42 @@
const { RGB_Linear_Shade, RGB_Alpha_Shade } = require('./colorUtils')
exports.createThemeColors = (rgbaColor, isDark) => {
const colors = {
'--color-primary': rgbaColor,
}
let preColor = rgbaColor
for (let i = 1; i < 11; i += 1) {
preColor = RGB_Linear_Shade(isDark ? 0.2 : -0.1, preColor)
colors[`--color-primary-dark-${i * 100}`] = preColor
for (let j = 1; j < 10; j += 1) {
colors[`--color-primary-dark-${i * 100}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)
colors[`--color-primary-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, rgbaColor)
}
}
preColor = rgbaColor
for (let i = 1; i < 10; i += 1) {
preColor = RGB_Linear_Shade(isDark ? -0.1 : 0.2, preColor)
colors[`--color-primary-light-${i * 100}`] = preColor
for (let j = 1; j < 10; j += 1) {
colors[`--color-primary-light-${i * 100}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)
}
}
preColor = RGB_Linear_Shade(isDark ? -0.2 : 1, preColor)
colors[`--color-primary-light-${1000}`] = preColor
for (let j = 1; j < 10; j += 1) {
colors[`--color-primary-light-${1000}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)
}
colors['--color-theme'] = isDark ? colors['--color-primary-light-900'] : rgbaColor
return colors
}
// rgb(238, 238, 238)
// let prec = 'rgb(255, 255, 255)'
// let colors = [prec]
// for (let j = 1; j < 11; j += 1) {
// colors.push(prec = RGB_Linear_Shade(-0.15, prec))
// }
// console.log(colors)

16
src/common/tsconfig.json Normal file
View File

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

456
src/common/types/app_setting.d.ts vendored Normal file
View File

@ -0,0 +1,456 @@
declare namespace LX {
type AddMusicLocationType = 'top' | 'bottom'
interface AppSetting {
version: string
/**
* id
*/
'common.windowSizeId': number
/**
* id
*/
'common.fontSize': number
/**
*
*/
'common.startInFullscreen': boolean
/**
* id
*/
'common.langId': string | null
/**
* api id
*/
'common.apiSource': string
/**
*
*/
'common.sourceNameType': 'alias' | 'real'
/**
*
*/
'common.font': string
/**
*
*/
'common.isShowAnimation': boolean
/**
*
*/
'common.randomAnimate': boolean
/**
*
*/
'common.isAgreePact': boolean
/**
*
*/
'common.controlBtnPosition': 'left' | 'right'
/**
*
*/
'player.startupAutoPlay': boolean
/**
*
*/
'player.togglePlayMethod': 'listLoop' | 'random' | 'list' | 'singleLoop' | 'none'
/**
* 320k
*/
'player.highQuality': boolean
/**
*
*/
'player.isShowTaskProgess': boolean
/**
*
*/
'player.volume': number
/**
*
*/
'player.isMute': boolean
/**
* id
*/
'player.mediaDeviceId': string
/**
*
*/
'player.isMediaDeviceRemovedStopPlay': boolean
/**
*
*/
'player.isShowLyricTranslation': boolean
/**
*
*/
'player.isShowLyricRoma': boolean
/**
*
*/
'player.isS2t': boolean
/**
* OK
*/
'player.isPlayLxlrc': boolean
/**
*
*/
'player.isSavePlayTime': boolean
/**
*
*/
'player.audioVisualization': boolean
/**
* -
*/
'player.waitPlayEndStop': boolean
/**
* -
*/
'player.waitPlayEndStopTime': string
/**
*
*/
'player.autoSkipOnError': boolean
/**
* -
*/
'playDetail.isZoomActiveLrc': boolean
/**
* -
*/
'playDetail.isShowLyricProgressSetting': boolean
/**
* -
*/
'playDetail.style.fontSize': number
/**
* -
*/
'playDetail.style.align': 'center' | 'left' | 'right'
/**
*
*/
'desktopLyric.enable': boolean
/**
*
*/
'desktopLyric.isLock': boolean
/**
*
*/
'desktopLyric.isAlwaysOnTop': boolean
/**
*
*/
'desktopLyric.isAlwaysOnTopLoop': boolean
/**
*
*/
'desktopLyric.audioVisualization': boolean
/**
*
*/
'desktopLyric.width': number
/**
*
*/
'desktopLyric.height': number
/**
* x
*/
'desktopLyric.x': number | null
/**
* y
*/
'desktopLyric.y': number | null
/**
*
*/
'desktopLyric.isLockScreen': boolean
/**
*
*/
'desktopLyric.isDelayScroll': boolean
/**
*
*/
'desktopLyric.isHoverHide': boolean
/**
*
*/
'desktopLyric.direction': 'horizontal' | 'vertical'
/**
*
*/
'desktopLyric.style.align': 'center' | 'left' | 'right'
/**
*
*/
'desktopLyric.style.font': string
/**
*
*/
'desktopLyric.style.fontSize': number
/**
*
*/
'desktopLyric.style.lyricUnplayColor': string
/**
*
*/
'desktopLyric.style.lyricPlayedColor': string
/**
*
*/
'desktopLyric.style.lyricShadowColor': string
/**
*
*/
// 'desktopLyric.style.fontWeight': boolean
/**
*
*/
'desktopLyric.style.opacity': number
/**
*
*/
'desktopLyric.style.ellipsis': boolean
/**
*
*/
'desktopLyric.style.isZoomActiveLrc': boolean
/**
*
*/
'list.isClickPlayList': boolean
/**
*
*/
'list.isShowSource': boolean
/**
*
*/
'list.isSaveScrollLocation': boolean
/**
*
*/
'list.addMusicLocationType': LX.AddMusicLocationType
/**
*
*/
'list.actionButtonsVisible': boolean
/**
*
*/
'download.enable': boolean
/**
*
*/
'download.savePath': string
/**
*
*/
'download.fileName': '歌名 - 歌手' | '歌手 - 歌名' | '歌名'
/**
*
*/
'download.maxDownloadNum': number
/**
* lrc
*/
'download.isDownloadLrc': boolean
/**
*
*/
'download.isDownloadTLrc': boolean
/**
*
*/
'download.isDownloadRLrc': boolean
/**
* lrc
*/
'download.lrcFormat': 'utf8' | 'gbk'
/**
*
*/
'download.isEmbedPic': boolean
/**
*
*/
'download.isEmbedLyric': boolean
/**
*
*/
'download.isUseOtherSource': boolean
/**
* id
*/
'theme.id': string
/**
* id
*/
'theme.lightId': string
/**
* id
*/
'theme.darkId': string
/**
*
*/
'search.isShowHotSearch': boolean
/**
*
*/
'search.isShowHistorySearch': boolean
/**
*
*/
'search.isFocusSearchBox': boolean
/**
*
*/
'network.proxy.enable': boolean
/**
*
*/
'network.proxy.host': string
/**
*
*/
'network.proxy.port': string
/**
*
*/
'network.proxy.username': string
/**
*
*/
'network.proxy.password': string
/**
*
*/
'tray.enable': boolean
/**
*
*/
// 'tray.isToTray': boolean
/**
* id
*/
'tray.themeId': number
/**
*
*/
'sync.enable': boolean
/**
*
*/
'sync.port': '23332' | string
/**
*
*/
'odc.isAutoClearSearchInput': boolean
/**
*
*/
'odc.isAutoClearSearchList': boolean
}
}

122
src/common/types/common.d.ts vendored Normal file
View File

@ -0,0 +1,122 @@
// import './app_setting'
declare namespace LX {
interface CmdParams {
/**
* -search="突然的自我 - 伍佰"
*/
search?: string
/**
*
*/
dha?: boolean
/**
*
*/
dt?: boolean
/**
*
*/
dhmkh?: boolean
/**
* -proxy-server="127.0.0.1:1081"v1.17.0--
*/
'proxy-server'?: string
/**
* -proxy-bypass-list="<local>;*.google.com;*foo.com;1.2.3.4:5678"-proxy-server使v1.17.0
*/
'proxy-bypass-list'?: string
/**
*
*/
play?: string
[key: string]: boolean | number | string
}
type OnlineSource = 'kw' | 'kg' | 'tx' | 'wy' | 'mg'
type Source = OnlineSource | 'local'
type Quality = '128k' | '320k' | 'flac' | 'flac24bit' | '192k' | 'ape' | 'wav'
type QualityList = Partial<Record<LX.Source, LX.Quality[]>>
interface EnvParams {
deeplink?: string | null
cmdParams: CmdParams
workAreaSize?: Electron.Size
}
interface HotKey {
name: string
action: string
type: keyof typeof keyName
}
interface HotKeyDownInfo {
type: 'local' | 'global'
key: string
}
interface HotKeyConfig {
enable: boolean
keys: {
[key: string]: HotKey
}
}
interface HotKeyConfigAll {
local: HotKeyConfig
global: HotKeyConfig
}
interface RegisterKeyInfo {
key: string
info: HotKey
}
type HotKeyState = Map<string, {
status: boolean
info: HotKey
}>
interface HotKeyActionWrap<T, D> {
action: T
data: D
source?: string
}
type HotKeyActions = HotKeyActionWrap<'config', HotKeyConfigAll>
| HotKeyActionWrap<'enable', boolean>
| HotKeyActionWrap<'register', RegisterKeyInfo>
| HotKeyActionWrap<'unregister', string>
interface HotKeyEvent {
type: string
key: string
}
interface TaskBarButtonFlags {
empty: boolean
collect: boolean
play: boolean
next: boolean
prev: boolean
}
interface Wait {
time: number
id: string
}
type WaitCancel = string
interface Interval {
time: number
id: string
}
type IntervalCancel = string
interface VersionInfo {
version: string
desc: string
}
}

9
src/common/types/config_files.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare namespace LX {
namespace ConfigFile {
interface MyListInfoPart {
type: 'playListPart_v2'
data: LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull
}
}
}

82
src/common/types/desktop_lyric.d.ts vendored Normal file
View File

@ -0,0 +1,82 @@
declare namespace LX {
namespace DesktopLyric {
interface Config {
'desktopLyric.enable': LX.AppSetting['desktopLyric.enable']
'desktopLyric.isLock': LX.AppSetting['desktopLyric.isLock']
'desktopLyric.isAlwaysOnTop': LX.AppSetting['desktopLyric.isAlwaysOnTop']
'desktopLyric.isAlwaysOnTopLoop': LX.AppSetting['desktopLyric.isAlwaysOnTopLoop']
'desktopLyric.audioVisualization': LX.AppSetting['desktopLyric.audioVisualization']
'desktopLyric.width': LX.AppSetting['desktopLyric.width']
'desktopLyric.height': LX.AppSetting['desktopLyric.height']
'desktopLyric.x': LX.AppSetting['desktopLyric.x']
'desktopLyric.y': LX.AppSetting['desktopLyric.y']
'desktopLyric.isLockScreen': LX.AppSetting['desktopLyric.isLockScreen']
'desktopLyric.isDelayScroll': LX.AppSetting['desktopLyric.isDelayScroll']
'desktopLyric.isHoverHide': LX.AppSetting['desktopLyric.isHoverHide']
'desktopLyric.direction': LX.AppSetting['desktopLyric.direction']
'desktopLyric.style.align': LX.AppSetting['desktopLyric.style.align']
'desktopLyric.style.font': LX.AppSetting['desktopLyric.style.font']
'desktopLyric.style.fontSize': LX.AppSetting['desktopLyric.style.fontSize']
'desktopLyric.style.lyricUnplayColor': LX.AppSetting['desktopLyric.style.lyricUnplayColor']
'desktopLyric.style.lyricPlayedColor': LX.AppSetting['desktopLyric.style.lyricPlayedColor']
'desktopLyric.style.lyricShadowColor': LX.AppSetting['desktopLyric.style.lyricShadowColor']
// 'desktopLyric.style.fontWeight': LX.AppSetting['desktopLyric.style.fontWeight']
'desktopLyric.style.opacity': LX.AppSetting['desktopLyric.style.opacity']
'desktopLyric.style.ellipsis': LX.AppSetting['desktopLyric.style.ellipsis']
'desktopLyric.style.isZoomActiveLrc': LX.AppSetting['desktopLyric.style.isZoomActiveLrc']
'common.langId': LX.AppSetting['common.langId']
'player.isShowLyricTranslation': LX.AppSetting['player.isShowLyricTranslation']
'player.isShowLyricRoma': LX.AppSetting['player.isShowLyricRoma']
'player.isPlayLxlrc': LX.AppSetting['player.isPlayLxlrc']
}
type WinMainActions = 'get_info' | 'get_status' | 'get_analyser_data_array'
interface LyricActionBase <A> {
action: A
}
interface LyricActionData<A, D> extends LyricActionBase<A> {
data: D
}
type LyricAction<A, D = undefined> = D extends undefined ? LyricActionBase<A> : LyricActionData<A, D>
type LyricActions = LyricAction<'set_info', {
id: string | null
singer: string
name: string
album: string
lrc: string | null
tlrc: string | null
rlrc: string | null
lxlrc: string | null
// pic: string | null
isPlay: boolean
line: number
played_time: number
}>
| LyricAction<'set_status', {
isPlay: boolean
line: number
played_time: number
}>
| LyricAction<'set_lyric', {
lrc: string | null
tlrc: string | null
rlrc: string | null
lxlrc: string | null
}>
| LyricAction<'set_offset', number>
| LyricAction<'set_play', number>
| LyricAction<'set_pause'>
| LyricAction<'set_stop'>
| LyricAction<'send_analyser_data_array', Uint8Array>
interface NewBounds {
x?: number | null
y?: number
w: number
h: number
}
}
}

66
src/common/types/download_list.d.ts vendored Normal file
View File

@ -0,0 +1,66 @@
// interface DownloadList {
// }
declare namespace LX {
namespace Download {
type DownloadTaskStatus = 'run'
| 'waiting'
| 'pause'
| 'error'
| 'completed'
type FileExt = 'mp3' | 'flac' | 'wav' | 'ape'
interface ProgressInfo {
progress: number
speed: string
downloaded: number
total: number
}
interface DownloadTaskActionBase <A> {
action: A
}
interface DownloadTaskActionData<A, D> extends DownloadTaskActionBase<A> {
data: D
}
type DownloadTaskAction<A, D = undefined> = D extends undefined ? DownloadTaskActionBase<A> : DownloadTaskActionData<A, D>
type DownloadTaskActions = DownloadTaskAction<'start'>
| DownloadTaskAction<'complete'>
| DownloadTaskAction<'refreshUrl'>
| DownloadTaskAction<'statusText', string>
| DownloadTaskAction<'progress', ProgressInfo>
| DownloadTaskAction<'error', {
error?: string
message?: string
}>
interface ListItem {
id: string
isComplate: boolean
status: DownloadTaskStatus
statusText: string
downloaded: number
total: number
progress: number
speed: string
metadata: {
musicInfo: LX.Music.MusicInfoOnline
url: string | null
quality: LX.Quality
ext: FileExt
fileName: string
filePath: string
}
}
interface saveDownloadMusicInfo {
list: ListItem[]
addMusicLocationType: LX.AddMusicLocationType
}
}
}

24
src/common/types/ipc_main.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
declare namespace LX {
interface IpcMainEvent {
event: Electron.IpcMainEvent
}
interface IpcMainEventParams<T> {
event: Electron.IpcMainEvent
params: T
}
type IpcMainEventListener = (params: LX.IpcMainEvent) => void
type IpcMainEventListenerParams<T> = (params: LX.IpcMainEventParams<T>) => void
interface IpcMainInvokeEvent {
event: Electron.IpcMainInvokeEvent
}
interface IpcMainInvokeEventParams<T> {
event: Electron.IpcMainInvokeEvent
params: T
}
type IpcMainInvokeEventListener = (params: LX.IpcMainInvokeEvent) => Promise<void>
type IpcMainInvokeEventListenerParams<T> = (params: LX.IpcMainInvokeEventParams<T>) => Promise<void>
type IpcMainInvokeEventListenerValue<V> = (params: LX.IpcMainInvokeEvent) => Promise<V>
type IpcMainInvokeEventListenerParamsValue<T, V> = (params: LX.IpcMainInvokeEventParams<T>) => Promise<V>
}

11
src/common/types/ipc_renderer.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
declare namespace LX {
interface IpcRendererEvent {
event: Electron.IpcRendererEvent
}
interface IpcRendererEventParams<T> {
event: Electron.IpcRendererEvent
params: T
}
type IpcRendererEventListener = (params: LX.IpcRendererEvent) => any
type IpcRendererEventListenerParams<T> = (params: LX.IpcRendererEventParams<T>) => any
}

142
src/common/types/list.d.ts vendored Normal file
View File

@ -0,0 +1,142 @@
declare namespace LX {
namespace List {
interface UserListInfo {
id: string
name: string
// list: LX.Music.MusicInfo[]
source?: LX.Source
sourceListId?: string
// position?: number
locationUpdateTime: number | null
}
interface MyDefaultListInfo {
id: 'default'
name: '试听列表'
// list: LX.Music.MusicInfo[]
}
interface MyLoveListInfo {
id: 'love'
name: '我的收藏'
// list: LX.Music.MusicInfo[]
}
interface MyTempListInfo {
id: 'temp'
name: '临时列表'
// list: LX.Music.MusicInfo[]
// TODO: save default lists info
meta: {
id?: string
}
}
type MyListInfo = MyDefaultListInfo | MyLoveListInfo | UserListInfo
interface MyAllList {
defaultList: MyDefaultListInfo
loveList: MyLoveListInfo
userList: UserListInfo[]
tempList: MyTempListInfo
}
type SearchHistoryList = string[]
type ListPositionInfo = Record<string, number>
type ListUpdateInfo = Record<string, {
updateTime: number
isAutoUpdate: boolean
}>
type ListSaveType = 'myList' | 'downloadList'
type ListSaveInfo = {
type: 'myList'
data: Partial<MyAllList>
} | {
type: 'downloadList'
data: LX.Download.ListItem[]
}
type ListActionDataOverwrite = MakeOptional<LX.List.ListDataFull, 'tempList'>
interface ListActionAdd {
position: number
listInfos: UserListInfo[]
}
type ListActionRemove = string[]
type ListActionUpdate = UserListInfo[]
interface ListActionUpdatePosition {
/**
* id
*/
ids: string[]
/**
*
*/
position: number
}
interface ListActionMusicAdd {
id: string
musicInfos: LX.Music.MusicInfo[]
addMusicLocationType: LX.AddMusicLocationType
}
interface ListActionMusicMove {
fromId: string
toId: string
musicInfos: LX.Music.MusicInfo[]
addMusicLocationType: LX.AddMusicLocationType
}
interface ListActionCheckMusicExistList {
listId: string
musicInfoId: string
}
interface ListActionMusicRemove {
listId: string
ids: string[]
}
type ListActionMusicUpdate = Array<{
id: string
musicInfo: LX.Music.MusicInfo
}>
interface ListActionMusicUpdatePosition {
listId: string
position: number
ids: string[]
}
interface ListActionMusicOverwrite {
listId: string
musicInfos: LX.Music.MusicInfo[]
}
type ListActionMusicClear = string
interface MyDefaultListInfoFull extends MyDefaultListInfo {
list: LX.Music.MusicInfo[]
}
interface MyLoveListInfoFull extends MyLoveListInfo {
list: LX.Music.MusicInfo[]
}
interface UserListInfoFull extends UserListInfo {
list: LX.Music.MusicInfo[]
}
interface MyTempListInfoFull extends MyTempListInfo {
list: LX.Music.MusicInfo[]
}
interface ListDataFull {
defaultList: LX.Music.MusicInfo[]
loveList: LX.Music.MusicInfo[]
userList: UserListInfoFull[]
tempList: LX.Music.MusicInfo[]
}
}
}

121
src/common/types/music.d.ts vendored Normal file
View File

@ -0,0 +1,121 @@
declare namespace LX {
namespace Music {
interface MusicQualityType { // {"type": "128k", size: "3.56M"}
type: LX.Quality
size: string | null
}
interface MusicQualityTypeKg { // {"type": "128k", size: "3.56M"}
type: LX.Quality
size: string | null
hash: string
}
type _MusicQualityType = Record<Quality, {
size: string | null
}>
type _MusicQualityTypeKg = Record<Quality, {
size: string | null
hash: string
}>
interface MusicInfoMetaBase {
songId: string | number // 歌曲IDmg源为copyrightIdlocal为文件路径
albumName: string // 歌曲专辑名称
picUrl?: string | null // 歌曲图片链接
}
interface MusicInfoMeta_online extends MusicInfoMetaBase {
qualitys: MusicQualityType[]
_qualitys: _MusicQualityType
albumId?: string | number // 歌曲专辑ID
}
interface MusicInfoMeta_local extends MusicInfoMetaBase {
filePath: string
ext: string
}
interface MusicInfoBase<S = LX.Source> {
id: string
name: string // 歌曲名
singer: string // 艺术家名
source: S // 源
interval: string | null // 格式化后的歌曲时长03:55
meta: MusicInfoMetaBase
}
interface MusicInfoLocal extends MusicInfoBase<'local'> {
meta: MusicInfoMeta_local
}
interface MusicInfo_online_common extends MusicInfoBase<'kw' | 'wy'> {
meta: MusicInfoMeta_online
}
interface MusicInfoMeta_kg extends MusicInfoMeta_online {
qualitys: MusicQualityTypeKg[]
_qualitys: _MusicQualityTypeKg
hash: string // 歌曲hash
}
interface MusicInfo_kg extends MusicInfoBase<'kg'> {
meta: MusicInfoMeta_kg
}
interface MusicInfoMeta_tx extends MusicInfoMeta_online {
strMediaMid: string // 歌曲strMediaMid
albumMid?: string // 歌曲albumMid
}
interface MusicInfo_tx extends MusicInfoBase<'tx'> {
meta: MusicInfoMeta_tx
}
interface MusicInfoMeta_mg extends MusicInfoMeta_online {
copyrightId: string // 歌曲copyrightId
lrcUrl?: string // 歌曲lrcUrl
mrcUrl?: string // 歌曲mrcUrl
trcUrl?: string // 歌曲trcUrl
}
interface MusicInfo_mg extends MusicInfoBase<'mg'> {
meta: MusicInfoMeta_mg
}
type MusicInfoOnline = MusicInfo_online_common | MusicInfo_kg | MusicInfo_tx | MusicInfo_mg
type MusicInfo = MusicInfoOnline | MusicInfoLocal
interface LyricInfo {
// 歌曲歌词
lyric: string
// 翻译歌词
tlyric?: string | null
// 罗马音歌词
rlyric?: string | null
// 逐字歌词
lxlyric?: string | null
}
interface LyricInfoSave {
id: string
lyrics: LyricInfo
}
interface MusicFileMeta {
title: string
artist: string | null
album: string | null
APIC: string | null
lyrics: string | null
}
interface MusicUrlInfo {
id: string
url: string
}
interface MusicInfoOtherSourceSave {
id: string
list: MusicInfoOnline[]
}
}
}

11
src/common/types/music_metadata.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import {
IAudioMetadata as iAudioMetadata,
} from 'music-metadata'
declare global {
namespace LX {
namespace MusicMetadataModule {
type IAudioMetadata = iAudioMetadata
}
}
}

19
src/common/types/player.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
declare namespace LX {
namespace Player {
interface ProgressBarOptions {
progress: number
mode?: Electron.ProgressBarOptions['mode']
}
type StatusButtonActions = 'unCollect'
| 'collect'
| 'prev'
| 'pause'
| 'play'
| 'next'
interface LyricInfo extends LX.Music.LyricInfo {
rawlrcInfo: LX.Music.LyricInfo
}
}
}

10
src/common/types/shims_vue.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
// declare module '*.vue' {
// import { App } from 'vue'
// export default App.Component
// }
declare module '*.vue' {
import { Component } from 'vue'
const component: Component
export default component
}

73
src/common/types/sync.d.ts vendored Normal file
View File

@ -0,0 +1,73 @@
declare namespace LX {
namespace Sync {
interface Enable {
enable: boolean
port: string
}
interface SyncActionBase <A> {
action: A
}
interface SyncActionData<A, D> extends SyncActionBase<A> {
data: D
}
type SyncAction<A, D = undefined> = D extends undefined ? SyncActionBase<A> : SyncActionData<A, D>
type SyncMainWindowActions = SyncAction<'select_mode', KeyInfo>
| SyncAction<'close_select_mode'>
| SyncAction<'status', Status>
type SyncServiceActions = SyncAction<'select_mode', Mode>
| SyncAction<'get_status'>
| SyncAction<'generate_code'>
| SyncAction<'enable', Enable>
type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite>
| SyncAction<'list_create', LX.List.ListActionAdd>
| SyncAction<'list_remove', LX.List.ListActionRemove>
| SyncAction<'list_update', LX.List.ListActionUpdate>
| SyncAction<'list_update_position', LX.List.ListActionUpdatePosition>
| SyncAction<'list_music_add', LX.List.ListActionMusicAdd>
| SyncAction<'list_music_move', LX.List.ListActionMusicMove>
| SyncAction<'list_music_remove', LX.List.ListActionMusicRemove>
| SyncAction<'list_music_update', LX.List.ListActionMusicUpdate>
| SyncAction<'list_music_update_position', LX.List.ListActionMusicUpdatePosition>
| SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite>
| SyncAction<'list_music_clear', LX.List.ListActionMusicClear>
interface List {
action: string
data: any
}
interface Status {
status: boolean
message: string
address: string[]
code: string
devices: KeyInfo[]
}
interface KeyInfo {
clientId: string
key: string
iv: string
deviceName: string
connectionTime?: number
}
type ListData = Omit<LX.List.ListDataFull, 'tempList'>
type Mode = 'merge_local_remote'
| 'merge_remote_local'
| 'overwrite_local_remote'
| 'overwrite_remote_local'
| 'overwrite_local_remote_full'
| 'overwrite_remote_local_full'
| 'none'
| 'cancel'
}
}

293
src/common/types/theme.d.ts vendored Normal file
View File

@ -0,0 +1,293 @@
declare namespace LX {
interface ThemeColors {
// '--color-000': string
// '--color-050': string
// '--color-100': string
// '--color-200': string
// '--color-300': string
// '--color-400': string
// '--color-500': string
// '--color-600': string
// '--color-700': string
// '--color-800': string
// '--color-900': string
'--color-theme': string
'--color-primary': string
'--color-primary-alpha-100': string
'--color-primary-alpha-200': string
'--color-primary-alpha-300': string
'--color-primary-alpha-400': string
'--color-primary-alpha-500': string
'--color-primary-alpha-600': string
'--color-primary-alpha-700': string
'--color-primary-alpha-800': string
'--color-primary-alpha-900': string
'--color-primary-dark-100': string
'--color-primary-dark-100-alpha-100': string
'--color-primary-dark-100-alpha-200': string
'--color-primary-dark-100-alpha-300': string
'--color-primary-dark-100-alpha-400': string
'--color-primary-dark-100-alpha-500': string
'--color-primary-dark-100-alpha-600': string
'--color-primary-dark-100-alpha-700': string
'--color-primary-dark-100-alpha-800': string
'--color-primary-dark-100-alpha-900': string
'--color-primary-dark-200': string
'--color-primary-dark-200-alpha-100': string
'--color-primary-dark-200-alpha-200': string
'--color-primary-dark-200-alpha-300': string
'--color-primary-dark-200-alpha-400': string
'--color-primary-dark-200-alpha-500': string
'--color-primary-dark-200-alpha-600': string
'--color-primary-dark-200-alpha-700': string
'--color-primary-dark-200-alpha-800': string
'--color-primary-dark-200-alpha-900': string
'--color-primary-dark-300': string
'--color-primary-dark-300-alpha-100': string
'--color-primary-dark-300-alpha-200': string
'--color-primary-dark-300-alpha-300': string
'--color-primary-dark-300-alpha-400': string
'--color-primary-dark-300-alpha-500': string
'--color-primary-dark-300-alpha-600': string
'--color-primary-dark-300-alpha-700': string
'--color-primary-dark-300-alpha-800': string
'--color-primary-dark-300-alpha-900': string
'--color-primary-dark-400': string
'--color-primary-dark-400-alpha-100': string
'--color-primary-dark-400-alpha-200': string
'--color-primary-dark-400-alpha-300': string
'--color-primary-dark-400-alpha-400': string
'--color-primary-dark-400-alpha-500': string
'--color-primary-dark-400-alpha-600': string
'--color-primary-dark-400-alpha-700': string
'--color-primary-dark-400-alpha-800': string
'--color-primary-dark-400-alpha-900': string
'--color-primary-dark-500': string
'--color-primary-dark-500-alpha-100': string
'--color-primary-dark-500-alpha-200': string
'--color-primary-dark-500-alpha-300': string
'--color-primary-dark-500-alpha-400': string
'--color-primary-dark-500-alpha-500': string
'--color-primary-dark-500-alpha-600': string
'--color-primary-dark-500-alpha-700': string
'--color-primary-dark-500-alpha-800': string
'--color-primary-dark-500-alpha-900': string
'--color-primary-dark-600': string
'--color-primary-dark-600-alpha-100': string
'--color-primary-dark-600-alpha-200': string
'--color-primary-dark-600-alpha-300': string
'--color-primary-dark-600-alpha-400': string
'--color-primary-dark-600-alpha-500': string
'--color-primary-dark-600-alpha-600': string
'--color-primary-dark-600-alpha-700': string
'--color-primary-dark-600-alpha-800': string
'--color-primary-dark-600-alpha-900': string
'--color-primary-dark-700': string
'--color-primary-dark-700-alpha-100': string
'--color-primary-dark-700-alpha-200': string
'--color-primary-dark-700-alpha-300': string
'--color-primary-dark-700-alpha-400': string
'--color-primary-dark-700-alpha-500': string
'--color-primary-dark-700-alpha-600': string
'--color-primary-dark-700-alpha-700': string
'--color-primary-dark-700-alpha-800': string
'--color-primary-dark-700-alpha-900': string
'--color-primary-dark-800': string
'--color-primary-dark-800-alpha-100': string
'--color-primary-dark-800-alpha-200': string
'--color-primary-dark-800-alpha-300': string
'--color-primary-dark-800-alpha-400': string
'--color-primary-dark-800-alpha-500': string
'--color-primary-dark-800-alpha-600': string
'--color-primary-dark-800-alpha-700': string
'--color-primary-dark-800-alpha-800': string
'--color-primary-dark-800-alpha-900': string
'--color-primary-dark-900': string
'--color-primary-dark-900-alpha-100': string
'--color-primary-dark-900-alpha-200': string
'--color-primary-dark-900-alpha-300': string
'--color-primary-dark-900-alpha-400': string
'--color-primary-dark-900-alpha-500': string
'--color-primary-dark-900-alpha-600': string
'--color-primary-dark-900-alpha-700': string
'--color-primary-dark-900-alpha-800': string
'--color-primary-dark-900-alpha-900': string
'--color-primary-dark-1000': string
'--color-primary-dark-1000-alpha-100': string
'--color-primary-dark-1000-alpha-200': string
'--color-primary-dark-1000-alpha-300': string
'--color-primary-dark-1000-alpha-400': string
'--color-primary-dark-1000-alpha-500': string
'--color-primary-dark-1000-alpha-600': string
'--color-primary-dark-1000-alpha-700': string
'--color-primary-dark-1000-alpha-800': string
'--color-primary-dark-1000-alpha-900': string
'--color-primary-light-100': string
'--color-primary-light-100-alpha-100': string
'--color-primary-light-100-alpha-200': string
'--color-primary-light-100-alpha-300': string
'--color-primary-light-100-alpha-400': string
'--color-primary-light-100-alpha-500': string
'--color-primary-light-100-alpha-600': string
'--color-primary-light-100-alpha-700': string
'--color-primary-light-100-alpha-800': string
'--color-primary-light-100-alpha-900': string
'--color-primary-light-200': string
'--color-primary-light-200-alpha-100': string
'--color-primary-light-200-alpha-200': string
'--color-primary-light-200-alpha-300': string
'--color-primary-light-200-alpha-400': string
'--color-primary-light-200-alpha-500': string
'--color-primary-light-200-alpha-600': string
'--color-primary-light-200-alpha-700': string
'--color-primary-light-200-alpha-800': string
'--color-primary-light-200-alpha-900': string
'--color-primary-light-300': string
'--color-primary-light-300-alpha-100': string
'--color-primary-light-300-alpha-200': string
'--color-primary-light-300-alpha-300': string
'--color-primary-light-300-alpha-400': string
'--color-primary-light-300-alpha-500': string
'--color-primary-light-300-alpha-600': string
'--color-primary-light-300-alpha-700': string
'--color-primary-light-300-alpha-800': string
'--color-primary-light-300-alpha-900': string
'--color-primary-light-400': string
'--color-primary-light-400-alpha-100': string
'--color-primary-light-400-alpha-200': string
'--color-primary-light-400-alpha-300': string
'--color-primary-light-400-alpha-400': string
'--color-primary-light-400-alpha-500': string
'--color-primary-light-400-alpha-600': string
'--color-primary-light-400-alpha-700': string
'--color-primary-light-400-alpha-800': string
'--color-primary-light-400-alpha-900': string
'--color-primary-light-500': string
'--color-primary-light-500-alpha-100': string
'--color-primary-light-500-alpha-200': string
'--color-primary-light-500-alpha-300': string
'--color-primary-light-500-alpha-400': string
'--color-primary-light-500-alpha-500': string
'--color-primary-light-500-alpha-600': string
'--color-primary-light-500-alpha-700': string
'--color-primary-light-500-alpha-800': string
'--color-primary-light-500-alpha-900': string
'--color-primary-light-600': string
'--color-primary-light-600-alpha-100': string
'--color-primary-light-600-alpha-200': string
'--color-primary-light-600-alpha-300': string
'--color-primary-light-600-alpha-400': string
'--color-primary-light-600-alpha-500': string
'--color-primary-light-600-alpha-600': string
'--color-primary-light-600-alpha-700': string
'--color-primary-light-600-alpha-800': string
'--color-primary-light-600-alpha-900': string
'--color-primary-light-700': string
'--color-primary-light-700-alpha-100': string
'--color-primary-light-700-alpha-200': string
'--color-primary-light-700-alpha-300': string
'--color-primary-light-700-alpha-400': string
'--color-primary-light-700-alpha-500': string
'--color-primary-light-700-alpha-600': string
'--color-primary-light-700-alpha-700': string
'--color-primary-light-700-alpha-800': string
'--color-primary-light-700-alpha-900': string
'--color-primary-light-800': string
'--color-primary-light-800-alpha-100': string
'--color-primary-light-800-alpha-200': string
'--color-primary-light-800-alpha-300': string
'--color-primary-light-800-alpha-400': string
'--color-primary-light-800-alpha-500': string
'--color-primary-light-800-alpha-600': string
'--color-primary-light-800-alpha-700': string
'--color-primary-light-800-alpha-800': string
'--color-primary-light-800-alpha-900': string
'--color-primary-light-900': string
'--color-primary-light-900-alpha-100': string
'--color-primary-light-900-alpha-200': string
'--color-primary-light-900-alpha-300': string
'--color-primary-light-900-alpha-400': string
'--color-primary-light-900-alpha-500': string
'--color-primary-light-900-alpha-600': string
'--color-primary-light-900-alpha-700': string
'--color-primary-light-900-alpha-800': string
'--color-primary-light-900-alpha-900': string
'--color-primary-light-1000': string
'--color-primary-light-1000-alpha-100': string
'--color-primary-light-1000-alpha-200': string
'--color-primary-light-1000-alpha-300': string
'--color-primary-light-1000-alpha-400': string
'--color-primary-light-1000-alpha-500': string
'--color-primary-light-1000-alpha-600': string
'--color-primary-light-1000-alpha-700': string
'--color-primary-light-1000-alpha-800': string
'--color-primary-light-1000-alpha-900': string
}
interface Theme {
id: string
name: string
isDark: boolean
isCustom: boolean
config: {
themeColors: ThemeColors
extInfo: {
'--color-app-background': string
'--color-main-background': string
'--color-nav-font': string
'--background-image': string
'--background-image-position': string
'--background-image-size': string
// 关闭按钮颜色
'--color-btn-hide': string
'--color-btn-min': string
'--color-btn-close': string
// 徽章颜色
'--color-badge-primary': string
'--color-badge-secondary': string
'--color-badge-tertiary': string
}
}
}
interface ThemeInfo {
themes: LX.Theme[]
userThemes: LX.Theme[]
dataPath: string
}
interface ThemeSetting {
shouldUseDarkColors: boolean
theme: {
id: string
name: string
isDark: boolean
colors: Record<string, string>
}
}
}

56
src/common/types/user_api.d.ts vendored Normal file
View File

@ -0,0 +1,56 @@
declare namespace LX {
namespace UserApi {
type UserApiSourceInfoType = 'music'
type UserApiSourceInfoActions = 'musicUrl'
interface UserApiSourceInfo {
name: string
type: UserApiSourceInfoType
actions: UserApiSourceInfoActions[]
qualitys: LX.Quality[]
}
type UserApiSources = Record<LX.Source, UserApiSourceInfo>
interface UserApiInfo {
id: string
name: string
description: string
script: string
allowShowUpdateAlert: boolean
sources?: UserApiSources
}
interface UserApiStatus {
status: boolean
message?: string
apiInfo?: UserApiInfo
}
interface UserApiUpdateInfo {
name: string
description: string
log: string
updateUrl?: string
}
interface UserApiRequestParams {
requestKey: string
data: any
}
type UserApiRequestCancelParams = string
type UserApiSetApiParams = string
interface UserApiSetAllowUpdateAlertParams {
id: string
enable: boolean
}
interface ImportUserApi {
apiInfo: UserApiInfo
apiList: UserApiInfo[]
}
}
}

7
src/common/types/utils.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
type MakeOptional<Type, Key extends keyof Type> = Omit<Type, Key> & Partial<Pick<Type, Key>>
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
}
type Modify<T, R> = Omit<T, keyof R> & R

View File

@ -1,222 +0,0 @@
const log = require('electron-log')
const { defaultSetting, overwriteSetting } = require('./defaultSetting')
// const apiSource = require('../renderer/utils/music/api-source-info')
const getStore = require('./store')
const defaultHotKey = require('./defaultHotKey')
exports.isLinux = process.platform == 'linux'
exports.isWin = process.platform == 'win32'
exports.isMac = process.platform == 'darwin'
/**
* 生成节流函数
* @param {*} fn
* @param {*} delay
*/
exports.throttle = (fn, delay = 100) => {
let timer = null
let _args = null
return function(...args) {
_args = args
if (timer) return
timer = setTimeout(() => {
timer = null
fn.apply(this, _args)
}, delay)
}
}
/**
* 生成防抖函数
* @param {*} fn
* @param {*} delay
*/
exports.debounce = (fn, delay = 100) => {
let timer = null
let _args = null
return function(...args) {
_args = args
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
timer = null
fn.apply(this, _args)
}, delay)
}
}
exports.log = log
// https://stackoverflow.com/a/53387532
exports.compareVer = (currentVer, targetVer) => {
// treat non-numerical characters as lower version
// replacing them with a negative number based on charcode of each character
const fix = s => `.${s.toLowerCase().charCodeAt(0) - 2147483647}.`
currentVer = ('' + currentVer).replace(/[^0-9.]/g, fix).split('.')
targetVer = ('' + targetVer).replace(/[^0-9.]/g, fix).split('.')
let c = Math.max(currentVer.length, targetVer.length)
for (let i = 0; i < c; i++) {
// convert to integer the most efficient way
currentVer[i] = ~~currentVer[i]
targetVer[i] = ~~targetVer[i]
if (currentVer[i] > targetVer[i]) return 1
else if (currentVer[i] < targetVer[i]) return -1
}
return 0
}
exports.isObject = item => item && typeof item === 'object' && !Array.isArray(item)
/**
* 对象深度合并
* @param {} target 要合并源对象
* @param {} source 要合并目标对象
*/
exports.objectDeepMerge = (target, source, mergedObj) => {
if (!mergedObj) {
mergedObj = new Set()
mergedObj.add(target)
}
let base = {}
Object.keys(source).forEach(item => {
if (exports.isObject(source[item])) {
if (mergedObj.has(source[item])) return
if (!exports.isObject(target[item])) target[item] = {}
mergedObj.add(source[item])
exports.objectDeepMerge(target[item], source[item], mergedObj)
return
}
base[item] = source[item]
})
Object.assign(target, base)
}
exports.mergeSetting = (setting, version) => {
let defaultSettingCopy = JSON.parse(JSON.stringify(defaultSetting))
let overwriteSettingCopy = JSON.parse(JSON.stringify(overwriteSetting))
const defaultVersion = defaultSettingCopy.version
if (!version) {
if (setting) {
version = setting.version
delete setting.version
}
}
if (!setting) {
setting = defaultSettingCopy
} else if (exports.compareVer(version, defaultVersion) < 0) {
exports.objectDeepMerge(defaultSettingCopy, setting)
exports.objectDeepMerge(defaultSettingCopy, overwriteSettingCopy)
setting = defaultSettingCopy
}
// if (!apiSource.some(api => api.id === setting.apiSource && !api.disabled)) {
// let api = apiSource.find(api => !api.disabled)
// if (api) setting.apiSource = api.id
// }
return { setting, version: defaultVersion }
}
/**
* 初始化设置
* @param {*} setting
* @param {*} isShowErrorAlert
*/
exports.initSetting = isShowErrorAlert => {
const electronStore_list = getStore('playList', true, isShowErrorAlert)
const electronStore_config = getStore('config')
const electronStore_downloadList = getStore('downloadList')
let setting = electronStore_config.get('setting')
if (setting) {
let version = electronStore_config.get('version')
if (!version) { // 迁移配置
version = electronStore_config.get('setting.version')
electronStore_config.set('version', version)
electronStore_config.delete('setting.version')
const list = electronStore_config.get('list')
if (list) {
if (list.defaultList) electronStore_list.set('defaultList', list.defaultList)
if (list.loveList) electronStore_list.set('loveList', list.loveList)
electronStore_config.delete('list')
}
const downloadList = electronStore_config.get('download')
if (downloadList) {
if (downloadList.list) electronStore_downloadList.set('list', downloadList.list)
electronStore_config.delete('download')
}
}
// 迁移列表滚动位置设置 ~0.18.3
if (setting.list.scroll) {
let scroll = setting.list.scroll
electronStore_config.delete('setting.list.scroll')
electronStore_config.set('setting.list.isSaveScrollLocation', scroll.enable)
delete setting.list.scroll
}
}
// 从我的列表分离下载列表 v1.7.0 后
let downloadList = electronStore_list.get('downloadList')
if (downloadList) {
electronStore_downloadList.set('list', downloadList)
electronStore_list.delete('downloadList')
}
const { version: settingVersion, setting: newSetting } = exports.mergeSetting(setting, electronStore_config.get('version'))
// 修正拼写问题 v1.8.2 及以前
if (newSetting.player.isShowLyricTransition != null) {
newSetting.player.isShowLyricTranslation = newSetting.player.isShowLyricTransition
delete newSetting.player.isShowLyricTransition
}
// 迁移v1.19.0之前的主题设置
if (newSetting.themeId != null) {
newSetting.theme.id = newSetting.themeId
delete newSetting.themeId
}
// 重置 ^0.18.2 排行榜ID
if (!newSetting.leaderboard.tabId.includes('__')) newSetting.leaderboard.tabId = 'kw__16'
// newSetting.controlBtnPosition = 'right'
electronStore_config.set({ version: settingVersion, setting: newSetting })
return { version: settingVersion, setting: newSetting }
}
/**
* 初始化快捷键设置
*/
exports.initHotKey = () => {
const electronStore_hotKey = getStore('hotKey')
let localConfig = electronStore_hotKey.get('local')
if (!localConfig) {
localConfig = defaultHotKey.local
electronStore_hotKey.set('local', localConfig)
}
let globalConfig = electronStore_hotKey.get('global')
// 移除v1.0.1及之前设置的全局声音媒体快捷键接管
if (globalConfig && globalConfig.keys.VolumeUp) {
delete globalConfig.keys.VolumeUp
delete globalConfig.keys.VolumeDown
delete globalConfig.keys.VolumeMute
electronStore_hotKey.set('global', globalConfig)
}
if (!globalConfig) {
globalConfig = defaultHotKey.global
electronStore_hotKey.set('global', globalConfig)
}
return {
global: globalConfig,
local: localConfig,
}
}

175
src/common/utils/common.ts Normal file
View File

@ -0,0 +1,175 @@
// 非业务工具方法
/**
* minmax
* @param {*} min
* @param {*} max
*/
export const getRandom = (min: number, max: number): number => Math.floor(Math.random() * (max - min)) + min
export const sizeFormate = (size: number): string => {
// https://gist.github.com/thomseddon/3511330
if (!size) return '0 B'
let units = ['B', 'KB', 'MB', 'GB', 'TB']
let number = Math.floor(Math.log(size) / Math.log(1024))
return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}`
}
const numFix = (n: number): string => n < 10 ? (`0${n}`) : n.toString()
/**
*
* @param {*} date
* @param {String} format YYYY-MM-DD hh:mm:ss
*/
export const dateFormat = (date: string | number | Date, format = 'YYYY-MM-DD hh:mm:ss') => {
if (typeof date != 'object') date = new Date(date)
return format
.replace('YYYY', date.getFullYear().toString())
.replace('MM', numFix(date.getMonth() + 1).toString())
.replace('DD', numFix(date.getDate()))
.replace('hh', numFix(date.getHours()))
.replace('mm', numFix(date.getMinutes()))
.replace('ss', numFix(date.getSeconds()))
}
export const formatPlayTime = (time: number) => {
let m = Math.trunc(time / 60)
let s = Math.trunc(time % 60)
return m == 0 && s == 0 ? '--/--' : numFix(m) + ':' + numFix(s)
}
export const formatPlayTime2 = (time: number) => {
let m = Math.trunc(time / 60)
let s = Math.trunc(time % 60)
return numFix(m) + ':' + numFix(s)
}
const encodeNames = {
'&nbsp;': ' ',
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&apos;': "'",
'&#039;': "'",
} as const
export const decodeName = (str: string | null = '') => {
return str?.replace(/(?:&amp;|&lt;|&gt;|&quot;|&apos;|&#039;|&nbsp;)/gm, (s: string) => encodeNames[s as keyof typeof encodeNames]) ?? ''
}
// 解析URL参数为对象
export const parseUrlParams = (str: string): Record<string, string> => {
const params: Record<string, string> = {}
if (typeof str !== 'string') return params
const paramsArr = str.split('&')
for (const param of paramsArr) {
let [key, value] = param.split('=')
params[key] = value
}
return params
}
/**
*
* @param fn
* @param delay
* @returns
*/
export function throttle<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {
let timer: NodeJS.Timeout | null = null
let _args: Args
return (...args: Args) => {
_args = args
if (timer) return
timer = setTimeout(() => {
timer = null
void fn(..._args)
}, delay)
}
}
/**
*
* @param fn
* @param delay
* @returns
*/
export function debounce<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {
let timer: NodeJS.Timeout | null = null
let _args: Args
return (...args: Args) => {
_args = args
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
timer = null
void fn(..._args)
}, delay)
}
}
const fileNameRxp = /[\\/:*?#"<>|]/g
export const filterFileName = (name: string): string => name.replace(fileNameRxp, '')
// https://blog.csdn.net/xcxy2015/article/details/77164126#comments
/**
*
* @param a
* @param b
*/
export const similar = (a: string, b: string) => {
if (!a || !b) return 0
if (a.length > b.length) { // 保证 a <= b
let t = b
b = a
a = t
}
let al = a.length
let bl = b.length
let mp = [] // 一个表
let i, j, ai, lt, tmp // ai字符串a的第i个字符。 lt左上角的值。 tmp暂存新的值。
for (i = 0; i <= bl; i++) mp[i] = i
for (i = 1; i <= al; i++) {
ai = a.charAt(i - 1)
lt = mp[0]
mp[0] = mp[0] + 1
for (j = 1; j <= bl; j++) {
tmp = Math.min(mp[j] + 1, mp[j - 1] + 1, lt + (ai == b.charAt(j - 1) ? 0 : 1))
lt = mp[j]
mp[j] = tmp
}
}
return 1 - (mp[bl] / bl)
}
/**
*
* @param arr
* @param data
*/
export const sortInsert = (arr: Array<{ num: number, data: any }>, data: { num: number, data: any }) => {
let key = data.num
let left = 0
let right = arr.length - 1
while (left <= right) {
let middle = Math.trunc((left + right) / 2)
if (key == arr[middle].num) {
left = middle
break
} else if (key < arr[middle].num) {
right = middle - 1
} else {
left = middle + 1
}
}
while (left > 0) {
if (arr[left - 1].num != key) break
left--
}
arr.splice(left, 0, data)
}

View File

@ -0,0 +1,183 @@
// https://github.com/tholman/cursor-effects/blob/master/src/bubbleCursor.js
class Particle {
constructor(x, y, fillStyle, strokeStyle) {
const lifeSpan = Math.floor(Math.random() * 60 + 60)
this.initialLifeSpan = lifeSpan //
this.lifeSpan = lifeSpan // ms
this.velocity = {
x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 10),
y: -0.4 + Math.random() * -1,
}
this.position = { x, y }
this.fillStyle = fillStyle
this.strokeStyle = strokeStyle
this.baseDimension = 4
}
update(context, width) {
this.position.x += this.velocity.x
this.position.y += this.velocity.y
this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75
this.velocity.y -= Math.random() / 600
if (this.position.x >= width - 2 || this.position.x <= 2) this.lifeSpan = 0
else if (this.position.y <= 5) this.lifeSpan = 0
this.lifeSpan--
const scale =
0.2 + (this.initialLifeSpan - this.lifeSpan) / this.initialLifeSpan
context.fillStyle = this.fillStyle
context.strokeStyle = this.strokeStyle
context.beginPath()
context.arc(
this.position.x - (this.baseDimension / 2) * scale,
this.position.y - this.baseDimension / 2,
this.baseDimension * scale,
0,
2 * Math.PI,
)
context.stroke()
context.fill()
context.closePath()
}
}
export default class BubbleCursor {
constructor({ element, fillStyle = 'rgba(77, 175, 124, 0.1)', strokeStyle = 'rgba(77, 175, 124, 0.3)' } = {}) {
this.hasWrapperEl = element
this.element = this.hasWrapperEl || document.body
this.width = window.innerWidth
this.height = window.innerHeight
this.cursor = { x: this.width / 2, y: this.width / 2 }
this.particles = []
this.canvas = null
this.context = null
this.fillStyle = fillStyle
this.strokeStyle = strokeStyle
this.init()
}
init() {
this.canvas = document.createElement('canvas')
this.context = this.canvas.getContext('2d')
this.canvas.style.top = '0px'
this.canvas.style.left = '0px'
this.canvas.style.pointerEvents = 'none'
this.canvas.style.zIndex = 100
if (this.hasWrapperEl) {
this.canvas.style.position = 'absolute'
this.element.appendChild(this.canvas)
this.canvas.width = this.element.clientWidth
this.canvas.height = this.element.clientHeight
} else {
this.canvas.style.position = 'fixed'
document.body.appendChild(this.canvas)
this.canvas.width = this.width
this.canvas.height = this.height
}
this.bindEvents()
this.loop()
}
bindEvents() {
this.element.addEventListener('mousemove', this.onMouseMove)
this.element.addEventListener('touchmove', this.onTouchMove, { passive: true })
this.element.addEventListener('touchstart', this.onTouchMove, { passive: true })
window.addEventListener('resize', this.onWindowResize)
}
onWindowResize = (e) => {
this.width = window.innerWidth
this.height = window.innerHeight
if (this.hasWrapperEl) {
this.canvas.width = this.element.clientWidth
this.canvas.height = this.element.clientHeight
} else {
this.canvas.width = this.width
this.canvas.height = this.height
}
}
onTouchMove = (e) => {
if (e.touches.length > 0) {
for (let i = 0; i < e.touches.length; i++) {
this.addParticle(
e.touches[i].clientX,
e.touches[i].clientY,
)
}
}
}
onMouseMove = (e) => {
if (this.hasWrapperEl) {
const boundingRect = this.element.getBoundingClientRect()
this.cursor.x = e.clientX - boundingRect.left
this.cursor.y = e.clientY - boundingRect.top
} else {
this.cursor.x = e.clientX
this.cursor.y = e.clientY
}
this.addParticle(this.cursor.x, this.cursor.y)
}
addParticle(x, y) {
this.particles.push(new Particle(x, y, this.fillStyle, this.strokeStyle))
}
updateParticles() {
this.context.clearRect(0, 0, this.width, this.height)
// Update
for (let i = 0; i < this.particles.length; i++) {
this.particles[i].update(this.context, this.width)
}
// Remove dead particles
for (let i = this.particles.length - 1; i >= 0; i--) {
if (this.particles[i].lifeSpan < 0) {
this.particles.splice(i, 1)
}
}
}
loop = () => {
this.updateParticles()
window.requestAnimationFrame(this.loop)
}
setColor(fillStyle, strokeStyle) {
this.fillStyle = fillStyle
this.strokeStyle = strokeStyle
}
destroy() {
this.element.removeEventListener('mousemove', this.onMouseMove)
this.element.removeEventListener('touchmove', this.onTouchMove, { passive: true })
this.element.removeEventListener('touchstart', this.onTouchMove, { passive: true })
window.removeEventListener('resize', this.onWindowResize)
if (this.hasWrapperEl) {
this.element.removeChild(this.canvas)
} else {
document.body.removeChild(this.canvas)
}
this.canvas = null
this.context = null
}
}

View File

@ -1,70 +1,74 @@
import fs from 'fs'
import path from 'path'
import request from 'request'
import { EventEmitter } from 'events'
import { performance } from 'perf_hooks'
import { STATUS } from './util'
import http from 'http'
import { request, Options as RequestOptions } from './request'
export interface Options {
forceResume: boolean
requestOptions: RequestOptions
}
const defaultChunkInfo = {
path: null,
startByte: 0,
path: '',
startByte: '0',
endByte: '',
}
const defaultRequestOptions = {
method: 'GET',
const defaultRequestOptions: Options['requestOptions'] = {
method: 'get',
headers: {},
}
const defaultOptions = {
const defaultOptions: Options = {
forceResume: true,
requestOptions: { ...defaultRequestOptions },
}
class Task extends EventEmitter {
/**
*
* @param {String} url download url
* @param {Object} chunkInfo
* @param {Object} options
*/
constructor(url, savePath, filename, options = {}) {
resumeLastChunk: Buffer | null
downloadUrl: string
chunkInfo: { path: string, startByte: string, endByte: string }
status: typeof STATUS[keyof typeof STATUS]
options: Options
requestOptions: Options['requestOptions']
ws: fs.WriteStream | null = null
progress = { total: 0, downloaded: 0, speed: 0, progress: 0 }
statsEstimate = { time: 0, bytes: 0, prevBytes: 0 }
requestInstance: http.ClientRequest | null = null
constructor(url: string, savePath: string, filename: string, options: Partial<Options> = {}) {
super()
this.resumeLastChunk = null
this.downloadUrl = url
this.chunkInfo = Object.assign({}, defaultChunkInfo, {
path: path.join(savePath, filename),
startByte: 0,
startByte: '0',
})
if (!this.chunkInfo.endByte) this.chunkInfo.endByte = ''
// if (!this.chunkInfo.endByte) this.chunkInfo.endByte = ''
this.options = Object.assign({}, defaultOptions, options)
this.requestOptions = Object.assign({}, defaultRequestOptions, this.options.requestOptions || {})
if (!this.requestOptions.headers) this.requestOptions.headers = {}
this.requestOptions.headers = this.requestOptions.headers ? { ...this.requestOptions.headers } : {}
this.progress = {
total: 0,
downloaded: 0,
speed: 0,
}
this.statsEstimate = {
time: 0,
bytes: 0,
prevBytes: 0,
}
this.status = STATUS.idle
}
__init() {
this.status = STATUS.init
async __init() {
const { path, startByte, endByte } = this.chunkInfo
this.progress.downloaded = 0
if (startByte != null) this.requestOptions.headers.range = `bytes=${startByte}-${endByte}`
return new Promise((resolve, reject) => {
if (!path) return resolve()
this.progress.progress = 0
this.progress.speed = 0
if (startByte) this.requestOptions.headers!.range = `bytes=${startByte}-${endByte}`
if (!path) return
return new Promise<void>((resolve, reject) => {
fs.stat(path, (errStat, stats) => {
if (errStat) {
// console.log(errStat.code)
if (errStat.code !== 'ENOENT') {
this.__handleError(errStat)
reject(errStat)
@ -94,7 +98,7 @@ class Task extends EventEmitter {
// console.log(buffer)
this.resumeLastChunk = buffer
this.progress.downloaded = stats.size
this.requestOptions.headers.range = `bytes=${stats.size - 10}-${endByte || ''}`
this.requestOptions.headers!.range = `bytes=${stats.size - 10}-${endByte || ''}`
resolve()
})
})
@ -106,15 +110,15 @@ class Task extends EventEmitter {
})
}
__httpFetch(url, options) {
__httpFetch(url: string, options: Options['requestOptions']) {
// console.log(options)
this.request = request(url, options)
this.requestInstance = request(url, options)
.on('response', response => {
if (response.statusCode !== 200 && response.statusCode !== 206) {
if (response.statusCode == 416) {
fs.unlink(this.chunkInfo.path, async err => {
await this.__handleError(new Error(response.statusMessage))
this.chunkInfo.startByte = 0
fs.unlink(this.chunkInfo.path, (err) => {
this.__handleError(new Error(response.statusMessage))
this.chunkInfo.startByte = '0'
this.resumeLastChunk = null
this.progress.downloaded = 0
if (err) this.__handleError(err)
@ -124,13 +128,13 @@ class Task extends EventEmitter {
this.status = STATUS.failed
this.emit('fail', response)
this.__closeRequest()
this.__closeWriteStream()
void this.__closeWriteStream()
return
}
this.emit('response', response)
try {
this.__initDownload(response)
} catch (error) {
} catch (error: any) {
return this.__handleError(error)
}
this.status = STATUS.running
@ -142,34 +146,44 @@ class Task extends EventEmitter {
this.__handleComplete()
} else {
// this.__handleError(new Error('The connection was terminated while the message was still being sent'))
this.stop()
void this.stop()
}
})
})
.on('error', err => this.__handleError(err))
.on('close', () => this.__closeWriteStream())
.on('close', () => {
void this.__closeWriteStream()
})
.end()
}
__initDownload(response) {
this.progress.total = parseInt(response.headers['content-length'] || 0)
let options = {}
let isResumable = this.options.forceResume || response.headers['accept-ranges'] !== 'none' || (typeof response.headers['accept-ranges'] == 'string' && parseInt(response.headers['accept-ranges'].replace(/^bytes=(\d+)/, '$1')) > 0)
__initDownload(response: http.IncomingMessage) {
this.progress.total = response.headers['content-length'] ? parseInt(response.headers['content-length']) : 0
if (!this.progress.total) return this.__handleError(new Error('Content length is 0'))
let options: any = {}
let isResumable = this.options.forceResume ||
response.headers['accept-ranges'] !== 'none' ||
(typeof response.headers['accept-ranges'] == 'string' &&
parseInt(response.headers['accept-ranges'].replace(/^bytes=(\d+)/, '$1')) > 0)
if (isResumable) {
options.flags = 'a'
if (this.progress.downloaded) this.progress.total -= 10
} else {
if (this.chunkInfo.startByte > 0) return this.__handleError(new Error('The resource cannot be resumed download.'))
if (this.chunkInfo.startByte != '0') return this.__handleError(new Error('The resource cannot be resumed download.'))
}
this.progress.total += this.progress.downloaded
this.statsEstimate.prevBytes = this.progress.downloaded
if (!this.chunkInfo.path) return this.__handleError(new Error('Chunk save Path is not set.'))
this.ws = fs.createWriteStream(this.chunkInfo.path, options)
this.ws.on('finish', () => this.__closeWriteStream())
this.ws.on('finish', () => {
void this.__closeWriteStream()
})
this.ws.on('error', err => {
fs.unlink(this.chunkInfo.path, async unlinkErr => {
await this.__handleError(err)
this.chunkInfo.startByte = 0
fs.unlink(this.chunkInfo.path, (unlinkErr: any) => {
this.__handleError(err)
this.chunkInfo.startByte = '0'
this.resumeLastChunk = null
this.progress.downloaded = 0
if (unlinkErr && unlinkErr.code !== 'ENOENT') this.__handleError(unlinkErr)
@ -179,7 +193,7 @@ class Task extends EventEmitter {
__handleComplete() {
if (this.status == STATUS.error) return
this.__closeWriteStream().then(() => {
void this.__closeWriteStream().then(() => {
if (this.progress.downloaded == this.progress.total) {
this.status = STATUS.completed
this.emit('completed')
@ -188,21 +202,22 @@ class Task extends EventEmitter {
this.emit('stop')
}
})
console.log('end')
// console.log('end')
}
__handleError(error) {
__handleError(error: Error) {
if (this.status == STATUS.error) return
this.status = STATUS.error
this.__closeRequest()
this.__closeWriteStream()
void this.__closeWriteStream()
if (error.message == 'aborted') return
this.emit('error', error)
}
__closeWriteStream() {
return new Promise((resolve, reject) => {
async __closeWriteStream() {
return new Promise<void>((resolve, reject) => {
if (!this.ws) return resolve()
console.log('close write stream')
// console.log('close write stream')
this.ws.close(err => {
if (err) {
this.status = STATUS.error
@ -217,18 +232,28 @@ class Task extends EventEmitter {
}
__closeRequest() {
if (!this.request) return
console.log('close request')
this.request.abort()
this.request = null
if (!this.requestInstance || this.requestInstance.destroyed) return
// console.log('close request')
this.requestInstance.destroy()
this.requestInstance = null
}
__handleWriteData(chunk) {
__handleWriteData(chunk: Buffer) {
if (this.resumeLastChunk) {
chunk = this.__handleDiffChunk(chunk)
if (!chunk) {
const result = this.__handleDiffChunk(chunk)
if (result) chunk = result
else {
this.__handleStop().finally(() => {
this.__handleError(new Error('Resume failed, response chunk does not match.'))
// this.__handleError(new Error('Resume failed, response chunk does not match.'))
// Resume failed, response chunk does not match, remove file and restart download
console.log('Resume failed, response chunk does not match.')
fs.unlink(this.chunkInfo.path, (unlinkErr: any) => {
// this.__handleError(err)
this.chunkInfo.startByte = '0'
this.resumeLastChunk = null
if (unlinkErr && unlinkErr.code !== 'ENOENT') return this.__handleError(unlinkErr)
void this.start()
})
})
return
}
@ -240,35 +265,32 @@ class Task extends EventEmitter {
if (!err) return
console.log(err)
this.__handleError(err)
this.stop()
void this.stop()
})
}
__handleDiffChunk(chunk) {
__handleDiffChunk(chunk: Buffer): Buffer | null {
// console.log('diff', chunk)
let resumeLastChunkLen = this.resumeLastChunk.length
let resumeLastChunkLen = this.resumeLastChunk!.length
let chunkLen = chunk.length
let isOk
if (chunkLen >= resumeLastChunkLen) {
isOk = chunk.slice(0, resumeLastChunkLen).toString('hex') === this.resumeLastChunk.toString('hex')
isOk = chunk.slice(0, resumeLastChunkLen).toString('hex') === this.resumeLastChunk!.toString('hex')
if (!isOk) return null
this.resumeLastChunk = null
return chunk.slice(resumeLastChunkLen)
} else {
isOk = chunk.slice(0, chunkLen).toString('hex') === this.resumeLastChunk.slice(0, chunkLen).toString('hex')
isOk = chunk.slice(0, chunkLen).toString('hex') === this.resumeLastChunk!.slice(0, chunkLen).toString('hex')
if (!isOk) return null
this.resumeLastChunk = this.resumeLastChunk.slice(chunkLen)
this.resumeLastChunk = this.resumeLastChunk!.slice(chunkLen)
return chunk.slice(chunkLen)
}
}
__handleStop() {
return new Promise((resolve, reject) => {
if (this.request) {
this.request.abort()
this.request = null
}
async __handleStop() {
return new Promise<void>((resolve, reject) => {
this.__closeRequest()
if (this.ws) {
this.ws.close(err => {
if (err) {
@ -285,7 +307,7 @@ class Task extends EventEmitter {
})
}
__calculateProgress(receivedBytes) {
__calculateProgress(receivedBytes: number) {
const currentTime = performance.now()
const elaspsedTime = currentTime - this.statsEstimate.time
@ -309,24 +331,26 @@ class Task extends EventEmitter {
}
async start() {
this.status = STATUS.running
this.status = STATUS.init
await this.__init()
if (this.status !== STATUS.init) return
this.status = STATUS.running
this.__httpFetch(this.downloadUrl, this.requestOptions)
this.emit('start')
}
async stop() {
if (this.status === STATUS.stopped) return
if (this.status == STATUS.stopped || this.status == STATUS.completed) return
this.status = STATUS.stopped
await this.__handleStop()
this.emit('stop')
}
refreshUrl(url) {
refreshUrl(url: string) {
this.downloadUrl = url
}
updateSaveInfo(filePath, fileName) {
updateSaveInfo(filePath: string, fileName: string) {
this.chunkInfo.path = path.join(filePath, fileName)
}
}

View File

@ -0,0 +1,100 @@
import Downloader, { Options as DownloaderOptions } from './Downloader'
import { getRequestAgent } from './util'
import { sizeFormate } from '@common/utils'
import http from 'http'
// these are the default options
// const options = {
// method: 'GET', // Request Method Verb
// // Custom HTTP Header ex: Authorization, User-Agent
// headers: {},
// fileName: '', // Custom filename when saved
// override: false, // if true it will override the file, otherwise will append '(number)' to the end of file
// forceResume: false, // If the server does not return the "accept-ranges" header, can be force if it does support it
// // httpRequestOptions: {}, // Override the http request options
// // httpsRequestOptions: {}, // Override the https request options, ex: to add SSL Certs
// }
export interface Options {
url: string
path: string
fileName: string
method?: DownloaderOptions['requestOptions']['method']
headers?: DownloaderOptions['requestOptions']['headers']
forceResume?: boolean
proxy?: { host: string, port: number }
onCompleted?: () => void
onError?: (error: Error) => void
onFail?: (response: http.IncomingMessage) => void
onStart?: () => void
onStop?: () => void
onProgress?: (progress: LX.Download.ProgressInfo) => void
}
const noop = () => {}
export const createDownload = ({
url,
path,
fileName,
method = 'get',
forceResume,
proxy,
// resumeTime = 5000,
onCompleted = noop,
onError = noop,
onFail = noop,
onStart = noop,
onStop = noop,
onProgress = noop,
}: Options) => {
const dl = new Downloader(url, path, fileName, {
requestOptions: {
method,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
},
agent: getRequestAgent(url, proxy),
timeout: 60 * 1000,
},
forceResume,
})
dl.on('completed', () => {
onCompleted()
}).on('error', (err: any) => {
if (err.message === 'socket hang up') return
onError(err)
}).on('start', () => {
onStart()
// pauseResumeTimer(dl, resumeTime)
}).on('progress', (stats) => {
const speed = sizeFormate(stats.speed)
onProgress({
progress: parseInt(stats.progress.toFixed(2)),
speed,
downloaded: stats.downloaded,
total: stats.total,
})
// if (debugDownload) {
// const downloaded = sizeFormate(stats.downloaded)
// const total = sizeFormate(stats.total)
// console.log(`${speed}/s - ${progress}% [${downloaded}/${total}]`)
// }
}).on('stop', () => {
onStop()
// debugDownload && console.log('paused')
}).on('fail', resp => {
onFail(resp)
// debugDownload && console.log('fail')
})
// debugDownload && console.log('Downloading: ', url)
dl.start().catch(err => {
onError(err)
})
return dl
}
export type DownloaderType = Downloader

View File

@ -0,0 +1,73 @@
import { URL } from 'url'
import http from 'http'
import https from 'https'
export interface Options {
method: 'get' | 'head' | 'delete' | 'patch' | 'post' | 'put'
params?: Record<string, string>
// body?: Record<string, string>
headers?: Record<string, string>
timeout?: number
agent?: http.Agent
}
const defaultOptions: Options = {
method: 'get',
}
type HttpCallback = (res: http.IncomingMessage) => void
const sendRequest = (url: string, options: Options, callback?: HttpCallback) => {
const urlParse = new URL(url)
const httpOptions: http.RequestOptions | https.RequestOptions = {
host: urlParse.hostname,
port: urlParse.port,
path: urlParse.pathname + urlParse.search,
method: options.method,
}
if (options.params) {
(httpOptions.path as string) += `${urlParse.search ? '&' : '?'}${Object.entries(options.params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&')}`
}
if (options.headers) httpOptions.headers = { ...options.headers }
if (options.agent) httpOptions.agent = options.agent
return urlParse.protocol == 'https:'
? https.request(httpOptions, callback)
: http.request(httpOptions, callback)
}
const applyTimeout = (request: http.ClientRequest, time: number) => {
let timeout: NodeJS.Timeout | null = setTimeout(() => {
timeout = null
if (request.destroyed) return
request.destroy(new Error('Request timeout'))
}, time)
request.on('response', () => {
if (!timeout) return
clearTimeout(timeout)
timeout = null
})
}
// const isRequireRedirect = (response: http.IncomingMessage) => {
// return response.statusCode &&
// response.statusCode > 300 &&
// response.statusCode < 400 &&
// Object.hasOwn(response.headers, 'location') &&
// response.headers.location
// }
// export function request(url: string, callback: HttpCallback)
// export function request(url: string, options: Partial<Options>, callback: HttpCallback)
export function request(url: string, _options: Partial<Options>, callback?: HttpCallback) {
let options: Options = { ...defaultOptions, ..._options }
const request = sendRequest(url, options, callback)
if (options.timeout) applyTimeout(request, options.timeout)
return request
}

View File

@ -0,0 +1,26 @@
import { httpOverHttp, httpsOverHttp } from 'tunnel'
export const STATUS = {
idle: 'IDLE',
init: 'INIT',
running: 'RUNNING',
paused: 'PAUSED',
stopped: 'STOPPED',
completed: 'COMPLETED',
error: 'ERROR',
failed: 'FAILED',
} as const
const httpsRxp = /^https:/
export const getRequestAgent = (url: string, proxy?: { host: string, port: number }) => {
let options
if (proxy) {
options = {
proxy: {
host: proxy.host,
port: proxy.port,
},
}
}
return options ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)(options) : undefined
}

View File

@ -0,0 +1,38 @@
import { shell, clipboard } from 'electron'
/**
*
* @param {string} dir
*/
export const openDirInExplorer = (dir: string) => {
shell.showItemInFolder(dir)
}
/**
* URL
* @param {*} url
*/
export const openUrl = async(url: string) => {
if (!/^https?:\/\//.test(url)) return
await shell.openExternal(url)
}
/**
*
* @param str
*/
export const clipboardWriteText = (str: string) => {
clipboard.writeText(str)
}
/**
*
* @returns
*/
export const clipboardReadText = (): string => {
return clipboard.readText()
}

40
src/common/utils/index.ts Normal file
View File

@ -0,0 +1,40 @@
import log from 'electron-log'
log.transports.file.level = 'info'
export const isLinux = process.platform == 'linux'
export const isWin = process.platform == 'win32'
export const isMac = process.platform == 'darwin'
export const isProd = process.env.NODE_ENV == 'production'
// https://stackoverflow.com/a/53387532
export function compareVer(currentVer: string, targetVer: string): -1 | 0 | 1 {
// treat non-numerical characters as lower version
// replacing them with a negative number based on charcode of each character
const fix = (s: string) => `.${s.toLowerCase().charCodeAt(0) - 2147483647}.`
const currentVerArr: Array<string | number> = ('' + currentVer).replace(/[^0-9.]/g, fix).split('.')
const targetVerArr: Array<string | number> = ('' + targetVer).replace(/[^0-9.]/g, fix).split('.')
let c = Math.max(currentVerArr.length, targetVerArr.length)
for (let i = 0; i < c; i++) {
// convert to integer the most efficient way
currentVerArr[i] = ~~currentVerArr[i]
targetVerArr[i] = ~~targetVerArr[i]
if (currentVerArr[i] > targetVerArr[i]) return 1
else if (currentVerArr[i] < targetVerArr[i]) return -1
}
return 0
}
export const encodePath = (path: string) => {
// https://github.com/lyswhut/lx-music-desktop/issues/963
return path.replaceAll('%', '%25')
}
export {
log,
}
export * from './common'

View File

@ -2,14 +2,20 @@ const { getNow, TimeoutTools } = require('./utils')
// const fontFormateRxp = /(?=<\d+,\d+>).*?/g
const fontSplitRxp = /(?=<\d+,\d+>).*?/g
const timeRxpAll = /<(\d+),(\d+)>/g
const timeRxp = /<(\d+),(\d+)>/
// Create animation
const createAnimation = (dom, duration) => new window.Animation(new window.KeyframeEffect(dom, [
{ backgroundSize: '0 100%' },
{ backgroundSize: '100% 100%' },
], {
const createAnimation = (dom, duration, isVertical) => new window.Animation(new window.KeyframeEffect(dom, isVertical
? [
{ backgroundSize: '100% 0' },
{ backgroundSize: '100% 100%' },
]
: [
{ backgroundSize: '0 100%' },
{ backgroundSize: '100% 100%' },
], {
duration,
easing: 'linear',
},
@ -20,25 +26,45 @@ const createAnimation = (dom, duration) => new window.Animation(new window.Keyfr
// https://jsfiddle.net/ceqpnbky/1/
module.exports = class FontPlayer {
constructor({ time = 0, lyric = '', extendedLyrics = '', lineClassName = '', fontClassName = '', extendedLrcClassName = '', lineModeClassName = '', shadowContent = false, shadowClassName = '' }) {
constructor({
time = 0,
lyric = '',
lineContentClassName = 'line-content',
lineClassName = 'line',
shadowClassName = 'shadow',
fontModeClassName = 'font-mode',
lineModeClassName = 'line-mode',
fontLrcClassName = 'font-lrc',
extendedLrcClassName = 'extended',
shadowContent = false,
extendedLyrics = [],
isVertical = false,
}) {
this.time = time
this.lyric = lyric
this.extendedLyrics = extendedLyrics
this.isVertical = isVertical
this.lineContentClassName = lineContentClassName
this.lineClassName = lineClassName
this.fontClassName = fontClassName
this.extendedLrcClassName = extendedLrcClassName
this.lineModeClassName = lineModeClassName
this.shadowContent = shadowContent
this.shadowClassName = shadowClassName
this.extendedLyrics = extendedLyrics
this.fontModeClassName = fontModeClassName
this.fontLrcClassName = fontLrcClassName
this.extendedLrcClassName = extendedLrcClassName
this.lineModeClassName = lineModeClassName
this.isPlay = false
this.curFontNum = 0
this.maxFontNum = 0
this._performanceTime = 0
this._startTime = 0
this.fontContent = null
this.lineContent = null
this.timeoutTools = new TimeoutTools(80)
this.waitPlayTimeout = new TimeoutTools(80)
@ -53,32 +79,43 @@ module.exports = class FontPlayer {
this.lineContent = document.createElement('div')
this.lineContent.time = this.time
if (this.lineClassName) this.lineContent.classList.add(this.lineClassName)
this.fontContent = document.createElement('div')
this.fontContent.style = 'position:relative;display:inline-block;'
if (this.fontClassName) this.fontContent.classList.add(this.fontClassName)
if (this.shadowContent) {
this.fontShadowContent = document.createElement('div')
this.fontShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
this.fontShadowContent.className = this.shadowClassName
this.fontContent.appendChild(this.fontShadowContent)
}
this.lineContent.appendChild(this.fontContent)
this.lineContent.className = this.lineContentClassName
this.line = document.createElement('div')
this.line.style = 'position:relative;display:inline-block;'
this.line.className = this.lineClassName
this.lineContent.appendChild(this.line)
this.lrcContent = document.createElement('div')
this.lrcContent.className = this.fontLrcClassName
// if (this.shadowContent) {
// this.lrcShadowContent = document.createElement('div')
// this.lrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
// this.lrcShadowContent.className = this.shadowClassName
// this.line.appendChild(this.lrcShadowContent)
// }
this.line.appendChild(this.lrcContent)
for (const lrc of this.extendedLyrics) {
const extendedLrcContent = document.createElement('div')
extendedLrcContent.style = 'position:relative;display:inline-block;'
extendedLrcContent.className = this.extendedLrcClassName
extendedLrcContent.textContent = lrc
this.lineContent.appendChild(document.createElement('br'))
this.lineContent.appendChild(extendedLrcContent)
if (this.shadowContent) {
const extendedLrcShadowContent = document.createElement('div')
extendedLrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
extendedLrcShadowContent.className = this.shadowClassName
extendedLrcShadowContent.textContent = lrc
extendedLrcContent.appendChild(extendedLrcShadowContent)
}
// if (this.shadowContent) {
// const extendedLrcShadowContent = document.createElement('div')
// extendedLrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
// extendedLrcShadowContent.className = this.shadowClassName
// extendedLrcShadowContent.textContent = lrc
// extendedLrcContent.appendChild(extendedLrcShadowContent)
// }
const lineContent = document.createElement('div')
lineContent.className = this.fontLrcClassName
lineContent.textContent = lrc.replace(timeRxpAll, '')
extendedLrcContent.appendChild(lineContent)
}
this._parseLyric()
}
@ -90,21 +127,24 @@ module.exports = class FontPlayer {
this.maxFontNum = fonts.length - 1
this.fonts = []
let text
// let lineText = ''
let lrcShadowContent
for (const font of fonts) {
text = font.replace(timeRxp, '')
if (RegExp.$2 == '') return this._handleLineParse()
const time = parseInt(RegExp.$2)
const dom = document.createElement('span')
let shadowDom
dom.textContent = text
const animation = createAnimation(dom, time)
this.fontContent.appendChild(dom)
const animation = createAnimation(dom, time, this.isVertical)
this.lrcContent.appendChild(dom)
// lineText += text
if (this.shadowContent) {
shadowDom = document.createElement('span')
if (!lrcShadowContent) lrcShadowContent = document.createElement('div')
const shadowDom = document.createElement('span')
shadowDom.textContent = text
this.fontShadowContent.appendChild(shadowDom)
lrcShadowContent.appendChild(shadowDom)
}
// dom.style = shadowDom.style = this.fontStyle
// dom.className = shadowDom.className = this.fontClassName
@ -114,29 +154,34 @@ module.exports = class FontPlayer {
startTime: parseInt(RegExp.$1),
time,
dom,
shadowDom,
animation,
})
}
if (this.shadowContent && lrcShadowContent) {
lrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
lrcShadowContent.className = this.shadowClassName
this.line.appendChild(lrcShadowContent)
}
this.line.appendChild(this.lrcContent)
this.fonts.at(-1)?.animation.addEventListener('finish', () => {
this.lineContent.classList.add('played')
this.isPlay = false
})
this.lineContent.classList.add(this.fontModeClassName)
// if (this.shadowContent) this.lrcShadowContent.textContent = lineText
// console.log(this.fonts)
}
_handleLineParse() {
this.isLineMode = true
const dom = document.createElement('span')
let shadowDom
dom.classList.add(this.lineModeClassName)
dom.textContent = this.lyric
if (this.shadowContent) {
shadowDom = document.createElement('span')
shadowDom.textContent = this.lyric
this.fontShadowContent.appendChild(shadowDom)
}
this.fontContent.appendChild(dom)
this.lineContent.classList.add(this.lineModeClassName)
this.lrcContent.textContent = this.lyric
// if (this.shadowContent) this.lrcShadowContent.textContent = this.lyric
this.fonts.push({
text: this.lyric,
dom,
shadowDom,
})
}
@ -157,10 +202,11 @@ module.exports = class FontPlayer {
const driftTime = currentTime - curFont.startTime
if (currentTime > curFont.startTime + curFont.time) {
this._handlePlayFont(curFont, driftTime, true)
this.lineContent.classList.add('played')
this.isPlay = false
this.pause()
} else {
this._handlePlayFont(curFont, driftTime)
this.isPlay = false
}
}
@ -185,7 +231,12 @@ module.exports = class FontPlayer {
_handlePlayLine(isPlayed) {
this.isPlay = false
this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0'
if (isPlayed) {
this.lineContent.classList.add('played')
} else {
this.lineContent.classList.remove('played')
}
// this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0'
}
_handlePauseFont(font) {
@ -246,6 +297,7 @@ module.exports = class FontPlayer {
this.pause()
if (this.isLineMode) return this._handlePlayLine(true)
this.lineContent.classList.remove('played')
this.isPlay = true
this._performanceTime = getNow()
this._startTime = curTime
@ -281,6 +333,7 @@ module.exports = class FontPlayer {
finish() {
this.pause()
if (this.isLineMode) return this._handlePlayLine(true)
this.lineContent.classList.add('played')
for (const font of this.fonts) {
font.animation.cancel()
@ -292,6 +345,7 @@ module.exports = class FontPlayer {
reset() {
this.pause()
if (this.isLineMode) return this._handlePlayLine(false)
this.lineContent.classList.remove('played')
for (const font of this.fonts) {
font.animation.cancel()
font.dom.style.backgroundSize = '0 100%'

View File

@ -8,33 +8,46 @@ module.exports = class Lyric {
lyric = '',
extendedLyrics = [],
offset = 0,
lineClassName = '',
fontClassName = 'font',
lineContentClassName = 'line-content',
lineClassName = 'line',
shadowClassName = 'shadow',
fontModeClassName = 'font-mode',
lineModeClassName = 'line-mode',
fontLrcClassName = 'font-lrc',
extendedLrcClassName = 'extended',
activeLineClassName = 'active',
lineModeClassName = 'line',
shadowClassName = '',
shadowContent = false,
onPlay = function() { },
onSetLyric = function() { },
isVertical = false,
onPlay = function(line, text) { },
onSetLyric = function(lines, offset) { },
onUpdateLyric = function(lines) { },
}) {
this.lyric = lyric
this.extendedLyrics = extendedLyrics
this.offset = offset
this.onPlay = onPlay
this.onSetLyric = onSetLyric
this.onUpdateLyric = onUpdateLyric
this.lineContentClassName = lineContentClassName
this.lineClassName = lineClassName
this.fontClassName = fontClassName
this.shadowClassName = shadowClassName
this.fontModeClassName = fontModeClassName
this.lineModeClassName = lineModeClassName
this.fontLrcClassName = fontLrcClassName
this.extendedLrcClassName = extendedLrcClassName
this.activeLineClassName = activeLineClassName
this.lineModeClassName = lineModeClassName
this.shadowClassName = shadowClassName
this.shadowContent = shadowContent
this.isVertical = isVertical
this.playingLineNum = -1
this.isLineMode = false
this.initInfo = {
lines: [],
offset: 0,
}
this.linePlayer = new LinePlayer({
offset: this.offset,
onPlay: this._handleLinePlayerOnPlay,
@ -93,7 +106,7 @@ module.exports = class Lyric {
this.onPlay(num, this._lines[num].text)
}
_handleLinePlayerOnSetLyric = (lyricLines, offset) => {
_initLines = (lyricLines, offset, isUpdate) => {
// console.log(lyricLines)
// this._lines = lyricsLines
this.isLineMode = lyricLines.length && !/^<\d+,\d+>/.test(lyricLines[0].text)
@ -105,12 +118,15 @@ module.exports = class Lyric {
time: line.time,
lyric: line.text,
extendedLyrics: line.extendedLyrics,
lineContentClassName: this.lineContentClassName,
lineClassName: this.lineClassName,
fontClassName: this.fontClassName,
extendedLrcClassName: this.extendedLrcClassName,
lineModeClassName: this.lineModeClassName,
shadowClassName: this.shadowClassName,
fontModeClassName: this.fontModeClassName,
lineModeClassName: this.lineModeClassName,
fontLrcClassName: this.fontLrcClassName,
extendedLrcClassName: this.extendedLrcClassName,
shadowContent: this.shadowContent,
isVertical: this.isVertical,
})
this._lineFonts.push(fontPlayer)
@ -127,11 +143,15 @@ module.exports = class Lyric {
time: line.time,
lyric: line.text,
extendedLyrics: line.extendedLyrics,
lineContentClassName: this.lineContentClassName,
lineClassName: this.lineClassName,
fontClassName: this.fontClassName,
extendedLrcClassName: this.extendedLrcClassName,
shadowClassName: this.shadowClassName,
fontModeClassName: this.fontModeClassName,
lineModeClassName: this.lineModeClassName,
fontLrcClassName: this.fontLrcClassName,
extendedLrcClassName: this.extendedLrcClassName,
shadowContent: this.shadowContent,
isVertical: this.isVertical,
})
this._lineFonts.push(fontPlayer)
@ -148,7 +168,15 @@ module.exports = class Lyric {
let newOffset = this.isLineMode ? this.offset + 60 : this.offset
offset = offset - this.linePlayer.offset + newOffset
this.linePlayer.offset = newOffset
this.onSetLyric(this._lines, offset)
if (isUpdate) this.onUpdateLyric(this._lines)
else this.onSetLyric(this._lines, offset)
}
_handleLinePlayerOnSetLyric = (lyricLines, offset) => {
this._initLines(lyricLines, offset, false)
this.playingLineNum = 0
this.initInfo.lines = lyricLines
this.initInfo.offset = offset
}
play(curTime) {
@ -159,7 +187,11 @@ module.exports = class Lyric {
pause() {
if (!this.linePlayer) return
this.linePlayer.pause()
if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum].pause()
if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum]?.pause()
}
setOffset(offset) {
this.linePlayer.offset = offset
}
setLyric(lyric, extendedLyrics) {
@ -167,4 +199,14 @@ module.exports = class Lyric {
this.extendedLyrics = extendedLyrics
this._init()
}
setVertical(isVertical) {
this.isVertical = isVertical
this._initLines(this.initInfo.lines, this.initInfo.offset, true)
if (this.linePlayer.isPlay) {
const num = this.playingLineNum
this.playingLineNum = 0
this._handleLinePlayerOnPlay(num, '', this.linePlayer._currentTime())
} else this.playingLineNum = 0
}
}

View File

@ -1,6 +1,7 @@
const { getNow, TimeoutTools } = require('./utils')
const timeExp = /^\[([\d:.]*)\]{1}/g
const timeFieldExp = /^(?:\[[\d:.]+\])+/g
const timeExp = /[\d:.]+/g
const tagRegMap = {
title: 'ti',
artist: 'ar',
@ -15,13 +16,18 @@ const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
const extendedLines = extendedLyric.split(/\r\n|\n|\r/)
for (let i = 0; i < extendedLines.length; i++) {
const line = extendedLines[i].trim()
let result = timeExp.exec(line)
let result = timeFieldExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
const timeField = result[0]
const text = line.replace(timeFieldExp, '').trim()
if (text) {
const timeStr = RegExp.$1.replace(/(\.\d\d)0$/, '$1')
const targetLine = lrcLinesMap[timeStr]
if (targetLine) targetLine.extendedLyrics.push(text)
const times = timeField.match(timeExp)
if (times == null) continue
for (const time of times) {
const timeStr = time.replace(/(\.\d\d)0$/, '$1')
const targetLine = lrcLinesMap[timeStr]
if (targetLine) targetLine.extendedLyrics.push(text)
}
}
}
}
@ -69,21 +75,30 @@ module.exports = class LinePlayer {
const linesMap = {}
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
let result = timeExp.exec(line)
let result = timeFieldExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
const timeField = result[0]
const text = line.replace(timeFieldExp, '').trim()
if (text) {
const timeStr = RegExp.$1.replace(/(\.\d\d)0$/, '$1')
const timeArr = timeStr.split(':')
if (timeArr.length < 3) timeArr.unshift(0)
if (timeArr[2].indexOf('.') > -1) {
timeArr.push(...timeArr[2].split('.'))
timeArr.splice(2, 1)
}
linesMap[timeStr] = {
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
text,
extendedLyrics: [],
const times = timeField.match(timeExp)
if (times == null) continue
for (const time of times) {
const timeStr = time.replace(/(\.\d\d)0$/, '$1')
if (linesMap[timeStr]) {
linesMap[timeStr].extendedLyrics.push(text)
continue
}
const timeArr = timeStr.split(':')
if (timeArr.length < 3) timeArr.unshift(0)
if (timeArr[2].indexOf('.') > -1) {
timeArr.push(...timeArr[2].split('.'))
timeArr.splice(2, 1)
}
linesMap[timeStr] = {
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
text,
extendedLyrics: [],
}
}
}
}

View File

@ -17,7 +17,7 @@ exports.TimeoutTools = class TimeoutTools {
// console.log('diff', diff)
if (diff > 0) {
if (diff < this.thresholdTime) return this.run()
return this.timeoutId = setTimeout(() => {
return this.timeoutId = window.setTimeout(() => {
this.timeoutId = null
this.run()
}, diff - this.thresholdTime)

View File

@ -0,0 +1,139 @@
import { compareVer } from './index'
const oldThemeMap = {
0: 'green',
1: 'blue',
2: 'yellow',
3: 'orange',
4: 'red',
10: 'pink',
5: 'purple',
6: 'grey',
11: 'ming',
12: 'blue2',
13: 'black',
7: 'mid_autumn',
8: 'naruto',
9: 'happy_new_year',
} as const
export default (setting: any): Partial<LX.AppSetting> => {
setting = { ...setting }
// 迁移 v2 之前的配置
if (compareVer(setting.version, '2.0.0') < 0) {
// 迁移列表滚动位置设置 ~0.18.3
if (setting.list.scroll) {
let scroll = setting.list.scroll
if (setting.list?.isSaveScrollLocation) setting.list.isSaveScrollLocation = scroll.enable
delete setting.list.scroll
}
// 修正拼写问题 v1.8.2 及以前
if (setting.player.isShowLyricTransition != null) {
setting.player.isShowLyricTranslation = setting.player.isShowLyricTransition
delete setting.player.isShowLyricTransition
}
// 迁移v1.19.0之前的主题设置
if (setting.themeId != null) {
setting.theme = {
id: setting.themeId,
}
delete setting.themeId
}
setting.tray.enable = setting.tray.isShow
setting['common.windowSizeId'] = setting.windowSizeId
setting['common.startInFullscreen'] = setting.startInFullscreen
setting['common.langId'] = setting.langId
setting['common.apiSource'] = setting.apiSource
setting['common.sourceNameType'] = setting.sourceNameType
setting['common.font'] = setting.font
setting['common.isShowAnimation'] = setting.isShowAnimation
setting['common.randomAnimate'] = setting.randomAnimate
setting['common.isAgreePact'] = setting.isAgreePact
setting['common.controlBtnPosition'] = setting.controlBtnPosition
setting['player.togglePlayMethod'] = setting.player.togglePlayMethod
setting['player.highQuality'] = setting.player.highQuality
setting['player.isShowTaskProgess'] = setting.player.isShowTaskProgess
setting['player.volume'] = setting.player.volume
setting['player.isMute'] = setting.player.isMute
setting['player.mediaDeviceId'] = setting.player.mediaDeviceId
setting['player.isMediaDeviceRemovedStopPlay'] = setting.player.isMediaDeviceRemovedStopPlay
setting['player.isShowLyricTranslation'] = setting.player.isShowLyricTranslation
setting['player.isShowLyricRoma'] = setting.player.isShowLyricRoma
setting['player.isS2t'] = setting.player.isS2t
setting['player.isPlayLxlrc'] = setting.player.isPlayLxlrc
setting['player.isSavePlayTime'] = setting.player.isSavePlayTime
setting['player.audioVisualization'] = setting.player.audioVisualization
setting['player.waitPlayEndStop'] = setting.player.waitPlayEndStop
setting['player.waitPlayEndStopTime'] = setting.player.waitPlayEndStopTime
setting['player.autoSkipOnError'] = setting.player.autoSkipOnError
setting['playDetail.isZoomActiveLrc'] = setting.playDetail.isZoomActiveLrc
setting['playDetail.isShowLyricProgressSetting'] = setting.playDetail.isShowLyricProgressSetting
setting['playDetail.style.fontSize'] = setting.playDetail.style.fontSize
setting['playDetail.style.align'] = setting.playDetail.style.align
setting['desktopLyric.enable'] = setting.desktopLyric.enable
setting['desktopLyric.isLock'] = setting.desktopLyric.isLock
setting['desktopLyric.isAlwaysOnTop'] = setting.desktopLyric.isAlwaysOnTop
setting['desktopLyric.isAlwaysOnTopLoop'] = setting.desktopLyric.isAlwaysOnTopLoop
setting['desktopLyric.width'] = setting.desktopLyric.width
setting['desktopLyric.height'] = setting.desktopLyric.height
setting['desktopLyric.x'] = setting.desktopLyric.x
setting['desktopLyric.y'] = setting.desktopLyric.y
setting['desktopLyric.isLockScreen'] = setting.desktopLyric.isLockScreen
setting['desktopLyric.isDelayScroll'] = setting.desktopLyric.isDelayScroll
setting['desktopLyric.isHoverHide'] = setting.desktopLyric.isHoverHide
setting['desktopLyric.style.font'] = setting.desktopLyric.style.font
setting['desktopLyric.style.fontSize'] = setting.desktopLyric.style.fontSize / 100 * 16
setting['desktopLyric.style.opacity'] = setting.desktopLyric.style.opacity
setting['desktopLyric.style.isZoomActiveLrc'] = setting.desktopLyric.style.isZoomActiveLrc
setting['list.isClickPlayList'] = setting.list.isClickPlayList
setting['list.isShowAlbumName'] = setting.list.isShowAlbumName
setting['list.isShowSource'] = setting.list.isShowSource
setting['list.isSaveScrollLocation'] = setting.list.isSaveScrollLocation
setting['list.addMusicLocationType'] = setting.list.addMusicLocationType
setting['download.enable'] = setting.download.enable
setting['download.savePath'] = setting.download.savePath
setting['download.fileName'] = setting.download.fileName
setting['download.maxDownloadNum'] = setting.download.maxDownloadNum
setting['download.isDownloadLrc'] = setting.download.isDownloadLrc
setting['download.lrcFormat'] = setting.download.lrcFormat
setting['download.isEmbedPic'] = setting.download.isEmbedPic
setting['download.isEmbedLyric'] = setting.download.isEmbedLyric
setting['download.isUseOtherSource'] = setting.download.isUseOtherSource
setting['search.isShowHotSearch'] = setting.search.isShowHotSearch
setting['search.isShowHistorySearch'] = setting.search.isShowHistorySearch
setting['search.isFocusSearchBox'] = setting.search.isFocusSearchBox
setting['network.proxy.enable'] = setting.network.proxy.enable
setting['network.proxy.host'] = setting.network.proxy.host
setting['network.proxy.port'] = setting.network.proxy.port
setting['network.proxy.username'] = setting.network.proxy.username
setting['network.proxy.password'] = setting.network.proxy.password
setting['tray.enable'] = setting.tray.enable
setting['tray.themeId'] = setting.tray.themeId
setting['sync.enable'] = setting.sync.enable
setting['sync.port'] = setting.sync.port
setting['theme.id'] = oldThemeMap[setting.theme.id as keyof typeof oldThemeMap]
setting['theme.lightId'] = oldThemeMap[setting.theme.lightId as keyof typeof oldThemeMap]
setting['theme.darkId'] = oldThemeMap[setting.theme.darkId as keyof typeof oldThemeMap]
setting['odc.isAutoClearSearchInput'] = setting.odc.isAutoClearSearchInput
setting['odc.isAutoClearSearchList'] = setting.odc.isAutoClearSearchList
}
return setting
}

View File

@ -0,0 +1,71 @@
const http = require('http')
const https = require('https')
const fs = require('fs')
const sendRequest = (url) => {
const urlParse = new URL(url)
const httpOptions = {
method: 'get',
host: urlParse.hostname,
port: urlParse.port,
path: urlParse.pathname + urlParse.search,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
},
}
// console.log(httpOptions)
return url.protocol === 'https:'
? https.request(httpOptions)
: http.request(httpOptions)
}
module.exports = (url, filePath) => {
return new Promise((resolve) => {
sendRequest(url)
.on('response', response => {
// console.log(response.statusCode)
if (response.statusCode !== 200 && response.statusCode != 206) {
response.destroy(new Error('failed'))
return
}
response
.pipe(fs.createWriteStream(filePath))
.on('finish', () => {
// console.log('finish')
if (response.complete) {
// console.log('complete')
// meta.APIC = picPath
// handleWriteMeta(meta, filePath)
resolve(true)
} else {
resolve(false)
fs.unlink(filePath, err => {
if (err) console.log(err.message)
})
}
}).on('error', err => {
// console.log('response error')
if (err) console.log(err.message)
fs.unlink(filePath, err => {
if (err) console.log(err.message)
})
resolve(false)
})
})
.on('error', err => {
if (err) console.log(err.message)
// delete meta.APIC
// handleWriteMeta(meta, filePath)
resolve(false)
})
.end()
})
}
// const url = 'https://y.gtimg.cn/music/photo_new/T002R500x500M000000nfgwP0D6qxd.jpg'
// // const url = 'http://p4.music.126.net/-U2K8GKlASCSXK0cRre1gA==/109951163188718762.jpg'
// const picPath = require('path').join(__dirname, 'test.jpg')
// module.exports(url, picPath).then((sucee) => {
// console.log(sucee)
// })

View File

@ -2,9 +2,9 @@ const fs = require('fs')
const fsPromises = fs.promises
const path = require('path')
const getImgSize = require('image-size')
const request = require('request')
const download = require('./downloader')
const FlacProcessor = require('./flac-metadata')
const FlacProcessor = require('./flac-metadata/index')
const extReg = /^(\.(?:jpe?g|png)).*$/
const vendor = 'reference libFLAC 1.2.1 20070917'
@ -67,29 +67,14 @@ module.exports = (filePath, meta) => {
let ext = path.extname(picUrl)
let picPath = filePath.replace(/\.flac$/, '') + (ext ? ext.replace(extReg, '$1') : '.jpg')
request(picUrl)
.on('response', respones => {
if (respones.statusCode !== 200 && respones.statusCode != 206) return writeMeta(filePath, meta)
respones
.pipe(fs.createWriteStream(picPath))
.on('finish', async() => {
if (respones.complete) {
await writeMeta(filePath, meta, picPath)
} else {
writeMeta(filePath, meta)
}
fs.unlink(picPath, err => {
if (err) console.log(err.message)
})
})
.on('error', err => {
download(picUrl, picPath).then(success => {
if (success) {
writeMeta(filePath, meta, picPath).finally(() => {
fs.unlink(picPath, err => {
if (err) console.log(err.message)
writeMeta(filePath, meta)
})
})
.on('error', err => {
if (err) console.log(err.message)
writeMeta(filePath, meta)
})
})
} else writeMeta(filePath, meta)
})
}

8
src/common/utils/musicMeta/index.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
export interface MusicMeta {
title: string
artist: string | null
album: string | null
APIC: string | null
lyrics: string | null
}
export function setMeta(filePath: string, meta: MusicMeta): void

View File

@ -0,0 +1,38 @@
const NodeID3 = require('node-id3')
const path = require('path')
const fs = require('fs')
const download = require('./downloader')
const extReg = /^(\.(?:jpe?g|png)).*$/
const handleWriteMeta = (meta, filePath) => {
if (meta.lyrics) {
meta.unsynchronisedLyrics = {
language: 'zho',
text: meta.lyrics,
}
delete meta.lyrics
}
NodeID3.write(meta, filePath)
}
module.exports = (filePath, meta) => {
if (!meta.APIC) return handleWriteMeta(meta, filePath)
if (!/^http/.test(meta.APIC)) {
delete meta.APIC
return handleWriteMeta(meta, filePath)
}
let ext = path.extname(meta.APIC)
let picPath = filePath.replace(/\.mp3$/, '') + (ext ? ext.replace(extReg, '$1') : '.jpg')
download(meta.APIC, picPath).then(success => {
if (success) {
meta.APIC = picPath
handleWriteMeta(meta, filePath)
fs.unlink(picPath, err => {
if (err) console.log(err.message)
})
} else {
delete meta.APIC
handleWriteMeta(meta, filePath)
}
})
}

159
src/common/utils/nodejs.ts Normal file
View File

@ -0,0 +1,159 @@
import fs from 'fs'
import crypto from 'crypto'
import { gzip, gunzip } from 'zlib'
import { log } from '@common/utils'
import path from 'path'
export const joinPath = (...paths: string[]): string => path.join(...paths)
export const extname = (p: string): string => path.extname(p)
export const basename = (p: string, ext?: string): string => path.basename(p, ext)
export const dirname = (p: string): string => path.dirname(p)
/**
*
* @param {*} path
*/
export const checkPath = async(path: string): Promise<boolean> => {
return await new Promise(resolve => {
fs.access(path, fs.constants.F_OK, err => {
if (err) return resolve(false)
resolve(true)
})
})
}
export const getFileStats = async(path: string): Promise<fs.Stats | null> => {
return await new Promise(resolve => {
fs.stat(path, (err, stats) => {
if (err) return resolve(null)
resolve(stats)
})
})
}
/**
*
* @param path
* @returns
*/
export const createDir = async(path: string): Promise<void> => {
return await new Promise((resolve, reject) => {
fs.access(path, fs.constants.F_OK | fs.constants.W_OK, err => {
if (err) {
if (err.code === 'ENOENT') {
fs.mkdir(path, { recursive: true }, err => {
if (err) return reject(err)
resolve()
})
return
}
return reject(err)
}
resolve()
})
})
}
export const removeFile = async(path: string) => new Promise<void>((resolve, reject) => {
fs.access(path, fs.constants.F_OK, err => {
if (err) return err.code == 'ENOENT' ? resolve() : reject(err)
fs.unlink(path, err => {
if (err) return reject(err)
resolve()
})
})
})
export const readFile = async(path: string) => fs.promises.readFile(path)
/**
* MD5 hash
* @param {*} str
*/
export const toMD5 = (str: string) => crypto.createHash('md5').update(str).digest('hex')
export const gzipData = async(str: string): Promise<Buffer> => {
return await new Promise((resolve, reject) => {
gzip(str, (err, result) => {
if (err) return reject(err)
resolve(result)
})
})
}
export const gunzipData = async(buf: Buffer): Promise<string> => {
return await new Promise((resolve, reject) => {
gunzip(buf, (err, result) => {
if (err) return reject(err)
resolve(result.toString())
})
})
}
/**
* lx
* @param path
* @param data
*/
export const saveLxConfigFile = async(path: string, data: any) => {
if (!path.endsWith('.lxmc')) path += '.lxmc'
fs.writeFile(path, await gzipData(JSON.stringify(data)), 'binary', err => {
console.log(err)
})
}
/**
* lx
* @param path
* @returns
*/
export const readLxConfigFile = async(path: string): Promise<any> => {
let isJSON = path.endsWith('.json')
let data: string | Buffer = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary')
if (!data || isJSON) return data
data = await gunzipData(Buffer.from(data, 'binary'))
data = JSON.parse(data)
// 修复v1.14.0出现的导出数据被序列化两次的问题
if (typeof data != 'object') {
try {
data = JSON.parse(data)
} catch (err) {
return data
}
}
return data
}
export const saveStrToFile = async(path: string, str: string | Buffer): Promise<void> => {
await new Promise<void>((resolve, reject) => {
fs.writeFile(path, str, err => {
if (err) {
log.error(err)
reject(err)
return
}
resolve()
})
})
}
export const b64DecodeUnicode = (str: string): string => {
// Going backwards: from bytestream, to percent-encoding, to original string.
// return decodeURIComponent(window.atob(str).split('').map(function(c) {
// return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
// }).join(''))
return Buffer.from(str, 'base64').toString()
}
export const copyFile = async(sourcePath: string, distPath: string) => {
return fs.promises.copyFile(sourcePath, distPath)
}
export const moveFile = async(sourcePath: string, distPath: string) => {
return fs.promises.rename(sourcePath, distPath)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
const fs = require('fs').promises
const path = require('path')
const sourceFilePath = path.join(__dirname, './kMandarin_8105.txt')
const distFilePath = path.join(__dirname, './pinyin.txt')
const yuanyin = [
['ā', 'a'],
['á', 'a'],
['ǎ', 'a'],
['à', 'a'],
['ē', 'e'],
['é', 'e'],
['ě', 'e'],
['è', 'e'],
['ī', 'i'],
['í', 'i'],
['ǐ', 'i'],
['ì', 'i'],
['ō', 'o'],
['ó', 'o'],
['ǒ', 'o'],
['ò', 'o'],
['ū', 'u'],
['ú', 'u'],
['ǔ', 'u'],
['ù', 'u'],
['ǖ', 'v'],
['ǘ', 'v'],
['ǚ', 'v'],
['ǜ', 'v'],
]
const parse = async() => {
let datas = (await fs.readFile(sourceFilePath)).toString()
datas = datas.replace(/ +=> +(\w|\+)+ */gm, ' ')
for (const [y1, y2] of yuanyin) datas = datas.replaceAll(y1, y2)
// console.log(datas)
const lines = datas.split('\n')
const dict = {}
for (let line of lines) {
if (!line || line.startsWith('#')) continue
line = line.trim().replace(/^[\w+]+: */, '')
let [p1, comment] = line.split('#')
let [z, ps] = comment.split(/(?: *\? *-> *| *-> *)/)
const ys = new Set([p1.trim()])
if (ps != null) ps.split(/(?: +| *, *)/).forEach(y => ys.add(y.trim()))
dict[z.trim()] = Array.from(ys)
}
fs.writeFile(distFilePath, JSON.stringify(dict))
}
parse()
// let dict = {}
// let line = 'U+2CBBF: qi # 𬮿 ?-> gai,ai'
// line = line.trim().replace(/^[\w+]+: */, '')
// let [p1, comment] = line.split('#')
// let [z, ps] = comment.split(/ *\? *-> */)
// const ys = dict[z.trim()] = [p1.trim()]
// console.log(ps)
// if (ps != null) ys.push(...ps.split(/(?: +| *, *)/).map(y => y.trim()))
// console.log(dict)

File diff suppressed because one or more lines are too long

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