diff --git a/plugins/lx-music-ztools/.gitattributes b/plugins/lx-music-ztools/.gitattributes new file mode 100644 index 000000000..cc6ed1c28 --- /dev/null +++ b/plugins/lx-music-ztools/.gitattributes @@ -0,0 +1,2 @@ +# Preserve upstream source bytes, including historical whitespace, for diffing. +vendor/lx/** -whitespace diff --git a/plugins/lx-music-ztools/.gitignore b/plugins/lx-music-ztools/.gitignore new file mode 100644 index 000000000..aeea62fa7 --- /dev/null +++ b/plugins/lx-music-ztools/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +.superpowers/ +src-ztools/preload/upstream/ +src-ztools/dist/ +artifacts/ +test-results/ +.env* +*.log diff --git a/plugins/lx-music-ztools/CHANGELOG.md b/plugins/lx-music-ztools/CHANGELOG.md new file mode 100644 index 000000000..ee8f997fe --- /dev/null +++ b/plugins/lx-music-ztools/CHANGELOG.md @@ -0,0 +1,16 @@ +# 更新日志 + +## 0.0.2 - 2026-11-10 + +- 默认入口切换为原 LX Music 界面,补齐 Webpack 与可读 CommonJS 服务构建。 +- 支持 LX API 2.0.0 自定义音源的本地 JS、URL 导入与选择;脚本在隔离 Worker 中运行。 +- 接通原播放器、列表和设置,验证受控音源解析播放、暂停/继续、后台播放与下一首。 +- 去掉 LX 内部的缩小、关闭按钮及桌面窗口留白、圆角、阴影,内容铺满 ZTools 插件区域。 +- 构建时安装插件运行依赖,确保插件中心从源码构建的 ZIP 包含所需模块。 +- 当前为 Windows 开发版,不预置音源;下载、同步、开放 API 和系统集成仍在移植,真实用户音源/平台服务未全量验证。 + +## 0.0.1 - 2026-11-09 + +- 使用官方 Vue 模板建立插件框架。 +- 接入生命周期、preload 诊断与存储探测。 +- 增加构建、打包校验与发布演练。 diff --git a/plugins/lx-music-ztools/LICENSE b/plugins/lx-music-ztools/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/plugins/lx-music-ztools/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/lx-music-ztools/README.md b/plugins/lx-music-ztools/README.md new file mode 100644 index 000000000..7576b9cc1 --- /dev/null +++ b/plugins/lx-music-ztools/README.md @@ -0,0 +1,55 @@ +# LX Music for ZTools + +将 LX Music 2.12.6 移植到 ZTools 的开发项目,不依赖另装 LX 桌面版。当前版本为 **0.0.2 开发版**,完整移植仍在进行。 + +目前已验证原 LX 界面、自定义音源本地/URL 导入、隔离执行、受控音源解析与音频播放、暂停/继续、后台播放及下一首。下载、同步、开放 API 等功能尚未完成验证,详见 [功能与验证状态](docs/ztools/compatibility.md)。不预置第三方音源。 + +## 开发与安装 + +```powershell +npm ci +npm run build:ztools +``` + +本地代码和隔离测试中的安装记录不会自动出现在日常使用的 ZTools 中。把 `src-ztools/plugin.json` 导入 ZTools 开发项目,然后安装、启用项目,才能输入「洛雪音乐」「LX Music」或「lxmusic」打开插件。 + +```powershell +npm run pack +``` + +完整运行目录为 `src-ztools`,默认播放器入口为 `dist/lx.html`。构建包含原 LX Webpack 前端、逐模块可读 CommonJS 服务与隔离音源 Worker。ZIP 输出到工作目录同级的 `output/lx-music-ztools`,包含运行依赖,不需要开发服务器。 + +在 ZTools「设置 → 插件应用 → 导入本地插件」选择 ZIP,确认安装并启用,再输入 `lxmusic` 打开。音源在原 LX 设置的「自定义源管理」中通过本地 JS 或 URL 导入并选择。详见 [本地安装](docs/ztools/installation.md)。 + +## 验证 + +- `npm run build:ztools`:类型检查、前端和服务构建、插件安装目录校验。 +- `npm run test:ztools`:桥接、SQLite 事务、数据、设置、网络、文件能力与音源契约。 +- `npm run test:p0`:隔离音源、安全边界、宿主生命周期及存储能力。 +- `npm run test:source`:受控音源协议和原界面本地/URL 导入流程。 +- `npm run test:player`:原 LX 引擎播放受控 WAV,检查进度、暂停/继续、后台和下一首。 +- `node scripts/test-install.mjs`:实际 ZIP 安装后搜索 `lxmusic` 并打开原播放器。 +- `npm run test:publish`:调用官方 CLI 1.3.2,在新临时 Git 仓库中两次发布;GitHub 和认证被替换成测试响应,不创建线上 PR。 +- `npm run test:host`:使用独立 Electron 运行本机 ZTools app.asar,测试数据写入独立临时目录。通过 `ZTOOLS_EXECUTABLE` 指定 Electron 可执行文件,通过 `ZTOOLS_APP` 指定 ZTools app.asar;两者均需已存在。宿主测试不使用用户真实配置。 + +原生移植测试报告位于 `artifacts/`。受控音源和音频结果不能代替用户真实音源、真实平台服务及人工听音验收。 + +## 真实发布 + +```powershell +gh auth status +npm run pack +git add . +git commit -m "chore: prepare release" +npm run publish:plugin +``` + +发布包装器复用已登录的 gh 凭据,通过 gh api 发送真实 GitHub REST 请求,并修正 CLI 1.3.2 在 Windows 下的 Git 参数引号;其余发布校验、Fork、上游同步与分支保护、提交、推送和 Draft PR 由官方 CLI 执行。缓存位于 `~/.config/ztools/lx-music-ztools-sparse`,凭据仅用于当前进程,完成后清除缓存 remote URL 中的凭据。 + +CLI 1.3.2 要求当前版本的日志标题 `## 0.0.2 - YYYY-MM-DD`、非默认图标以及干净的 Git 工作区。该版本会从最新 upstream/main 重建发布分支,并使用精确 force-with-lease 防止覆盖竞态提交;这与官网页面旧版“只追加、不改写”的描述不同。 + +Draft PR 创建后须补充截图、自检结果和明确的功能范围,再提交审核。当前以开发版范围送审,不宣称下载、同步等完整移植已经完成。 + +## 来源与许可 + +原 LX 源码基线为 `lyswhut/lx-music-desktop@323e4bac`,只读保留在 `vendor/lx`,沿用 Apache-2.0 许可证。适配代码、构建及宿主替换模块位于其外,出处见 `vendor/lx/UPSTREAM.md`。项目从 ZTools 官方 Vue 模板起步,是第三方移植开发,不是 LX 官方发行版。 diff --git a/plugins/lx-music-ztools/build-config/ztools/webpack.config.cjs b/plugins/lx-music-ztools/build-config/ztools/webpack.config.cjs new file mode 100644 index 000000000..c56f5429e --- /dev/null +++ b/plugins/lx-music-ztools/build-config/ztools/webpack.config.cjs @@ -0,0 +1,56 @@ +const path = require('node:path') +const webpack = require('webpack') +const { VueLoaderPlugin } = require('vue-loader') +const HTMLPlugin = require('html-webpack-plugin') +const root = path.resolve(__dirname, '../..') +const lx = path.join(root, 'vendor/lx/src') +const adapter = name => path.join(root, 'src/ztools', name) +const alias = { + '@root': lx, '@renderer': path.join(lx, 'renderer'), '@common': path.join(lx, 'common'), '@static': path.join(lx, 'static'), + [path.join(lx, 'common/rendererIpc.ts')]: adapter('rendererIpc.ts'), + '@common/rendererIpc$': adapter('rendererIpc.ts'), '@common/error$': adapter('errors.ts'), + '@common/utils/electron$': adapter('electron.ts'), '@common/utils/nodejs$': adapter('nodejs.ts'), + '@renderer/utils/request$': adapter('request.ts'), '@renderer/worker$': adapter('worker.ts'), '@renderer/worker/utils$': adapter('worker.ts'), + 'music-metadata$': adapter('metadata.ts'), 'electron-log/node$': adapter('logger.ts'), + 'node:os$': adapter('os.ts'), 'os$': adapter('os.ts'), 'node:path$': require.resolve('path-browserify'), + 'node:crypto$': require.resolve('crypto-browserify'), 'node:url$': adapter('url.ts'), 'url$': adapter('url.ts'), + 'zlib$': adapter('zlib.ts'), + 'dns$': adapter('dns.ts'), +} +// Exact module aliases must precede directory aliases such as @renderer. +const sortedAliases = Object.fromEntries(Object.entries(alias).sort(([a], [b]) => Number(b.endsWith('$')) - Number(a.endsWith('$')))) +const absoluteReplacements = new Map([ + ['common/rendererIpc', 'rendererIpc.ts'], ['common/error', 'errors.ts'], + ['common/utils/electron', 'electron.ts'], ['common/utils/nodejs', 'nodejs.ts'], + ['renderer/utils/request', 'request.ts'], ['renderer/worker', 'worker.ts'], ['renderer/worker/index', 'worker.ts'], + ['renderer/worker/utils', 'worker.ts'], ['renderer/worker/utils/index', 'worker.ts'], +].map(([file, replacement]) => [path.join(lx, file), adapter(replacement)])) +const css = module => ['style-loader', { loader: 'css-loader', options: { esModule: false, modules: module ? { namedExport: false, exportLocalsConvention: 'camelCase' } : false } }] +module.exports = { + mode: 'development', target: 'web', devtool: 'source-map', + entry: { lx: adapter('entry.ts') }, + output: { path: path.join(root, 'src-ztools/dist'), filename: '[name].js', publicPath: '', clean: false }, + resolve: { extensions: ['.ts', '.js', '.json'], alias: sortedAliases, + modules: [path.join(root, 'node_modules'), 'node_modules'], + fallback: { vm: false, crypto: require.resolve('crypto-browserify'), stream: require.resolve('stream-browserify'), buffer: require.resolve('buffer/'), events: require.resolve('events/'), path: require.resolve('path-browserify'), querystring: require.resolve('querystring-es3') } }, + module: { rules: [ + { test: /\.vue$/, loader: 'vue-loader', options: { compilerOptions: { whitespace: 'preserve' } } }, + { test: /\.ts$/, loader: 'ts-loader', options: { transpileOnly: true, appendTsSuffixTo: [/\.vue$/], configFile: path.join(root, 'tsconfig.json'), compilerOptions: { noEmit: false, allowImportingTsExtensions: false, module: 'ESNext' } }, + parser: { worker: ['*audioContext.audioWorklet.addModule()', '...'] } }, + { test: /\.pug$/, loader: 'pug-plain-loader' }, + { test: /\.css$/, oneOf: [{ resourceQuery: /module/, use: css(true) }, { use: css(false) }] }, + { test: /\.less$/, oneOf: [{ resourceQuery: /module/, use: [...css(true), 'less-loader'] }, { use: [...css(false), 'less-loader'] }] }, + { test: /\.(png|jpe?g|gif|svg|woff2?|ttf|mp3|wav|flac|ogg)$/, type: 'asset/resource', generator: { filename: 'assets/[name]-[contenthash:8][ext]' } }, + ] }, + plugins: [new VueLoaderPlugin(), new HTMLPlugin({ filename: 'lx.html', template: path.join(lx, 'renderer/index.html'), chunks: ['lx'] }), + new webpack.NormalModuleReplacementPlugin(/.*/, resource => { + if (!resource.request?.startsWith('.')) return + const original = path.resolve(resource.context, resource.request).replace(/\.(ts|js)$/, '') + if (absoluteReplacements.has(original)) resource.request = absoluteReplacements.get(original) + }), + new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'], process: 'process/browser' }), + new webpack.DefinePlugin({ 'process.platform': JSON.stringify('win32'), global: 'globalThis', __VUE_OPTIONS_API__: true, __VUE_PROD_DEVTOOLS__: false, __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false }), + new webpack.NormalModuleReplacementPlugin(/^node:/, resource => { if (resource.request === 'node:path') resource.request = require.resolve('path-browserify'); else if (resource.request === 'node:crypto') resource.request = require.resolve('crypto-browserify'); else if (resource.request === 'node:os') resource.request = adapter('os.ts'); else if (resource.request === 'node:url') resource.request = adapter('url.ts') }), + ], + stats: { preset: 'errors-warnings', errorDetails: true }, +} diff --git a/plugins/lx-music-ztools/docs/screenshots/framework.png b/plugins/lx-music-ztools/docs/screenshots/framework.png new file mode 100644 index 000000000..ecf5bfce5 Binary files /dev/null and b/plugins/lx-music-ztools/docs/screenshots/framework.png differ diff --git a/plugins/lx-music-ztools/docs/screenshots/settings.png b/plugins/lx-music-ztools/docs/screenshots/settings.png new file mode 100644 index 000000000..866bb3047 Binary files /dev/null and b/plugins/lx-music-ztools/docs/screenshots/settings.png differ diff --git a/plugins/lx-music-ztools/docs/screenshots/source-import.png b/plugins/lx-music-ztools/docs/screenshots/source-import.png new file mode 100644 index 000000000..335aa9649 Binary files /dev/null and b/plugins/lx-music-ztools/docs/screenshots/source-import.png differ diff --git a/plugins/lx-music-ztools/docs/superpowers/plans/2026-11-09-ztools-native-port.md b/plugins/lx-music-ztools/docs/superpowers/plans/2026-11-09-ztools-native-port.md new file mode 100644 index 000000000..20dd25a8c --- /dev/null +++ b/plugins/lx-music-ztools/docs/superpowers/plans/2026-11-09-ztools-native-port.md @@ -0,0 +1,215 @@ +# LX Music 原生 ZTools 插件实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 将 LX Music 2.12.6 移植为不依赖独立 LX 程序的 ZTools 插件,保留自定义音源、原界面、搜索播放、列表、下载、同步及开放 API。 + +**Architecture:** 保留 Vue 渲染层和音乐业务模块,通过独立 Webpack 入口与显式桥接替换 Electron 宿主依赖。Node 服务以可读 CommonJS 交付;音源在隔离上下文内实现 LX API 2.0.0。首先证明宿主能力、音源隔离与存储提交语义,再接入完整功能。 + +**Tech Stack:** Vue 3、TypeScript、Pug、Less、Webpack;CommonJS、Node 内置测试;Playwright Electron;ZTools db/dbStorage 与公开插件 API。 + +**Spec:** [设计方案](../specs/2026-11-09-ztools-native-port-design.md)。执行前同时阅读两份文档。 + +状态:2026-11-09 规划稿,所有任务尚未执行。以下新路径、脚本和测试是待实现内容,不能当作已经存在或通过的验证记录。LX 基线 `323e4bac`;本机参考宿主 `76addc6` / package `3.2.0`,最低支持版本须由实际能力验证确定。 + +## Global Constraints + +- 原生插件移植,在 ZTools 内运行,不依赖另装 LX 桌面版。 +- 必须支持导入、使用 LX 自定义音源;本地 `.js` 和 HTTP(S) URL 两种入口。 +- 搜索/播放/音源、原界面与设置、列表/备份、榜单/歌单、下载、同步、开放 API 是正式交付目标;P2 不是最终交付。 +- Windows x64 是首个验证目标;首包顶层 `platform` 声明 `['win32']`,这是实施建议。 +- 构建环境沿用 LX 的 Node >=22 要求;插件运行时以实际 ZTools 的 Electron/Node 能力为准。 +- 新增 preload 与其依赖不做打包、压缩或混淆;普通前端资源可以走构建流程。 +- 插件使用独立 web 构建目标与相对资源路径,最终输出 `dist-ztools/`;不更换前端框架,不覆盖原桌面构建入口。 +- 不调用宿主 internal API;不将 fs、child_process、ipcRenderer、任意 require 暴露给音源。 +- 数据在业务提交时持久化;强杀不能依赖异步退出钩子;再次打开恢复数据,不擅自自动发声。 +- 每块序列化 UTF-8 小于 256 KiB;不能用 bulkDocs 代替事务成功判定。 +- 实机验证使用独立临时数据目录;不读写用户真实 ZTools/LX 配置,不修改 ZTools 主仓库。 +- 不预置第三方音源;本地模拟结果不代表真实音源或平台可用;任何 BLOCKED 都不能当作 PASS。 +- 本轮仅产出方案;后续实施不自动发布、推送或使用 `ztools publish`。 + +## Review Focus + +1. 切换音源后旧请求才返回:不能污染新音源的初始化状态、播放地址或错误提示(任务 5)。 +2. 大列表中途写失败、账号切换或活动指针先同步到达:应仍能读到上一完整曲库,不能混入半份数据(任务 3)。 +3. 下载服务忽略 Range 或返回不同资源:必须重新下载,不能生成表面完成但内容损坏的文件(任务 8)。 +4. 重复进入/隐藏/分离产生多实例:同一时间只播放一份音频,监听器和请求不能累积(任务 6、11)。 +5. 同步断线重连与本地同时排序:必须保持协议一致性,不重复歌曲、不丢用户选择,不形成无限回推(任务 9)。 + +## 文件组织与执行约定 + +以下路径均相对于本仓库;“参考”文件不表示必须修改。前端优先通过 Webpack 精确别名适配,只有无法合理注入时才给原模块增加明确的环境接口,不建立第二套音乐界面。 + +| 路径 | 职责 | +|---|---| +| `ztools/plugin.json`、`ztools/package.json`、`ztools/package-lock.json`、`ztools/logo.png` | 插件元数据、独立 CommonJS 运行依赖及图标 | +| `ztools/preload.cjs`、`ztools/runtime/` | 生命周期入口和可审阅 Node 服务;逐文件复制到产物 | +| `src/ztools/` | 类型契约、原 renderer 的宿主替换模块、插件启动 | +| `build-config/ztools/` | Web 前端构建、运行依赖组装、校验和本地打包 | +| `tests/ztools/`、`tests/ztools/e2e/` | 契约测试、故障注入、隔离 ZTools 全链路验证 | +| `docs/ztools/` | 宿主探针结果、模块映射、安装说明和功能验证矩阵 | + +运行时新增接口统一在 `src/ztools/contracts.ts` 声明,通过 JSDoc/声明文件对 CJS 实现进行类型校验。复用 LX 原有命名空间类型,不在各服务重复定义歌曲/列表结构。所有异步调用的失败保留可识别的 `code` 与 `message`,不转成虚假成功值。 + +任务 1 新增命令约定:`build:ztools` → `node build-config/ztools/build.cjs`;`test:ztools` → `node --test tests/ztools/*.test.cjs`;`test:ztools:e2e` → `playwright test --config tests/ztools/playwright.config.ts`;`check:ztools` → `node build-config/ztools/check.cjs`,检查类型、清单及依赖;任务 11 补充 `pack:ztools` → `node build-config/ztools/package.cjs`。E2E 的 describe 使用下文 grep 名称作为标签;使用 `ZTOOLS_SOURCE_DIR` 指向只读宿主源码,缺少宿主或音源时明确报缺失条件,不静默通过。 + +每个任务遵循:先添加下面指定的行为测试并运行确认有效失败,再实现,运行指定验证,最后只提交该任务文件。提交前检查 `git diff --check` 和 `git status --short`,保留用户已有修改;提交是本地检查点,不推送。 + +## P0:先证明运行前提 + +### Task 1: 建立最小插件、真实宿主探针和构建入口 + +**Files:** 新建 `ztools/plugin.json`、`ztools/package.json`、`ztools/package-lock.json`、`ztools/logo.png`、`ztools/preload.cjs`、`src/ztools/probe.ts`、`build-config/ztools/webpack.config.cjs`、`build-config/ztools/build.cjs`、`build-config/ztools/check.cjs`、`tests/ztools/playwright.config.ts`、`tests/ztools/e2e/host.fixture.ts`、`tests/ztools/e2e/probe.spec.ts`、`docs/ztools/host-capabilities.md`;修改 `package.json`、`package-lock.json`、`.gitignore`。参考 `build-config/renderer/webpack.config.base.js`、`src/renderer/components/base/Btn.vue`。 + +**Interfaces:** 输出 `HostCapabilities`:宿主/Node/Chromium 版本、必要 API 存在性、生命周期、后台/单实例、子窗口方法、音源运行方式和存储语义的探针结果;失败项含原因与证据位置。此类型在任务 2 归入统一契约。 + +- [ ] 添加 `probe.spec.ts`:真正加载 plugin.json 后能渲染一个原 LX Vue 组件;关闭开发服务后生产资源仍可加载;断言诊断中记录实际运行时版本,未加载插件/缺 API 必须失败。运行 `npm run test:ztools:e2e -- --grep host-probe`,确认尚未实现时失败。 +- [ ] 配置独立 web 入口,相对路径、资源/字体/AudioWorklet 规则继承可复用部分;不得保留 `electron-renderer` target 或 `commonjs2` 页面输出。探针不是最终播放器,后续任务切换到正式入口。 +- [ ] 创建隔离宿主 fixture:参照 ZTools `tests/e2e/plugin-esc-hide.spec.ts` 启动源码 Electron,设置临时 `ZTOOLS_DATA_ROOT`、`ZTOOLS_LEGACY_USER_DATA_PATH` 和必要的设置开发服务。加载本插件,不修改宿主源文件;失败截图只取插件 WebContentsView。 +- [ ] 测 guest 隔离:实际 `contextIsolation:true`、无 Node/宿主桥泄漏、独立非持久 Session、导航/弹窗/权限阻止、死循环可被外部终止,以及同步 crypto/Buffer 可用。普通 ZTools 子窗口传入的安全选项不作为通过证据。guest 不满足条件时测试 Worker 备选的同步工具/消息/终止能力,最终只保留一种已验证实现。 +- [ ] 测后台与存储:隐藏继续计时、分离/返回不重建;验证 `_rev` 冲突、错误结果对象、半批失败、文档大小、账号切换和引用缺块。确认公开接口能否可靠绑定存储上下文;不能时记录本地后端取舍并更新任务 3 的适配层,不能跳过一致性要求。 +- [ ] 运行 `npm run build:ztools`、`npm run check:ztools` 和探针;将结果写入 `host-capabilities.md`。音源隔离、后台生命周期、存储发布语义三项均有实证后进入 P1;任一失败先调整设计和本任务。本地提交:`build: add ZTools runtime feasibility probe`。 + +## P1:接通宿主和数据 + +### Task 2: 统一桥接、生命周期和原模块映射 + +**Files:** 新建 `src/ztools/contracts.ts`、`src/ztools/entry.ts`、`src/ztools/rendererIpc.ts`、`src/ztools/host.ts`、`src/ztools/worker.ts`、`ztools/runtime/bridge.cjs`、`ztools/runtime/lifecycle.cjs`、`ztools/runtime/settings.cjs`、`tests/ztools/bridge.test.cjs`、`docs/ztools/module-map.md`;修改任务 1 的 preload/构建配置。参考 `src/common/rendererIpc.ts`、`src/renderer/utils/ipc.ts`、`src/renderer/utils/env.js`、`src/renderer/worker/utils/index.ts`、`src/renderer/core/useApp/index.ts`。 + +**Interfaces:** `Bridge.invoke(channel:string, params?:unknown):Promise`;`send(channel,params):void`;`readSnapshot(key:string):T`;`subscribe(channel,listener):()=>void`;`dispose():Promise`。`Lifecycle.enter(action)`、`out(isKill:boolean)`、`detach()`、`dispose()` 管理同一实例。`rendererIpc.ts` 保持原导出签名和 `{event, params}` 回调结构,严格映射允许通道。 + +- [ ] 添加 `bridge.test.cjs`,断言 on/once/off 配对后回调次数分别正确、未知通道 reject、未准备的同步数据抛明确错误而非返回 Promise、重复 enter 不重复初始化、out(false) 不 dispose。运行 `node --test tests/ztools/bridge.test.cjs` 确认失败。 +- [ ] 用 `rg` 穷举 renderer/worker 的 Node、Electron、IPC 依赖,输出“调用方→原处理器→插件处理器→验证任务”映射。处理设置、环境、文件对话框、窗口及业务通道,禁止统一空函数、统一成功或一个无限制 Electron shim。 +- [ ] 实现桥接、初始化顺序和设置服务:在导入原 `src/renderer/main.ts` 前准备桥、环境及必要快照。宿主 onPluginEnter/out/detach/主题回调各注册一次,内部订阅分发;停止对象仅在真实终止时清理。注册主搜索框打开、搜索、播放/暂停、上下首命令,待任务 6 接入播放器。 +- [ ] 完成 web Worker/Node 能力分类:纯算法保留 Worker,网络/文件/数据库经显式服务;下载工厂保留 `LX.WorkerDownloadTypes` 对外契约,直到任务 8 接入前对应功能显示开发状态而不声称可用。 +- [ ] 运行桥接测试、`npm run build:ztools`、`npm run check:ztools`;记录尚待任务 3—10 接通的业务通道。提交:`feat: add ZTools host bridge and lifecycle`。 + +### Task 3: 实现可恢复的结构化存储与 LX 数据契约 + +**Files:** 新建 `ztools/runtime/storage/adapter.cjs`、`ztools/runtime/storage/snapshots.cjs`、`ztools/runtime/storage/lists.cjs`、`ztools/runtime/storage/records.cjs`、`tests/ztools/storage.test.cjs`、`tests/ztools/list-contract.test.cjs`、`tests/ztools/e2e/storage.spec.ts`;修改 `ztools/runtime/settings.cjs`、`src/ztools/contracts.ts`、`ztools/runtime/bridge.cjs`。参考 `src/main/worker/dbService/modules/list/index.ts` 及同目录六个数据模块。 + +**Interfaces:** `Storage.read(namespace:string,key:string):Promise`;`getRevision():Promise`;`commit(changes:StorageChange[],expectedRevision:string|null):Promise`;`subscribe(listener):()=>void`;`close():Promise`。`StorageChange={namespace:string,key:string,value:unknown|null}`,null 表示删除。列表服务保持原列表模块导出名/参数/结果,通过远程适配统一异步;download/dislike_list/lyric/music_url/music_other_source 同样逐方法对照。设置按 revision 暂存并纳入同一发布协议,dbStorage 的普通设置键只作提交后可重建镜像,防止整体导入半成功。 + +- [ ] 添加故障测试:第 N 块写入返回错误而不抛异常、指针冲突、发布前崩溃时,`read` 结果均严格等于旧版本;新版本缺块时不可激活;传给宿主的文档 `_id` 被修改也不污染原对象。运行两份 Node 测试确认失败。 +- [ ] 实现不可变分块和单活动指针发布,复用未变分块,所有分块/目录小于 256 KiB;对结果逐项判错。读取固定 revision,保留上一完整版本和正在读的版本,提交完成才发数据事件。 +- [ ] 实现所有 LX 数据方法及 schemaVersion 迁移。对跨列表移动、批量排序、覆盖、删除、去重、不喜欢、缓存过期分别比较操作前后完整数据;使用含相同歌曲 ID/不同平台的真实形状 fixture,不能靠只测试 CRUD 代替语义对照。 +- [ ] 添加账号切换/远端同步乱序 E2E:旧队列不得写入新上下文;缺块保留旧曲库并重试;冲突不能默默覆盖。按 P0 结论选择已证明后端,必要时本地文件快照实现相同 Storage 接口;账户数据不得被误认作设备私有数据。 +- [ ] 运行 `node --test tests/ztools/storage.test.cjs tests/ztools/list-contract.test.cjs`、`npm run test:ztools:e2e -- --grep storage`、构建检查;用 10,000 首/100 列表验证重启后顺序和内容一致。提交:`feat: persist LX data with recoverable snapshots`。 + +## P2:打通音源与播放 + +### Task 4: 迁移网络与音乐平台适配 + +**Files:** 新建 `ztools/runtime/network.cjs`、`src/ztools/request.ts`、`tests/ztools/network.test.cjs`、`tests/ztools/fixtures/http-server.cjs`、`tests/ztools/e2e/catalog.spec.ts`;修改 `src/ztools/contracts.ts`、构建别名与 `docs/ztools/module-map.md`。参考 `src/renderer/utils/request.js`、`src/renderer/utils/musicSdk`、`src/renderer/worker/main/music.ts`。 + +**Interfaces:** `Network.request(options:RequestOptions):{id:string,result:Promise,cancel:()=>void}`;RequestOptions 覆盖 method/url/headers/body/form/formData/timeout/redirect/proxy;HttpResponse 保留 statusCode/headers/rawBytes 与解析 body。`src/ztools/request.ts` 保持原 `httpFetch` 等实际导出和取消语义,不强迫所有调用方改成新协议。 + +- [ ] 添加受控 HTTP 测试,断言 POST/form/multipart、编码/压缩、代理、超时、取消、重定向与错误码行为;两个并发请求不同 headers 不互相污染,取消后不迟到回调。运行 `node --test tests/ztools/network.test.cjs` 确认失败。 +- [ ] 复用 LX 请求库及平台参数生成,替换不适合 Web 的 Node 入口。检索每个 musicSdk 模块的 crypto/dns/请求依赖,按映射移入受控服务或使用等价实现,保留平台请求和原始歌曲字段。 +- [ ] 接通搜索、分页、热搜/建议与平台切换;fixture 注入只在测试配置生效。为 kw/kg/tx/wy/mg 分别断言转换后的 ID/hash/albumId/音质字段,与原转换结果一致;单平台失败有明确提示且其他平台仍可用。 +- [ ] 运行网络测试、`npm run test:ztools:e2e -- --grep catalog`、构建检查。真实平台连通另记 LIVE 结果,不以网络 fixture 通过冒充。提交:`feat: adapt LX network and music catalog`。 + +### Task 5: 导入、隔离执行和切换自定义音源 + +**Files:** 新建 `ztools/runtime/source/manager.cjs`、`ztools/runtime/source/protocol.cjs`、`ztools/runtime/source/runtime.cjs`、`ztools/runtime/source/guest-preload.cjs`、`ztools/runtime/source/guest.html`、`tests/ztools/source-contract.test.cjs`、`tests/ztools/fixtures/source-api-v2.js`、`tests/ztools/e2e/source.spec.ts`;修改契约、桥接与音源设置页中必须调整的宿主调用。若 P0 选择 Worker,用 `src/ztools/source.worker.ts` 替换两个 guest 文件,并在映射中固定该选择。 + +**Interfaces:** `Sources.importScript(input:{kind:'file'|'url',value:string}):Promise`;`list():Promise`;`activate(id:string):Promise`;`remove(id:string):Promise`;`request(input:LX.UserApi.UserApiRequestParams):Promise`;`cancel(requestKey:string):void`;`getStatus():LX.UserApi.UserApiStatus`;`subscribe(listener):()=>void`;`dispose():Promise`。类型依据 `src/common/types/user_api.d.ts`;响应按 musicUrl/pic/lyric 动作校验。内部所有消息带 generation/requestId,不向脚本暴露宿主服务句柄。 + +- [ ] 依据上游 `src/main/modules/userApi/renderer/preload.js` 写契约 fixture:inited/request/updateAlert、全部支持能力与音质、失败和重复初始化;MD5/AES/RSA/Buffer 同步返回值使用固定输入与上游预期值,inflate/deflate 保持 Promise;HTTP 保持 callback/cancel 语义。运行 Node 契约和真实宿主 `source` 测试确认失败。 +- [ ] 按 P0 已选隔离方式实现 lx API 2.0.0。工具函数不得因跨上下文通信改成异步;脚本不能取得 ztools/Node/本地文件权限。验证无处理器、无限循环、超时、异常脚本退出后 UI/下一音源仍可工作。 +- [ ] 接通本地/URL 导入、信息、启用/删除、更新提示和错误展示;保留上游脚本字符串长度 `9_000_000` 限制,两种入口一致。按原规则解析元信息和源能力,不预置来源不明脚本。 +- [ ] 添加切换测试:A 请求挂起时启用 B,A 的成功/失败/初始化迟到消息均不能改变 B;旧网络取消,删除当前源后明确进入无源状态;request 返回值、音质与 URL 校验失败有准确错误。 +- [ ] 运行 `node --test tests/ztools/source-contract.test.cjs`、`npm run test:ztools:e2e -- --grep source` 和构建检查。记录标准 API 兼容证据及 DOM 扩展脚本限制;用户真实音源未提供时明确待验证。提交:`feat: support isolated LX custom sources`。 + +### Task 6: 恢复原播放器、列表与后台体验 + +**Files:** 新建 `src/ztools/player.ts`、`src/ztools/theme.ts`、`src/ztools/workerServices.ts`、`tests/ztools/e2e/player.spec.ts`;修改 `src/ztools/entry.ts`、`src/ztools/worker.ts`、桥接;按映射最小修改 `src/renderer/core/useApp/index.ts`、`src/renderer/core/useApp/usePlayer/index.ts`、`src/renderer/core/useApp/useEventListener.ts`。参考原 App/router、player、store/list、本地音乐与音效模块。 + +**Interfaces:** `Player.dispatch(command:PlayerCommand):Promise`;`getState():LX.Player.Status`;`subscribe(listener:(state:LX.Player.Status)=>void):()=>void`。PlayerCommand 是 `{type:'play'|'pause'|'playPause'|'prev'|'next'|'collect'|'unCollect'}`、`{type:'seek',value:number}`、`{type:'volume',value:number}`、`{type:'mute',value:boolean}`、`{type:'search',text:string}` 的联合类型;seek 为秒,volume 为 0—1。映射原 `LX.Player.StatusButtonActions` 和状态,不额外创建 Audio 实例,满足任务 10 所有开放 API 控制入口。 + +- [ ] 添加 E2E:导入测试源→搜索→播放本地受控音频,断言 loadedmetadata、currentTime 增长、暂停后稳定、切歌更新;收藏、排序、歌词、音量/进度/播放模式在重启后符合设置。首次实现前运行 `npm run test:ztools:e2e -- --grep player` 确认失败。 +- [ ] 正式入口加载原 Vue 页面及全部路由,接通设置/列表/队列服务;处理 main Worker 内 Node 依赖,不仅修下载 Worker。保留原语言、主题及控件,增加跟随宿主深浅主题,避免透明背景下文字不可读。 +- [ ] 保留 AudioWorklet、音效、媒体会话、本地文件读取/解码与定时停止;使用受控路径服务接入本地媒体,验证中文文件名、FLAC/MP3/WAV 样本。命令搜索和播放控制指向同一播放器。 +- [ ] 验证隐藏、切插件、分离、重新进入不中断;kill 后进程/音频停止,重进恢复数据不自动发声。重复进入不能多注册事件,普通 onPluginOut(false) 不能触发同步/下载清理。 +- [ ] 运行 player E2E、构建检查;在实际宿主确认可听输出并单列人工证据。提交:`feat: restore LX playback and background lifecycle`。 + +## P3:补全业务与下载 + +### Task 7: 排行榜、在线歌单、备份及完整设置 + +**Files:** 新建 `ztools/runtime/backup.cjs`、`tests/ztools/backup.test.cjs`、`tests/ztools/e2e/library.spec.ts`;修改桥接与必要的 `src/renderer/views/Setting/components/SettingBackup.vue` 宿主调用。参考 `src/renderer/router.ts`、musicSdk 的榜单/歌单实现及现有设置组件。 + +**Interfaces:** `Backup.export(kind:'all'|'lists'|'settings',path:string):Promise`;`import(path:string,kind):Promise`;`exportListText(path:string,format:'txt'|'csv'):Promise`。沿用 LX 备份格式、版本迁移、合并/覆盖规则;所有导入先完成解析与校验,再提交任务 3 的持久化事务。 + +- [ ] 添加旧版/当前 LX 备份 fixture,断言导入导出数据等价、损坏/不支持版本不改原数据、失败不留下新设置配旧歌单;运行 `node --test tests/ztools/backup.test.cjs` 确认失败。 +- [ ] 接通榜单分页、歌单详情/链接导入、整单播放/收藏/批量添加、列表编辑与自动更新。全选/跨页选择后数量准确;保留平台关联字段,不能把只有页面导航算作完成。 +- [ ] 实现备份和设置契约,覆盖列表/设置分别备份、全部备份、txt/csv 导出;对设置项逐条映射“有效/由宿主管理/有说明的限制”,禁用无效开关并解释,不删除核心功能以规避适配。 +- [ ] 运行 backup 测试、`npm run test:ztools:e2e -- --grep library`、构建检查,保存兼容矩阵初稿 `docs/ztools/compatibility.md`。提交:`feat: restore playlists charts and LX backups`。 + +### Task 8: 迁移下载执行器、续传与标签 + +**Files:** 新建 `ztools/runtime/download/queue.cjs`、`ztools/runtime/download/transfer.cjs`、`ztools/runtime/download/metadata.cjs`、`tests/ztools/download.test.cjs`、`tests/ztools/e2e/download.spec.ts`;修改 `src/ztools/worker.ts`、契约、桥接。参考 `src/renderer/worker/download`、`src/common/utils/download`,按原模块映射保留命名及歌词封面行为。 + +**Interfaces:** 对 UI 保持 `LX.WorkerDownloadTypes`;内部 `Downloads.start(task)`、`pause(id)`、`cancel(id)`、`retry(id)`、`restore():Promise`、`dispose():Promise`,异步方法返回 Promise;进度使用原下载事件字段,记录写入任务 3 的 download 数据服务。 + +- [ ] 添加本地服务器故障测试:206 正常续传、200 忽略 Range、Content-Range 错位、ETag 变化、无长度、半途中断、取消、地址过期。成功文件 `sha256` 等于预期;失败/取消无 completed 事件。运行 `node --test tests/ztools/download.test.cjs` 确认失败。 +- [ ] 将 Node 网络/文件执行移到 preload 服务,保留队列/并发/暂停/重试/进度;临时文件按任务隔离,校验成功后才发布。对失效 URL 重新向当前有效音源解析,明确重试上限,切源不能恢复旧会话请求。 +- [ ] 接通目录选择、文件名清理/同名策略、目录无权限、歌词/封面及标签写入;验证写入后仍可解码且 metadata 可读。若使用 runFFmpeg,单独检测并展示可用性,不假定所有宿主均内置该能力。 +- [ ] 验证隐藏继续、暂停重启后可恢复、kill 后残留任务为可重试状态;任务删除不得误删用户其他文件。跨设备/资源变化不能拼接旧文件。 +- [ ] 运行 download 契约与 E2E、构建检查,检查播放并行下载时 UI/音频未被同步 IO 阻塞。提交:`feat: port resumable downloads and metadata`。 + +## P4:同步和系统集成 + +### Task 9: 保留 LX 同步客户端与服务端 + +**Files:** 新建 `ztools/runtime/sync/index.cjs`、`ztools/runtime/sync/events.cjs`、`ztools/runtime/sync/storage.cjs`、`tests/ztools/sync.test.cjs`、`tests/ztools/e2e/sync.spec.ts`、`tests/ztools/fixtures/lx-sync-peer.cjs`;按 `docs/ztools/module-map.md` 将 `src/main/modules/sync/client`、`server`、`utils.ts`、`migrate.ts` 逐模块适配到 `ztools/runtime/sync/` 同名可读 CJS,更新契约和桥接。不把 Electron 主入口一起复制。 + +**Interfaces:** `Sync.start(config:SyncConfig):Promise`;`stop():Promise`;`getStatus():SyncStatus`;`subscribe(listener:(status:SyncStatus)=>void):()=>void`。SyncConfig 为 `({mode:'server'} & LX.Sync.EnableServer) | ({mode:'client'} & LX.Sync.EnableClient)`;SyncStatus 为 `({mode:'server'} & LX.Sync.ServerStatus) | ({mode:'client'} & LX.Sync.ClientStatus)`。其余设备管理/鉴权/冲突方法按原模块导出映射;列表/不喜欢事件在任务 3 提交成功后发布。 + +- [ ] 固定协议 fixture 并测试鉴权失败、首次同步、增删改排序、不喜欢列表、设备移除、冲突选择、断线重连;重点断言“本地排序同时重连后双方内容/顺序一致、无重复、无无限回推”。运行 `node --test tests/ztools/sync.test.cjs` 确认失败。 +- [ ] 复用原协议/消息/快照算法,注入配置、日志、事件和存储;移除 main_window_close 对插件隐藏的错误联动。数据提交失败不能通知远端成功,重新配置先关闭旧 socket/端口。 +- [ ] 使用一个未修改协议实现的 LX 对端,固定版本/commit并隔离其数据。插件做客户端与服务端各运行一次互通测试,覆盖认证与双向变更;测试程序只负责启动和驱动对端,不能重写对端协议来配合本插件。 +- [ ] 验证默认不监听、设置开启才监听,端口冲突明确报错、隐藏维持连接、kill 释放监听。缺少真实对端条件时记 BLOCKED,不能以两个插件副本互通替代。 +- [ ] 运行 sync 单元/E2E及构建检查,把对端版本和最终双方数据摘要写入兼容矩阵。提交:`feat: preserve LX sync client and server protocol`。 + +### Task 10: 开放 API、快捷键及桌面歌词 + +**Files:** 新建 `ztools/runtime/open-api.cjs`、`ztools/runtime/windows.cjs`、`src/ztools/lyric-entry.ts`、`tests/ztools/open-api.test.cjs`、`tests/ztools/e2e/system.spec.ts`;修改 plugin.json、桥接、构建配置;参考 `src/main/modules/openApi/index.ts`、`src/main/modules/winLyric`、`src/renderer-lyric`、`src/main/modules/hotKey`。 + +**Interfaces:** `OpenAPI.start(config:LX.OpenAPI.EnableServer):Promise`、`getStatus():LX.OpenAPI.Status`、`stop():Promise`;`Windows.openLyrics():Promise`、`closeLyrics():Promise`;都通过前述 Player/Storage/Lifecycle 订阅,禁止另建播放器。窗口调用按宿主代理实际异步行为归一化。 + +- [ ] 添加开放 API 测试,比较原有 status/lyric/SSE/播放控制响应;错误配置、端口冲突和停止后连接关闭必须明确可测。运行 `node --test tests/ztools/open-api.test.cjs` 确认失败。 +- [ ] 迁移开放 API 并绑定插件生命周期;默认关闭,不抢占桌面 LX 的现有端口;重进/设置变更不能重复监听,SSE 客户端断开及时移除订阅。 +- [ ] 将快捷键管理交给 ZTools 插件指令和 `redirectHotKeySetting`,支持启动/搜索/上下首/暂停;原应用退出映射退出插件,自更新与独立托盘映射宿主职责。 +- [ ] 复用原桌面歌词页面,实测子窗口置顶、鼠标穿透、尺寸/显示器/关闭。媒体键、任务栏按钮逐项验收:仅公开 API 能可靠完成的标支持,否则界面说明具体限制并写入矩阵;这些宿主差异不允许悄悄吞掉核心音乐功能。 +- [ ] 运行 open-api 测试、`npm run test:ztools:e2e -- --grep system`、构建检查;确认没有调用宿主 internal IPC。提交:`feat: integrate LX system controls with ZTools`。 + +## P5:形成可安装交付物 + +### Task 11: 独立运行包、完整回归和中文文档 + +**Files:** 新建 `build-config/ztools/package.cjs`、`tests/ztools/package.test.cjs`、`tests/ztools/e2e/acceptance.spec.ts`、`docs/ztools/README.md`、`docs/ztools/upstream-diff.md`;更新 runtime 锁文件、`docs/ztools/host-capabilities.md`、`docs/ztools/compatibility.md`、构建检查与 scripts。保留并组装 `LICENSE`、第三方 notices。 + +**Interfaces:** `npm run pack:ztools` 只产生 `dist-ztools/` 和版本化 zip;包根 plugin.json 指向本地 HTML/可读 preload,所需 Node 依赖由独立运行清单和锁文件完整装入,不依赖源码根 node_modules,不执行发布。 + +- [ ] 添加 package 测试:缺少传递依赖、主资源、语言/字体/AudioWorklet、源码许可时构建检查失败;preload/runtime 保持逐模块可读 CJS,没有 better-sqlite3 二进制或整套 LX 主进程。运行 `node --test tests/ztools/package.test.cjs` 确认失败。 +- [ ] 实现可重现组装/zip,记录插件版本、LX 基线、宿主验证版本、依赖清单与校验值。打包拷贝范围显式声明,不递归拷贝工作区或用户配置;前端可压缩,preload/runtime 不压缩/混淆。 +- [ ] 将包解到新的临时目录,断开源码目录与开发 node_modules 引用,在隔离真实宿主导入该包。从首次运行完成音源→搜索→播放→隐藏→歌单→下载→重启→备份→同步→开放 API,确认无额外 LX 进程依赖。 +- [ ] 稳定性验证:10,000 首/100 列表,连续 30 分钟后台播放与下载,50 次进入/退出/分离;保存监听器/音频实例/进程数量与资源趋势。检验受控音频时间推进、人工可听、下载哈希、重启数据、真实对端同步;发现泄漏或中断则回归责任任务。 +- [ ] 运行 `npm run test:ztools`、`npm run check:ztools`、`npm run build:ztools`、`npm run pack:ztools`、完整 E2E。根据修改范围验证原桌面 renderer 构建未被破坏;不为无关平台反复运行全量打包。 +- [ ] 完成中文安装、音源导入、数据迁移、后台/终止区别、下载目录、同步配置、排错与卸载说明;矩阵逐项注明 PASS/FAIL/BLOCKED、证据层级、目标版本,真实音源缺失保持待验证。提交:`docs: deliver validated ZTools plugin package and guide`。 + +## 阶段完成判定和计划自查 + +| 阶段 | 必须交付的可检查结果 | 对应任务 | +|---|---|---| +| P0 | 宿主、音源隔离和存储可行性实证;不能只靠源码推断 | 1 | +| P1 | 依赖/通道映射、统一生命周期、可恢复数据服务 | 2—3 | +| P2 | 原 UI、音源 API 契约、搜索到播放及后台生命周期 | 4—6 | +| P3 | 榜单/歌单、备份、完整下载与设置映射 | 7—8 | +| P4 | 原 LX 对端互通、开放 API、系统能力与差异清单 | 9—10 | +| P5 | 脱离开发目录可运行的包、完整回归与中文交付材料 | 11 | + +本计划已按设计逐项检查:核心模块都有任务归属;五项 Review Focus 都有故障测试;跨任务统一使用 Storage/Network/Sources/Player/Lifecycle 契约。任务 1 的实验可以改变宿主适配实现,但不能缩小功能范围;变化须同步更新设计、契约和后续任务。 + +最终报告必须把“已实现”“受控测试通过”“真实宿主通过”“真实外部平台/用户音源通过”分开。计划中没有保证任意第三方脚本和所有平台永久可用,也没有把尚未实现的测试当作当前成果。 diff --git a/plugins/lx-music-ztools/docs/superpowers/specs/2026-11-09-ztools-native-port-design.md b/plugins/lx-music-ztools/docs/superpowers/specs/2026-11-09-ztools-native-port-design.md new file mode 100644 index 000000000..d7a097975 --- /dev/null +++ b/plugins/lx-music-ztools/docs/superpowers/specs/2026-11-09-ztools-native-port-design.md @@ -0,0 +1,210 @@ +# LX Music 原生 ZTools 插件移植方案 + +日期:2026-11-09 + +版本:第二版,依据全部 14 页 ZTools 文档与本地源码重新核对。状态:设计与实施规划,尚未开始产品代码实现;文中的验证门槛是待完成工作,不表示已通过。 + +实施修订(2026-11-09):用户指定在现有独立仓库 `D:\user\Documents\基础项目\lx-music-ztools` 实施。P0 已在隔离的已安装 app.asar + Electron 42.3.3 组合通过;细项见 `docs/ztools/host-capabilities.md`。当前不是完整移植交付,已实现/待验证范围见 `docs/ztools/compatibility.md`,下文未完成的门槛仍为待办。 + +P0 后固定的实现取舍:原 LX 源码作为 vendor/lx 只读基线,产品界面使用独立 Webpack,现有 Vite 保留诊断用途。音源使用可信 loopback frame 启动的 CSP 浏览器 Worker,脚本不进入 frame/插件页面;不采用未能初始化的 guest。结构化数据使用已经实测的 pluginData + 内置 node:sqlite,保留原 LX SQL 数据模型和事务,不附带 better-sqlite3;原“host db 不可变分块 + 活动指针”方案保留为背景比较,当前不实施,256 KiB 分块要求仅适用于未来 host db 方案。存储不跟随 ZTools 云账号切换,设备本地曲库由显式 LX 同步/备份迁移。运行时必须检测 SQLite 可用性,不对未验证宿主声明兼容。 + +## 需求与已确认选择 + +用户要求将 `https://github.com/lyswhut/lx-music-desktop.git` 参考 ZTools 官方插件规范移植,仓库允许拉取到当前工作目录。 + +用户补充并确认: + +- 必须支持导入、使用 LX 自定义音源。 +- 尽量完整移植,同时保留下载、排行榜、歌单、同步等功能。 +- 采用原生插件移植,在 ZTools 内运行,不依赖另装 LX 桌面版。 + +本方案不将核心播放器演示视为完整交付。各模块可以分阶段实现,但最终应提供功能状态清单,未完成项不能标为支持。 + +## 当前源码证据 + +- LX 克隆路径:`D:\user\Documents\基础项目\lx-music-desktop`。 +- 上游基线:`323e4bac`,master,package.json 版本 `2.12.6`。 +- LX 使用 Vue 3、TypeScript、Pug、Less、Webpack,渲染入口是 `src/renderer/main.ts`。 +- `src/common/rendererIpc.ts` 与 `src/renderer/utils/ipc.ts` 封装了前端到 LX 主进程的通信;ZTools 不会注册这些业务 IPC 通道,不能直接复用原通信。 +- 搜索、排行榜、在线歌单在 `src/renderer/utils/musicSdk`;自定义音源独立于搜索,主要提供播放地址,不能只移植音源脚本运行器。 +- 自定义音源接口版本为 `2.0.0`,原实现位于 `src/main/modules/userApi/renderer/preload.js`,有事件、网络、加密、压缩等能力。 +- LX 主进程还负责配置、列表数据库、同步、系统窗口等;数据库使用带 Electron ABI 约束的 `better-sqlite3`。 +- 本机 ZTools 源码位于 `D:\user\Documents\code\ztools`,本轮核对 HEAD `76addc6`、package 版本 `3.2.0`,用于只读参考及后续隔离实例验证,不等同于用户已安装客户端。本项目不修改 ZTools 主仓库。 +- 已查看的 ZTools 插件视图和子窗口代码固定 `nodeIntegration: false`、`contextIsolation: false`、`webSecurity: false`。不能将音源直接注入插件特权页面,也不能假定传入 BrowserWindow 选项就能恢复 LX 的隔离边界。 +- LX 的 `src/renderer/worker/download` 使用 Node 文件/网络模块;移植时不能继续依赖普通 Web Worker 拥有这些能力。 +- LX 的列表/歌曲/排序使用 SQL 事务;ZTools `storageRouter.ts` 的 `bulkDocs` 可以逐条 put,不能假设跨文档回滚。当前 `lmdb/syncApi.ts` 的 put 有 1 MiB 文档检查,并可能返回错误结果对象而不抛异常。 + +## 移植路线比较 + +### A. 原生插件移植(用户已选择) + +复用 LX 界面、音乐平台适配、播放、歌词、列表、下载、同步协议等模块,新增 ZTools 宿主适配层,替换独立应用生命周期及主进程通信。使用时只需要 ZTools。 + +代价是必须完成真实的宿主、存储、音源隔离和同步适配,不能仅添加 plugin.json。此路线符合用户要求,也使后续对齐上游改动成为可能。 + +### B. LX 桌面版配套控制插件(未选) + +通过 LX 开放 API 或调用桌面程序保留其功能,移植量较小,但仍需单独运行 LX,不满足用户选择。 + +### C. 重写轻量播放器(未选) + +新写小型界面与少量搜索、播放模块,启动快,但需要重新补齐下载和同步等行为,容易形成另一套产品,不符合尽量完整保留的目标。 + +## 交付形态与技术决定 + +以下为实施者建议,非用户额外指定: + +- 在克隆仓库内开发 `codex/ztools-native-port` 分支,保留原桌面版入口;插件构建使用独立配置和入口。 +- 尽量沿用 LX 现有 Vue 界面、导航与数据结构,对 ZTools 的小窗口适配布局,并支持宿主分离窗口。 +- 插件 ID 暂定 `lx-music`,显示名暂定 `LX Music`,与 ZTools 插件中心已有名称冲突时再调整,不自动发布。 +- 插件包包含 plugin.json、PNG 图标、页面资源、可读的 CommonJS preload 及其依赖源代码、许可与上游说明。 +- 新增 preload 与其依赖不做打包、压缩或混淆,遵守 ZTools 规范;普通前端资源可以走构建流程。 +- Windows x64 是首个验证目标;首包顶层 `platform` 声明 `['win32']`,这是实施建议。macOS/Linux 保持可移植代码,发布前另行验证。 +- 构建环境沿用 LX 的 Node >=22 要求;插件运行时以实际 ZTools 的 Electron/Node 能力为准。文档中的 Node 16.x 描述和本机命令行 Node 版本都不能代替运行时检测。 +- 保留 Webpack,插件使用独立 web 构建目标与相对资源路径,最终输出 `dist-ztools/`。不因为插件化更换前端框架。 + +## 架构与模块边界 + +### 1. 前端复用与宿主桥接 + +插件构建以现有 renderer 为基础,新增插件启动初始化。利用构建别名将 rendererIpc 等宿主相关模块换成插件实现,保留音乐业务界面。 + +提供请求、发送事件、订阅和取消订阅的统一桥接契约,维护 LX 原来的参数和返回值结构;同步 IPC 只读已准备的数据,不把 Promise 伪装成同步结果。桥接只调用插件服务,不将 LX 私有 IPC 发给 ZTools 主进程,不覆盖 ZTools 内部通道。 + +UI 入口使用 `onPluginEnter`,不用 `window.exports + mode: 'none'`。进入/退出/分离窗口/主题回调统一注册一次,再内部订阅分发;宿主单回调接口不能被各业务模块重复覆盖。保留 LX 主题选择,并增加“跟随 ZTools”,适配透明材质及深浅主题。前端只获得必要业务服务,不获得完整 fs、child_process、ipcRenderer 或任意 require。 + +整理当前 renderer 使用的 Node/Electron 入口,按类别决定保留在可读 preload 中、通过显式服务暴露、或替换为 ZTools API。不能仅把构建成功当作运行兼容。 + +### 2. 插件服务与持久化 + +设置使用 `dbStorage`,结构化数据优先使用 `db`,下载音频存入用户选择目录;脚本正文使用附件或分块,备份由显式导出写入。`getPath('pluginData')` 是本机源码可见、公开文档未详述的能力,仅作为缓存路径在前置验证中核实,基础结构化存储不再把它设为唯一前提。迁移通过显式导入 LX 备份进行,不自动打开原 LX 数据库。 + +保留 LX 数据服务契约,替换 SQL 后端。必须覆盖列表、歌曲、排序、批量变更、去重、不喜欢列表、下载记录、歌词/URL 缓存及同步快照,不能将接口替换成空返回。避免捆绑绑定单个 Electron ABI 的 better-sqlite3。 + +跨文档修改使用“不可变数据分块 + 单一活动版本指针”发布:先串行写齐新分块并逐项检查结果,再使用当前 `_rev` 更新活动指针。发布失败时旧版本仍可读;读者固定一个版本读取。保留上一完整版本,垃圾清理不得删除活动/上一版本和正在读取的版本。不能用 bulkDocs 代替事务成功判定。 + +实施建议:每块序列化 UTF-8 小于 256 KiB,清单/目录也分块,避免接近已知 1 MiB 边界。唯一写入服务接收 UI、下载、同步修改,提交成功后才发业务事件。宿主账号/存储切换时刷新上下文;提交前重新校验存储根标识,避免旧缓存写入新存储。ZTools 自身存储同步与 LX 协议同步分别处理,不视为互相替代。 + +存储适配器传给宿主的是文档副本,避免其为 `_id` 加前缀时污染应用对象。分块复用未变内容,不能每次播放进度更新都重写整个曲库。活动指针的 `_rev` 冲突判定、宿主账号切换与异步同步导致的“指针先到、分块后到”均属于 P0/P1 验证项:不完整版本不可激活,保持上一完整版本;不能依赖提交前的一次身份检查解决所有竞态。若公开接口无法绑定写入上下文,须在 P0 决定使用可验证的插件本地存储后端或限制存储切换期间写入,并说明行为;不得宣称未经证明的跨账号原子性。 + +首次运行不得触碰用户真实配置。验证使用独立临时数据目录。导入失败不得覆盖有效列表与设置。 + +全部备份的恢复必须同时覆盖设置和曲库:设置也按 revision 暂存,只有活动指针发布后才生效;普通 dbStorage 设置键可作为可重建镜像,不能让其提前生效造成新设置与旧曲库混用。 + +### 3. 音源兼容与隔离 + +支持本地 `.js` 和用户给定 HTTP(S) URL 导入、脚本信息查看、启用/切换/删除、初始化状态、音质选择及更新提示;保留现有 LX 导入大小约束并统一两种入口行为。 + +按 LX `globalThis.lx` API 2.0.0 实现: + +- `EVENT_NAMES`、`on`、`send`、`version`、`env`、`currentScriptInfo`。 +- `inited`、`request`、`updateAlert` 事件及其参数校验。 +- `request(url, options, callback)`,包括请求方法、头、body/form/formData、超时、取消和响应对象。 +- Buffer 转换、MD5、AES、RSA、随机数、inflate/deflate;行为对照上游,而非仅保留同名函数。 +- `kw/kg/tx/wy/mg/local` 的能力和音质声明,播放 URL、歌词、封面返回值校验。 + +音源运行环境是第一项技术验证。首选验证独立非持久化 Session 的 guest WebContents/webview,保留 LX 的 `contextIsolation:true + 受限 contextBridge` 模式与同步加密工具。本机宿主源码开放 webview,但公开文档未承诺,必须实测 guest 实际配置、宿主 preload 是否注入、导航/弹窗/权限限制,以及失控脚本可终止性。 + +上一版直接选择 sandbox iframe 的依据不足:宿主关闭 webSecurity,且普通消息桥不能直接保持 crypto/Buffer 的同步返回。若 guest 无法建立隔离边界,备选独立浏览器 Worker + 同步 JS 加密/Buffer 实现;只使用标准 LX API 的脚本必须兼容,依赖 DOM 的脚本另做验证并明确限制。两种方式均不可行时先修订此设计,不改为特权 eval、Node vm 或另启 LX 桌面版。 + +音源不能访问 ztools、任意 Node 模块、插件存储或本地文件。crypto/Buffer 同步返回、HTTP 回调/取消和 zlib Promise 语义均按上游保持。每次启用生成会话代号,请求带 requestId;切源时取消旧请求并忽略迟到结果,脚本退出销毁上下文。初始化超时和失控脚本由上下文外部控制器终止。不能预先保证任意第三方脚本全部兼容。 + +超时、无 request 处理器、重复初始化、未知音质和失效 URL 都应显式显示失败,不伪造音源可用。 + +### 4. 搜索、榜单、歌单、歌词及播放 + +尽量复用 musicSdk 中现有平台模块及结果转换,保留源需要的歌曲 ID、hash、albumId 等原始字段,以兼容不同音源。实际搜索网络请求通过 preload 网络服务执行,避免浏览器跨域及禁止请求头限制。 + +保留搜索、分页、平台切换、排行榜、歌单浏览/导入、收藏及列表编辑、播放队列、播放模式、音量/进度和歌词显示。平台 API 故障与插件适配错误分别记录;单平台失败不得使整个界面无法启动。 + +复用现有音频引擎、AudioWorklet、歌词、音效及本地播放;验证中文路径及实际解码。`pluginSetting.backgroundRunning: true` 和 single 是本机源码可见能力,先验证再用于 manifest。隐藏、退出到后台、切换插件继续播放/下载/同步;强制终止停止。分离窗口和再次进入只保留一个播放器。数据在业务提交时持久化,不依赖强杀前一定能完成异步保存。再次打开恢复数据,不擅自自动发声。 + +### 5. 下载 + +保留添加任务、目录选择、并发/队列、暂停/取消/重试、进度、失败原因和记录恢复,复用原命名、歌词/封面及标签写入能力。下载从依赖 Node 的 renderer Worker 迁入 preload 服务,异步流式处理;纯计算 Worker 可以保留,密集转换按性能证据再迁入受控 Node Worker,不全局开启 Web Worker 的 Node 权限。使用任务专属临时文件,校验完成后再发布到目标路径。 + +处理无效文件名、同名文件、写权限、无 Content-Length、网络中断、重定向和取消。续传必须验证状态码、Range/Content-Range 与资源标识;不支持或资源变化时重下,不拼接损坏文件。未知长度显示已下载量;音源地址失效重新解析须有重试上限。网络头按请求管理,不全局污染宿主 Session。 + +下载位置由用户选择,插件内部数据与下载文件分开。杀死插件后任务在下次启动可恢复为可重试状态,不显示虚假完成。 + +### 6. 同步及开放 API + +复用 LX 现有同步服务端/客户端协议、鉴权、列表及不喜欢列表事件、设备管理与冲突选择。通过插件服务初始化模块需要的事件总线和持久化契约,替换其中的 Electron 主进程依赖。 + +保留局域网同步和连接 LX 同步服务的能力;关闭同步、重新配置和终止插件时关闭 socket/监听端口。默认不开放监听端口,用户在设置中开启后才启动。 + +开放 API 复用现有接口行为并由插件生命周期管理,不擅自占用 LX 桌面版已经使用的端口;端口冲突给出明确提示。 + +### 7. 系统功能与宿主生命周期 + +安装器、桌面应用自更新、独立托盘与应用退出不适合原样套用:自更新交给 ZTools;原退出动作映射到退出/终止插件,不能退出 ZTools。窗口操作映射到宿主能力。 + +桌面歌词、全局快捷键、任务栏按钮、媒体会话与音效等逐项做能力映射。窗口代理只使用实际支持的方法并处理异步返回;播放快捷键使用插件指令及 `redirectHotKeySetting` 交给 ZTools 管理。宿主未提供且无法可靠实现的功能在状态清单中注明并在 UI 中解释,不留下可点击但无效的控件。不调用宿主 internal API。 + +ZTools 版本及宿主能力在启动时检查,缺少必要 API 显示明确错误。主搜索框命令支持打开播放器、搜索、播放/暂停、上一首、下一首,并保持一个播放实例。 + +## 验收与证据 + +最终交付内容:插件源码、独立构建命令、可加载的插件目录及压缩包、中文安装/使用说明、上游归属及许可、功能兼容与验证状态清单。 + +验证分层,不能混称: + +1. 静态/构建:插件配置、资源路径、可读 preload、依赖包含、前端构建和必要类型检查。 +2. 自动契约与回归:音源事件、加密/压缩、网络/取消、存储持久化、列表排序、下载中断/续传、同步协议及冲突处理。 +3. 隔离 Electron/ZTools 集成:Playwright Electron 启动源码宿主,使用 `ZTOOLS_DATA_ROOT`、`ZTOOLS_LEGACY_USER_DATA_PATH` 临时目录,加载实际插件验证导航、导入音源、搜索到播放、后台继续、下载、重新进入及强制退出;只截插件 WebContentsView。同步至少与一个未修改的 LX 同步实现互通,不能仅用两个移植插件证明协议兼容。 +4. 真实外部平台与用户音源:仅在具备有效音源和环境时记录实际结果。用户尚未提供真实音源,不自动搜索或预置第三方音源;可用本地测试音源及受控音频服务器验证完整链路,但必须注明不代表用户真实音源可用。 + +受控 E2E 使用本地测试音频:验证 loadedmetadata、播放时间推进、暂停/切歌和后台继续,另在实际客户端确认可听输出。下载验证文件哈希;真实平台只做必要低频验证。任何 BLOCKED 都不能当作 PASS。 + +规模/稳定性验收建议:10,000 首歌曲、100 个列表、连续 30 分钟后台播放和 50 次进入/退出/分离往返,检查监听器/资源增长、排序及重启结果。这些数值为实施建议。安装包还应在脱离源码根目录与开发 node_modules 后加载,避免遗漏运行依赖。 + +完成标准不是所有按钮可见,而是目标模块有真实实现和对应证据;如果某项因宿主或外部服务受限,需明确指出,不能将残缺版本称作完整移植。 + +## 实施阶段 + +```mermaid +flowchart TB + UI[保留 LX 的 Vue 界面与播放引擎] --> Bridge[ZTools 业务桥接与生命周期] + Bridge --> Host[ZTools 公开 API] + Bridge --> Service[可读 CommonJS 服务:存储 / 网络 / 下载 / 同步] + Service <--> Source[隔离音源运行环境:LX API 2.0.0] + Service --> Data[插件数据与用户下载目录] +``` + +| 阶段 | 交付与检查 | 依赖 | +|---|---|---| +| P0 宿主可行性 | 最小原 Vue 入口;音源隔离/同步工具;隐藏/分离/终止;存储返回值、大小边界及半批失败;实际运行时版本 | 无 | +| P1 基础适配 | IPC/Node 依赖清单、统一桥、生命周期、存储一致性、主题及全部路由 | P0 | +| P2 音源与播放 | 本地/URL 音源、搜索、播放、歌词、音效、本地文件、队列、收藏及后台播放 | P1 | +| P3 业务补全 | 排行榜、在线歌单、批量导入、下载/续传/标签、备份与相关设置 | P2 | +| P4 同步与系统集成 | 原 LX 协议互通、开放 API、快捷键、桌面歌词和差异说明 | P3 | +| P5 交付 | 隔离真实宿主全链路验证、完整依赖运行包、中文说明和状态清单 | P4 | + +各阶段可单独验证,但不以 P2 作为最终交付。P0 失败必须记录证据并修订技术方案,不能通过 mock 成功隐藏宿主限制。实施计划见 [任务清单](../plans/2026-11-09-ztools-native-port.md)。 + +## 构建与发布边界 + +preload 和运行服务以清晰 CommonJS 源码交付,不 bundle/minify/混淆。需复用的上游 TS 主进程模块按原模块边界适配为可读 CJS,保留来源、版权和差异清单;不打包整个 LX 主进程。前端正常编译,Node 运行依赖及传递依赖在 preload 可解析位置组装,并带锁文件。 + +生产入口为本地 HTML,开发 URL 仅用于 development。插件版本与 LX 桌面 package 版本分别管理。`dist-ztools/` 和压缩包用于本地安装;`ztools publish` 是复制源码并产生远端 fork/提交/PR 的流程,不是本地打包命令,本轮不调用。保留 LICENSE、上游归属及第三方 notices;不自动改变许可证或添加支付/AI 功能。 + +## 功能完成标准 + +搜索/播放/音源、原界面与设置、列表/备份、榜单/歌单、下载、同步、开放 API 是正式交付目标。缺失任何一项须明确列为未完成;宿主特有的托盘、安装、自更新由 ZTools 接管。桌面歌词的置顶/穿透、任务栏、媒体键等单列宿主兼容性,不用无效控件冒充支持。 + +音源不是 ZTools Provider;后者目前用于翻译/OCR。ZTools AI、支付与服务端订单 API 已阅读,但不属于本次音乐移植范围。 + +## 尚由实施者决定 + +具体目录与桥接函数命名、按现有模块拆分的实现顺序、测试工具、默认窗口尺寸、样式微调及依赖版本由实施者结合实际代码决定。上述决定不得缩减用户确认的功能范围。 + +## 参考资料 + +- https://ztoolscenter.github.io/ZTools-doc/first-plugin.html +- https://ztoolscenter.github.io/ZTools-doc/plugin-json.html +- https://ztoolscenter.github.io/ZTools-doc/preload-js.html +- https://ztoolscenter.github.io/ZTools-doc/plugin-api.html +- https://lxmusic.toside.cn/desktop/custom-source +- 克隆仓库内 LICENSE、README.md 及上述源码。 + +本轮还完整阅读了 getting-started、file-structure、node-js、plugin-payment、developer-server-api、provider-development-guide、ai-api、ai-plugin-guide、vibe-coding、publish-and-update 页面,共 14 页;阅读副本位于父工作目录 `tmp/ztools-docs-20261109`。文档有入口必填项、feature.platform 和发布是否重放提交等描述差异,按专项页面和目标运行时核对。`pluginData`、backgroundRunning/single、webview 目前只有本机源码证据,不视为所有版本的公开保证。 diff --git a/plugins/lx-music-ztools/docs/ztools/compatibility.md b/plugins/lx-music-ztools/docs/ztools/compatibility.md new file mode 100644 index 000000000..5cb7c0bf7 --- /dev/null +++ b/plugins/lx-music-ztools/docs/ztools/compatibility.md @@ -0,0 +1,25 @@ +# 当前移植与验证状态 + +更新:2026-11-10,0.0.2 开发版。此表记录实施中的实际结果,不是最终版本功能承诺。 + +| 模块 | 当前结果 | 证据 | +|---|---|---| +| P0 生产入口、生命周期、存储能力 | PASS,特定宿主/运行时组合 | `npm run test:p0`;artifacts/p0/report.json | +| 原 LX 数据模型 | 已迁入原列表/下载记录/不喜欢/歌词/URL/换源数据模块 | 设备本地 SQLite;列表及事务回滚已验证,其他数据契约继续补测 | +| 原界面构建 | PASS,独立 Webpack web target | `node scripts/build-lx.mjs` | +| 搜索页、基本设置页 | PASS,真实宿主导航及可见区域截图 | `node scripts/test-lx-renderer.mjs`;artifacts/renderer/settings.png | +| 插件布局 | PASS,去掉 LX 缩小/关闭控件及桌面留白/圆角/阴影,铺满插件视图;播放详情返回按钮保留 | renderer 边界断言;player 详情页控件验证 | +| 自定义音源 API 2.0.0 | 已实现隔离执行、初始化、HTTP 回调、同步 crypto/Buffer/zlib、能力与结果校验 | P0 与 source 集成验证 | +| 导入及使用测试音源 | PASS,导入原始脚本→启用→解析播放 URL;未知音质拒绝;切源取消旧请求 | `node scripts/test-source.mjs`;只使用本地可控脚本/HTTP 服务 | +| 原界面本地文件/URL 导入操作 | PASS,两种入口均已通过 | `node scripts/test-source-ui.mjs`;文件选择器返回受控路径,其余为实际 UI、宿主 API 与文件服务 | +| 用户真实音源/真实平台搜索 | 未提供真实音源,未全量验证外部服务 | 不预置第三方源,不以本地 fixture 结果代替 | +| 音频解码、实际播放与后台播放 | PASS,原 LX 引擎播放受控 WAV,进度、暂停/继续、后台及下一首 | `node scripts/test-player.mjs`;不替代真实平台及人工听音验收 | +| 排行榜、在线歌单、备份 | 原模块已随上游源码迁入,业务适配/回归待完成 | 不算支持完成 | +| 下载与续传/标签 | 待完成执行器移植 | 下载记录 SQL 模块可用不等于下载已可用 | +| LX 同步、开放 API、桌面歌词、快捷键 | 待实现和独立验证 | 不算支持完成 | +| 可安装开发包 | PASS,ZIP 安装后搜索 lxmusic 并打开原界面,无开发服务器 | `node scripts/test-install.mjs`;artifacts/install/report.json;完整功能仍在移植,不能称为最终包 | +| 插件中心源码构建准备 | PASS,本地复现 CLI 源码筛选后在新目录以 Node 20.20.2 安装、构建、打包并验证全部 6 个运行依赖落在插件目录内 | artifacts/fresh-build.json;不代表仓库方 GitHub Actions 已批准或已通过 | + +验证环境:ZTools app.asar `3.3.0-beta.2` + 独立 Electron `42.3.3`、Chromium `148`、Node `24.15.0`,使用随机临时数据目录。源代码宿主与安装包自带运行时的兼容比较仍需单列,不能把该组合推广为所有 ZTools 版本。 + +独立测试宿主冷启动曾出现主页面 `window.ztools` 未注入、搜索 API 不可用,发生于插件页面加载前,原因尚未定位。最终串行 ZIP 安装、界面、音源和播放验证成功;不据此宣称宿主冷启动稳定性问题已经解决。 diff --git a/plugins/lx-music-ztools/docs/ztools/host-capabilities.md b/plugins/lx-music-ztools/docs/ztools/host-capabilities.md new file mode 100644 index 000000000..c4d673d1a --- /dev/null +++ b/plugins/lx-music-ztools/docs/ztools/host-capabilities.md @@ -0,0 +1,29 @@ +# P0 宿主可行性验证 + +2026-11-09。运行命令:`npm run build`、`npm run test:p0`。 + +验证宿主为本机已安装 `ZTools 3.3.0-beta.2` 的 app.asar,使用独立 Electron 42.3.3 / Chromium 148 / Node 24.15.0 启动,全部配置写入随机临时目录。本轮不是本机 3.2.0 源码宿主验证,也不证明其他版本兼容。结果与完整运行时信息保存在 `artifacts/p0/report.json`,不提交临时目录和可执行文件。 + +| 检查 | 结果与证据 | +|---|---| +| 生产资源 | 无开发服务器,从复制后的完整插件目录加载;原 LX Btn.vue 和 Less 正常渲染 | +| 源 guest | 不采用;实际 webPreferences 有 webviewTag,但所创建元素缺少 webview 方法,8 秒内没有生成 guest WebContents | +| 源 Worker 备选 | PASS;可信 loopback frame 创建同源浏览器 Worker;HTTP 响应 CSP 阻止直接网络/文件访问和子 Worker,不在插件页面执行音源代码 | +| 同步工具 | MD5 固定向量、AES-128-CBC 固定向量、1024 位 RSA_NO_PADDING 与 Node 独立结果相同;中文 inflate/deflate 往返正常 | +| 权限边界 | Worker 中无 require/ztools;file:///C:/Windows/win.ini 读取失败,直接 HTTP 请求失败,服务端只收到三个启动资源请求 | +| 失控终止 | Worker 进入死循环后,可信 frame 仍能调用 terminate 并回执停止 | +| 后台 | outPlugin(false) 后计时继续,重进实例 ID 不变 | +| 分离窗口 | 宿主 detachPlugin 成功,原实例 ID 不变 | +| 强制退出 | outPlugin(true) 后插件 WebContents 消失 | +| 宿主 db | 读写正常;陈旧 _rev 返回 conflict;超过 1 MiB 返回错误对象;bulkDocs 的第一条成功、第二条失败会留下第一条 | +| 本地持久化 | getPath('pluginData') 指向隔离实例的插件专属目录;内置 node:sqlite 已验证事务失败回滚、关闭后重新打开数据不变 | + +P0 判定:上述目标宿主与运行时的可行性通过。P1/P2 将实现完整 API 契约及音源导入,不把当前探针视为已能使用第三方音源。 + +## 实施取舍 + +1. 固定采用浏览器 Worker;loopback 服务只返回三个固定启动资源,不提供文件/API代理端点。音源 HTTP 走受控消息桥。可信 iframe 只启动/转发/终止 Worker,音源代码不进入 iframe。第三方脚本依赖 DOM 的行为不兼容标准 Worker,须明确报告。 +2. 结构化数据选用已实测的设备本地 `pluginData` + 内置 SQLite,不依赖宿主账号切换时的云端数据库路由。复用 LX 数据模型与 SQL 事务,避免附带 better-sqlite3 原生二进制;账户云同步由显式 LX 同步处理。启动必须检查 node:sqlite 可用性,不对旧宿主假报支持。 +3. 当前框架继续保留 Vite 作为 P0 诊断入口,原 LX 产品界面后续由独立 Webpack 入口接入。 + +所有验证均在隔离数据实例中完成;尚未把插件安装到用户日常 ZTools。完整稳定性、音频播放、真实音源、完整下载及 LX 对端同步另属后续阶段。 diff --git a/plugins/lx-music-ztools/docs/ztools/installation.md b/plugins/lx-music-ztools/docs/ztools/installation.md new file mode 100644 index 000000000..ab2e3f03d --- /dev/null +++ b/plugins/lx-music-ztools/docs/ztools/installation.md @@ -0,0 +1,25 @@ +# 本地安装与搜索 + +代码文件和构建产物不会自动安装到日常使用的 ZTools。隔离测试实例的安装记录也不会出现在日常实例中。 + +## 安装开发包 + +1. 在仓库根目录执行 `npm ci`。构建时会自动安装插件的独立运行依赖。 +2. 执行 `npm run pack`。运行依赖以可读文件包含在 ZIP 内,安装后无需运行 Vite/Node 开发服务器。 +3. 在 ZTools 的设置中打开「插件应用」,点击「导入本地插件」。 +4. 选择生成的 `../output/lx-music-ztools/lx-music-ztools-0.0.2.zip`,确认安装并启用。 +5. 回到主搜索框输入 `lxmusic`、`LX Music` 或 `洛雪音乐`。 + +也可将 `src-ztools/plugin.json` 登记为开发项目后,在 ZTools 开发工具中安装、启用该项目。单纯选择目录或生成 plugin.json 还不等于安装。 + +默认入口为 `dist/lx.html`,保留原 LX 界面。`dist/index.html` 是诊断页,不应作为播放器入口。 + +## 导入音源 + +在原 LX 设置页点击「自定义源管理」,通过「本地导入」选择兼容 LX API 2.0.0 的 JS 文件,或通过「在线导入」输入脚本 HTTP(S) URL。导入后选择该源。插件不附带第三方音源。 + +## 验证边界 + +当前为开发版。隔离实例已验证 ZIP 安装、关键词搜索、原界面、两种音源导入、受控音源解析播放、暂停/继续、后台播放和下一首。下载、同步等功能仍在移植,详见 compatibility.md。 + +自动测试使用 ZTools app.asar 3.3.0-beta.2 和独立 Electron 42.3.3 / Node 24.15.0,临时数据与日常实例分开。本机 ZTools.exe 文件内标识为 Electron 41.4.0 / Node 24.15.0;直接启动该 EXE 的 Playwright 验证未成功,因此不能把前述测试当作对安装版 EXE 的完整实机验收。 diff --git a/plugins/lx-music-ztools/docs/ztools/module-map.md b/plugins/lx-music-ztools/docs/ztools/module-map.md new file mode 100644 index 000000000..aded70ff7 --- /dev/null +++ b/plugins/lx-music-ztools/docs/ztools/module-map.md @@ -0,0 +1,23 @@ +# LX 到 ZTools 的模块映射 + +所有 vendor/lx 源文件来自上游 323e4bac;修改集中在适配器和构建别名。已导入完整 src 供独立构建,使用时不依赖旁边 desktop 工作目录。 + +| 原调用方 | 原处理器 | 插件替代 | 实施状态 | +|---|---|---|---| +| common/rendererIpc.ts | Electron ipcMain | src/ztools/rendererIpc.ts + preload/runtime/bridge.cjs | 桥接及订阅契约已验证;业务通道逐项接入 | +| renderer/main.ts / utils/ipc.ts | config / environment / rendererEvent | preload/runtime/settings.cjs + explicit handlers | 待接入 | +| renderer/core/useApp | 独立应用窗口生命周期 | preload/runtime/lifecycle.cjs | 进入/隐藏/分离/终止契约已验证 | +| store/list、main Worker | dbService/modules/list | 原 LX SQL 模块 + node:sqlite 适配 | 待接入 | +| download/dislike/lyric/URL/other-source 数据 | better-sqlite3 / Comlink | 同一设备本地 SQL 服务 | 待接入 | +| musicSdk / utils/request.js | needle / crypto / dns | preload 网络服务,纯计算留前端 | 任务 4 | +| Setting UserApi / core apiSource | userApi hidden BrowserWindow | CSP 浏览器 Worker + HTTP/cancel 消息桥 | 隔离与同步工具已实测,业务 API 待接入 | +| renderer/worker/main | Node/数据库/纯计算混合 | 原远程方法契约;Node 到 preload、纯计算保留 | 任务 6 | +| renderer/worker/download | Node 流与文件 | preload/runtime/download | 任务 8 | +| sync/client、sync/server | global.lx / main lifecycle | 原协议 + 同一存储 + 适配事件总线 | 任务 9 | +| openApi | player_status / taskbar event | 同一 Player 业务控制 | 任务 10 | +| winLyric / renderer-lyric | BrowserWindow | ZTools 公开窗口代理 | 任务 10 | +| hotKey / tray / updater | 全局注册与应用管理 | 插件指令、宿主快捷键设置、宿主更新 | 任务 10 | + +已扫描 renderer/common 的真实 Electron 入口:common/rendererIpc.ts、common/utils/electron.ts,electron-log/node 由 common/utils/index.ts 导入。renderer 内 electron-updater 引用为类型,不装载应用更新器。Node 入口按依赖图继续分类,未适配通道必须报错,不允许空返回掩盖缺失功能。 + +实施进展:列表及其事件/IPC、设置、JSON KV、主题、原网络请求和音源管理已接入。Node 服务复用原 request.js 的平台请求构造,版本输入固定为 LX 基线;前端通过可取消的 network:fetch 调用,DNS 使用受控服务。CJS 转换对平台请求日志作静默处理,避免记录可能带访问参数的 URL。原 Vue 页面已在实际宿主渲染;可见基本设置截图与检验日志保存在 artifacts/renderer。worker 的 Node 文件能力继续迁入服务;移植中的具体未完成项以 compatibility.md 为准。 diff --git a/plugins/lx-music-ztools/index.html b/plugins/lx-music-ztools/index.html new file mode 100644 index 000000000..b3d93680c --- /dev/null +++ b/plugins/lx-music-ztools/index.html @@ -0,0 +1,11 @@ + + + + LX Music + + + +
+ + + diff --git a/plugins/lx-music-ztools/package-lock.json b/plugins/lx-music-ztools/package-lock.json new file mode 100644 index 000000000..02b6f044b --- /dev/null +++ b/plugins/lx-music-ztools/package-lock.json @@ -0,0 +1,5788 @@ +{ + "name": "lx-music-ztools", + "version": "0.0.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lx-music-ztools", + "version": "0.0.2", + "dependencies": { + "@simonwep/pickr": "^1.9.1", + "buffer": "^6.0.3", + "comlink": "^4.3.1", + "crypto-browserify": "^3.12.1", + "crypto-js": "^4.2.0", + "events": "^3.3.0", + "iconv-lite": "^0.7.3", + "jschardet": "^3.1.4", + "long": "^5.3.2", + "pako": "^3.0.3", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "querystring-es3": "^0.2.1", + "sortablejs": "^1.15.7", + "stream-browserify": "^3.0.0", + "url": "^0.11.4", + "vue": "^3.5.13", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@electron/asar": "3.4.1", + "@playwright/test": "1.58.2", + "@vitejs/plugin-vue": "^5.2.1", + "@ztools-center/plugin-cli": "1.3.2", + "@ztools-center/ztools-api-types": "^1.0.1", + "css-loader": "^7.1.5", + "esbuild": "^0.25.12", + "html-webpack-plugin": "^5.6.7", + "less": "^4.9.1", + "less-loader": "12.3.3", + "pug": "^3.0.4", + "pug-plain-loader": "^1.1.0", + "style-loader": "^4.0.0", + "ts-loader": "^9.6.2", + "typescript": "^5.3.0", + "vite": "^6.0.11", + "vue-loader": "^17.4.2", + "vue-tsc": "^2.0.0", + "webpack": "^5.106.2", + "webpack-cli": "^7.0.2" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.13", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.13.tgz", + "integrity": "sha512-fWegr+WsEsiMVW4KUNdIIzzMTZSzNiqhy0//6+2MRi2d5NFNsL/AYwBQW8eBnRbTBVRnDVzjaJEUqKaCWjMMdQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.13", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.13.tgz", + "integrity": "sha512-fJM50YYjlTiBhgdkGmC1tBpv+FMAyo4annf2X6Aks+0yRlaiVkYW/3f07xAfVVkYIoZ9fFZp+mEThUFnuybhkQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz", + "integrity": "sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.68.1.tgz", + "integrity": "sha512-67xgK/taQ7w18anfSxH4dy3RVwiu7GFT8aZglHVUnYisqIe4Qf1xQW7hWxe3lB7R5XSVfPJJKdKP31eGMID2zw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.68.1.tgz", + "integrity": "sha512-nGmSABuQDWUGPgGOy3VJygaIQl9/2SMPEwwR2+W+uTxit+MoZcve1y2ebaO9Dhg/MZXMeeoYL+HPGq8X5LTzgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-x64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-x64/-/rollup-android-x64-4.68.1.tgz", + "integrity": "sha512-Es4xgvucd6umxthid6WojNNYHLsfB6oySkLypUn4VXxR7IuK2TS2sdPB6+3U/mw/uU7ahQk9aw7fDUI6y2GkKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.68.1.tgz", + "integrity": "sha512-biPWTUPYDWejnysuD4A1kJ4mjT121duL6WJ8JJ0lyFWuLSnC91WF6C/X63CSz7z8yJIQEXixlHOpPK/VzPQ3Gw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.68.1.tgz", + "integrity": "sha512-EhxsI1TfSp3Lvyc9W6Y4o9W/zR43zaUgDsENK3hCix/OP/jrGc1CAxNhgmA28SJTsODpAv9ZBYYgP1rO27dfRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.68.1.tgz", + "integrity": "sha512-p6s+MBrmkj41sOH4no/z1g/5E446H9//kUblCvZY/uy+NWijHT+0/F6sAcRqI3l3RWLyeW6kHl0IqZ0+p3VjrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.68.1.tgz", + "integrity": "sha512-sm2K6aFA1tn+5gH59LLsm3SbNJ736cXOZ3IcpmxsUxuAEn6h04LFKWk3ji/Jjw1IEDdZ8QhgdxLPzGHnnOD4fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.68.1.tgz", + "integrity": "sha512-kirHmtPRCh5dVC6v8/9i/ldS60r6TYLiGunHCXpba/4ws1SPyvdW4PYkRLOxpAfYC+A9r2Km2zOLpm4cMty1Rg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.68.1.tgz", + "integrity": "sha512-TR1ec0+aZylxhD9/4UkelsSiCQ5EhtHjzty0LXKiMwwyWa4rgF55bIGExYqxp/+BwCH4GEV8F/EZB9JYwLDTdw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.68.1.tgz", + "integrity": "sha512-dxsv4BC45+TGKImva8fIgVdvuJ1/o2Bov+sGjPX/RFcDp/UtXNJ7K5/6UTS0fbAw0P7kTIvH5M0h6ViS5CP9xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.68.1.tgz", + "integrity": "sha512-/ATJPUYJUdlm1/Zil0IyU9x14zyBXicfoOvNbvHAUH4c8IjU5xNBCtnDteTD+xd08Ym6fdGVrdZLLThYRvsZOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.68.1.tgz", + "integrity": "sha512-Bp17x/tj1Fi45pNE4A654jkKAiSUgB6UjQuPltwT+e2jfYw7TfaYzQNGgbzx0MNH27eYQDNVUNxGk9m9L/xUaA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.68.1.tgz", + "integrity": "sha512-QvECQp0CyvIhiAv9mn1ouYZfIcDBIRdZek7M/ZYqRtiWzpiKAPu2gnGD13yNtI0KykSPk41szQitphAbHfHdvQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.68.1.tgz", + "integrity": "sha512-+ACzFhkXUiYvXLebVBb4ge8gr7EKM/xGDW+J8cmnwv8SSlYyoKXD8Es3OpOPFnHLV5vevmrqlQgCP/qQxr69JQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.68.1.tgz", + "integrity": "sha512-/wMEhoQ0Ithx4CrmhMogO57CKyt0LRB+2A7i97v+GjBDGLh+uhTNNSlIm1NixaTEwMXyiIIZ3bXVkSh4jfAhKg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.68.1.tgz", + "integrity": "sha512-N92PYV7Ulw6pOS16eE32Q/ykWSj2ly+ojYX/IHCsD9+FIj7n10qjeiPhdht7r8UTnCpYdmrGf7VeNonovOvuQQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.68.1.tgz", + "integrity": "sha512-ab332+PwM7/sgbqRlE0BYb2eGNme8Rs1T/2Ob9mYSwrQqK7a1zawpLVYrXtnnRBPQn4TNGGtwaBy6toClPU7aQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.68.1.tgz", + "integrity": "sha512-zaQ2AHQP401CBAa+qhWLLTGU78MQ6nsB0RfWYbw5+AUWNLjp9juAokzZbjtyiXf7CBe8JfBNV7g2lNwbxmLKFg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.68.1.tgz", + "integrity": "sha512-J2KJofHPtGP7AZQUbRfylo7f1ZKdSjY2XG5VXjDwh+Mu2oM/bJN53mDWSjBGzNU3AGaXBqIxIhEAa0tSHQdO6w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.68.1.tgz", + "integrity": "sha512-WMXzHCmeAhFpAwP2w3ugGucdDp5jYJm8fwLO1e9uUtUQXhceWGfobu1E+seQ/zSixxGB+foAX8cs3iiebF+QjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.68.1.tgz", + "integrity": "sha512-PZadv8s8G/omPyl31lvtraKxlM//Ad0P32kLBBHGjImR31vCXZkiDNqnIQCZ6gXTZ5xN5GkTGhzZdJzMz3EOnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.68.1.tgz", + "integrity": "sha512-eXI1iou+f8g5iJFGnF9koInoopknJdulR51phO8ggVNGyTT2Ya68M7scDLYpIOTWaTN9cDPxRJ1o0d6ce/P2cQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.68.1.tgz", + "integrity": "sha512-la8czh2p2DlfCC+/XgVbxItS01f2/1ljPSy+U/Z1j5sWtlQv+Ammx04gP2w5MZWsGuKD/r85cTWudlASulQMEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.68.1.tgz", + "integrity": "sha512-WMc++sC1YObbiuITB73IYD6Neejvz0591glNh9kpJj2ZUA3YkwtYzVVXXkADyyGhttdJkJndR/dd4XKKxCHO+A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.68.1.tgz", + "integrity": "sha512-jDDAn+OoI7R63WRCsJDMLa9v4Qii/zTrnstftTy8+1kVZpZBJFSF6K6PDyot51Wh9wTcjh0zUU9dHztVNo1+uw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.68.1.tgz", + "integrity": "sha512-kR8W3osyXy6mB3EaaTsC2FTAdgqZgRsOXXs31aIvC/KjbxeMqDon9l3Xyajo1ynQuS1JHlMhkYyqL10VKA3VZw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@simonwep/pickr": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.9.1.tgz", + "integrity": "sha512-fR3qmfAcPf/HSFS7GEnTmZLM3+xERv1+jyMBbzT63ilRRM8veYjI7ELvkHHKk0/du3lHp7uh/FqatjM3646X1g==", + "license": "MIT", + "dependencies": { + "core-js": "3.37.0", + "nanopop": "2.4.2" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.6", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.45.tgz", + "integrity": "sha512-XGHJrM6ufZARAc/w+tyOswStw9tdKiqCKBScI4gGaNxD+aaGu/HQWgKJ+JItjRLPqenQ7iE7G8AJC7T5ew1p+g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.12", + "@vue/shared": "3.5.45", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.45.tgz", + "integrity": "sha512-LbP2d19GSOYGPmvMBBCVJVlv7TKtjM1Cfeyu3cYia720pknZP+1cLX7H5MVatvruT6MuJhiPuNbQBW/MDn/EwA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.45", + "@vue/shared": "3.5.45" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.45.tgz", + "integrity": "sha512-MKFjb2UX0M/l/jhfzqMLrI8VM4pfvFKG3pqFZkSZFHRLtNld/O4RtUPZFWSwhro/14qP88UQEkWzHxAZAQ0tsw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.12", + "@vue/compiler-core": "3.5.45", + "@vue/compiler-dom": "3.5.45", + "@vue/compiler-ssr": "3.5.45", + "@vue/shared": "3.5.45", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.32", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.45.tgz", + "integrity": "sha512-RF5sbZOfYz5AK00tOJUn2PwFXR2fS1u2HVzI8D9VF9rGAFxuGBqg2bB1kJ3ZHMprqSltXnT9MZE1IsqjSM8oew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.45", + "@vue/shared": "3.5.45" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.45.tgz", + "integrity": "sha512-WqkyubNWpI4OYBOipSrjIssMdJO8K1i9Zts/Wm8Er10/0msOEwCyWWWvZnAI7WW3kQsTong6DJp8Cbb+vd4cPA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.45" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.45.tgz", + "integrity": "sha512-bzIO3+FABjpj85uIKSc+Qnj36/sxDNI3tcH9i6w6xKA1k6e0ejv0G0jMCfeJBvK3yGL5XbJ3DMZj+ClqyCHFYw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.45", + "@vue/shared": "3.5.45" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.45.tgz", + "integrity": "sha512-ZvRTFMDgfqTDYWvJfoVpQTJx1RN5M6l7Pyd6wfdY2NaXzbPQVh9ezeQYFbNPP6bIcRVn2SqSSR25N/jSW69BFw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.45", + "@vue/runtime-core": "3.5.45", + "@vue/shared": "3.5.45", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.45.tgz", + "integrity": "sha512-1fNXUHhhsWf4EVL4XixbGBU5mKdiIXJAB82feHRTyPPo6ZUEYWIJuEeUl3mVV6QDBp50HJOX4obLhxYXlrjrzA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.45", + "@vue/runtime-dom": "3.5.45", + "@vue/shared": "3.5.45" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.45.tgz", + "integrity": "sha512-ue9fgHmVNPlP7iW3+cxoBX/Cnlw53dHlVSw0JKGY6z1lObg8pHoj928M2nzG4/yVsMZ3NaRZ2s2z61uC16NDwQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@ztools-center/plugin-cli": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@ztools-center/plugin-cli/-/plugin-cli-1.3.2.tgz", + "integrity": "sha512-ZQdFxHpo1al068Lw6k3t5uhrsLN0Ete/3DflqGo+lWxjDEU5JI497Cs0EVP3cF7vyC8kBIAPHebcvVrLaVpm3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.8.0", + "minimist": "^1.2.8", + "prompts": "^2.4.2" + }, + "bin": { + "ztools": "dist/index.js" + } + }, + "node_modules/@ztools-center/ztools-api-types": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@ztools-center/ztools-api-types/-/ztools-api-types-1.0.3.tgz", + "integrity": "sha512-dv1eOAIasAupqKaQL/gESk1i2+RebdM/1gvZhrvH2D/bo3enCUsAGJ8nrHnlCLBSOGB81eC/SU0IH8BNsUlmvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmmirror.com/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmmirror.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.35", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.35.tgz", + "integrity": "sha512-GNk8KX52CHqeNKGk8+7tZnQab/2k1t/BgrUHPKNdijzpEhF1saPQZHVKb6m5loSAxB39gyrTYw34Qf9CQ3u1qQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.6", + "resolved": "https://registry.npmmirror.com/browserify-sign/-/browserify-sign-4.2.6.tgz", + "integrity": "sha512-sd+Q65fjlWCYWtZKXiKfrUc8d+4jtp/8f0W2NkwzLtoW4bI6UDnWusLWIurHnmurW0XShIRxpwiOX4EoPtXUAg==", + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.3", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserslist": { + "version": "4.29.5", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001823", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001823.tgz", + "integrity": "sha512-TQ5s/djDjNk1axreUW+4rULH+BmdCMLiJkhaXnO5wqivG7tITkSUj4ItIdzqdZHKaXcHAV8dhH3JxOM7/7bwqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/comlink": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/comlink/-/comlink-4.3.1.tgz", + "integrity": "sha512-+YbhUdNrpBZggBAHWcgQMLPLH1KDF3wJpeqrCKieWQ8RL7atmgsgTQko1XEBK6PsecfopWNntopJ+ByYG1lRaA==", + "license": "Apache-2.0" + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-js": { + "version": "3.37.0", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.37.0.tgz", + "integrity": "sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmmirror.com/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "deprecated": "Active development of CryptoJS has been discontinued. This library is no longer maintained.", + "license": "MIT" + }, + "node_modules/css-loader": { + "version": "7.1.5", + "resolved": "https://registry.npmmirror.com/css-loader/-/css-loader-7.1.5.tgz", + "integrity": "sha512-Q7iAfQkU2twNBryKX/vGAlE+GAmkF7quhSzAGNK8fBimxk3+tqg245rH52UPb9dioBolBc8rP0ihmHBaDTJQBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.478", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.478.tgz", + "integrity": "sha512-pftG+ma5Lm8O6/p45bhYT6StBN5ziQymaRzmCZwFl8DwWHKPPD93FdOmP1aqftcepolQYvrcyTLa17kE8Yf+jg==", + "dev": true, + "license": "ISC" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.26.1", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.26.1.tgz", + "integrity": "sha512-PPnnSJq3azKOIewXR/uppBV/6h98dprBJ/WaQ9Qgj10xX1lJLNfg+XShpbsrSqcDSy2yYYyhfXM1wWhftqgyzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmmirror.com/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.9", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmmirror.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.7", + "resolved": "https://registry.npmmirror.com/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", + "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.18.0", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.18.0.tgz", + "integrity": "sha512-qyax9fgaKOEC4qK8WZn7Q4IcoZC9RC1O1fFxRcRTq/oMMkWT/uXpqpXa12kzWR409zAt1drYN9N9BO26UmBCmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jschardet": { + "version": "3.1.4", + "resolved": "https://registry.npmmirror.com/jschardet/-/jschardet-3.1.4.tgz", + "integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==", + "license": "LGPL-2.1+", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/less": { + "version": "4.9.1", + "resolved": "https://registry.npmmirror.com/less/-/less-4.9.1.tgz", + "integrity": "sha512-orp15PfJvvNDIqJdVWzMI9Sjpjp3VTiw3sfvbB+67LlISTEn8uVT2EdYSuyl02BLvaftv6sdk9Umnxmm5rckmg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "copy-anything": "^3.0.5", + "parse-node-version": "^1.0.1" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "make-dir": "^5.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "probe-image-size": "^7.2.3", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "12.3.3", + "resolved": "https://registry.npmmirror.com/less-loader/-/less-loader-12.3.3.tgz", + "integrity": "sha512-F0+ErFFDj3Pt+nVrCN6VlEGEzocv9s7x/aR9v2riI+WM83UAfTYBDBjGPJnT55lBLR6JSI4fmWblt+aA2JN6/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-5.1.0.tgz", + "integrity": "sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanopop": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.4.2.tgz", + "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==", + "license": "MIT" + }, + "node_modules/needle": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/needle/-/needle-3.5.0.tgz", + "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.63", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.63.tgz", + "integrity": "sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pako": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/pako/-/pako-3.0.3.tgz", + "integrity": "sha512-tmrebBBYI5FOw8QICGwJNqE8fotC8R+gwcxOvoFNv+GKehOI6Lc6YBolNcQNnC4v96l1wt7rh102ifD4kVeZAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.10", + "resolved": "https://registry.npmmirror.com/parse-asn1/-/parse-asn1-5.1.10.tgz", + "integrity": "sha512-gBlTKLbYnAJN9WjmawVOMfaGvVoy4lX0LH18t7ccwtEG9RgAa4ETf2eu3d2J4e/5SoCjCms0A0UF4tTw/Eyn6Q==", + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.7", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pbkdf2": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/pbkdf2/-/pbkdf2-3.1.7.tgz", + "integrity": "sha512-nS4mvFgVwUrecPTRdpdseM2fwpdfnX0/HibA8DgBRQtihUAzHQYJFU2l+OlEtiXtpqkrMvCPDwyXMUnASDt6Qw==", + "license": "MIT", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.2.0.tgz", + "integrity": "sha512-Khh/tERVBn0I1499dvBSHY0pWJloMJWUxDsOBM0CCDPAcVZ10Jl2L1qy1f19uEX22spa8Mv7DCdEQJc0cGwBhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/probe-image-size": { + "version": "7.5.0", + "resolved": "https://registry.npmmirror.com/probe-image-size/-/probe-image-size-7.5.0.tgz", + "integrity": "sha512-ietdx4XPgmnzHRZOq1/KtcnsBkL4bRsBe+Szj5Tuq1/KPqDG42OspG1IhJb7vz6Z9pSzhp0J6UPnw6hujJTWoQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "lodash.merge": "^4.6.2", + "needle": "^2.5.2" + } + }, + "node_modules/probe-image-size/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/probe-image-size/node_modules/needle": { + "version": "2.9.1", + "resolved": "https://registry.npmmirror.com/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/pug": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pug/-/pug-3.0.4.tgz", + "integrity": "sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "pug-code-gen": "^3.0.4", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pug-code-gen/-/pug-code-gen-3.0.4.tgz", + "integrity": "sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-plain-loader": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/pug-plain-loader/-/pug-plain-loader-1.1.0.tgz", + "integrity": "sha512-1nYgIJLaahRuHJHhzSPODV44aZfb00bO7kiJiMkke6Hj4SVZftuvx6shZ4BOokk50dJc2RSFqNUBOlus0dniFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^1.1.0" + }, + "peerDependencies": { + "pug": "^2.0.0 || ^3.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.17.1", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.17.1.tgz", + "integrity": "sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.14", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.14.tgz", + "integrity": "sha512-1ZjYvR9sbkiwC5+9QHetqW5HmA2pHRMaoupD01t2HlYnV/vydC2zPwLixhsMieSWVZt0JJRg5LVIk0Ow1D/TrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.18.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rollup": { + "version": "4.68.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.68.1.tgz", + "integrity": "sha512-kjVU1iN2BbQeFPFTpmUtl1bR/DbMl2HPmiSLYpx6XLromTaMkidR+mlRMdS/Doovm6uKlwigyxMcHRhRe1vZ5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.10" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.68.1", + "@rollup/rollup-android-arm64": "4.68.1", + "@rollup/rollup-android-x64": "4.68.1", + "@rollup/rollup-darwin-arm64": "4.68.1", + "@rollup/rollup-darwin-x64": "4.68.1", + "@rollup/rollup-freebsd-arm64": "4.68.1", + "@rollup/rollup-freebsd-x64": "4.68.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.68.1", + "@rollup/rollup-linux-arm-musleabihf": "4.68.1", + "@rollup/rollup-linux-arm64-gnu": "4.68.1", + "@rollup/rollup-linux-arm64-musl": "4.68.1", + "@rollup/rollup-linux-loong64-gnu": "4.68.1", + "@rollup/rollup-linux-loong64-musl": "4.68.1", + "@rollup/rollup-linux-ppc64-gnu": "4.68.1", + "@rollup/rollup-linux-ppc64-musl": "4.68.1", + "@rollup/rollup-linux-riscv64-gnu": "4.68.1", + "@rollup/rollup-linux-riscv64-musl": "4.68.1", + "@rollup/rollup-linux-s390x-gnu": "4.68.1", + "@rollup/rollup-linux-x64-gnu": "4.68.1", + "@rollup/rollup-linux-x64-musl": "4.68.1", + "@rollup/rollup-openbsd-x64": "4.68.1", + "@rollup/rollup-openharmony-arm64": "4.68.1", + "@rollup/rollup-win32-arm64-msvc": "4.68.1", + "@rollup/rollup-win32-ia32-msvc": "4.68.1", + "@rollup/rollup-win32-x64-gnu": "4.68.1", + "@rollup/rollup-win32-x64-msvc": "4.68.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/schema-utils": { + "version": "4.8.3", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.8.3.tgz", + "integrity": "sha512-qIfo3f+lRNqnMPgpXSKvUhsT3pY5U+ppmgGr4j8LanFP3W/Ze+xj8DY/dPb5qVu3m6qFIMyMWKVRLNKxrNhxWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "bin": { + "schema-utils": "bin/schema-utils.js" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/semver": { + "version": "7.8.6", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmmirror.com/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sortablejs": { + "version": "1.15.7", + "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-browserify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-loader": { + "version": "9.6.2", + "resolved": "https://registry.npmmirror.com/ts-loader/-/ts-loader-9.6.2.tgz", + "integrity": "sha512-R4iuczmtgxvtuI556s+hTZ6/7Ee03VCAk/l/M8LY1OAsUgB7YydsCxkgq9D9pKRaD7GJqUi2u8fp9zZP/ufjKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "picomatch": "^4.0.0", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "loader-utils": "*", + "typescript": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "loader-utils": { + "optional": true + } + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/tslib": { + "version": "2.9.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.9.1.tgz", + "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmmirror.com/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.5", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.5.tgz", + "integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.45", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.45.tgz", + "integrity": "sha512-3cvsib8m9c0vB24+MDKsgcW4+Da4xaBl9Z/5Tbm2pmSIsRPxcil4cdXjXlVsmJ2g64XJo5h+IRd7rsdGT9pZaw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.45", + "@vue/compiler-sfc": "3.5.45", + "@vue/runtime-dom": "3.5.45", + "@vue/server-renderer": "3.5.45", + "@vue/shared": "3.5.45" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-loader": { + "version": "17.4.2", + "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-17.4.2.tgz", + "integrity": "sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "watchpack": "^2.4.0" + }, + "peerDependencies": { + "webpack": "^4.1.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.6.4", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.6.4.tgz", + "integrity": "sha512-5H3Tce2GfzWbcr9VnSqyak2whFaONw4BdmI5BDnMXold4qSlCFd64XaMQESRh2vkDKmdUkv6rUo17UvZ9f9nXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/webpack-cli/-/webpack-cli-7.0.2.tgz", + "integrity": "sha512-dB0R4T+C/8YuvM+fabdvil6QE44/ChDXikV5lOOkrUeCkW5hTJv2pGLE3keh+D5hjYw8icBaJkZzpFoaHV4T+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^1.0.0", + "commander": "^14.0.3", + "cross-spawn": "^7.0.6", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.101.0", + "webpack-bundle-analyzer": "^4.0.0 || ^5.0.0", + "webpack-dev-server": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.7.0.tgz", + "integrity": "sha512-PdUhbCycwEgiS1hB/FBDksWKg/ashAIuJKqwdbPn8/dFVgU5LxQiBvG0js6VRRIzPujMrStYCczywp82P0KWng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/plugins/lx-music-ztools/package.json b/plugins/lx-music-ztools/package.json new file mode 100644 index 000000000..da95b428e --- /dev/null +++ b/plugins/lx-music-ztools/package.json @@ -0,0 +1,66 @@ +{ + "name": "lx-music-ztools", + "version": "0.0.2", + "description": "LX Music native ZTools port with isolated custom sources (development)", + "type": "module", + "scripts": { + "prebuild": "npm ci --prefix src-ztools/preload --omit=dev --no-audit --no-fund", + "dev": "npm run build:runtime && node scripts/build-worker.mjs && webpack --watch --config build-config/ztools/webpack.config.cjs", + "dev:diagnostics": "vite --host 127.0.0.1 --port 5173 --strictPort", + "build": "vue-tsc && vite build && node scripts/build-runtime.mjs && node scripts/build-worker.mjs && node scripts/build-lx.mjs && node scripts/check.mjs", + "build:ztools": "npm run build", + "build:runtime": "node scripts/build-runtime.mjs", + "check:ztools": "vue-tsc && node scripts/check.mjs", + "test:p0": "node scripts/test-p0.mjs", + "test:source": "node scripts/test-source.mjs && node scripts/test-source-ui.mjs", + "test:player": "node scripts/test-player.mjs", + "test:ztools": "node --test tests/ztools/*.test.cjs", + "check": "node scripts/check.mjs", + "pack": "npm run build && node scripts/package.mjs", + "publish:plugin": "node scripts/publish.mjs", + "test:publish": "node scripts/rehearse-publish.mjs", + "test:host": "node scripts/test-install.mjs" + }, + "dependencies": { + "@simonwep/pickr": "^1.9.1", + "buffer": "^6.0.3", + "comlink": "^4.3.1", + "crypto-browserify": "^3.12.1", + "crypto-js": "^4.2.0", + "events": "^3.3.0", + "iconv-lite": "^0.7.3", + "jschardet": "^3.1.4", + "long": "^5.3.2", + "pako": "^3.0.3", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "querystring-es3": "^0.2.1", + "sortablejs": "^1.15.7", + "stream-browserify": "^3.0.0", + "url": "^0.11.4", + "vue": "^3.5.13", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@electron/asar": "3.4.1", + "@playwright/test": "1.58.2", + "@vitejs/plugin-vue": "^5.2.1", + "@ztools-center/plugin-cli": "1.3.2", + "@ztools-center/ztools-api-types": "^1.0.1", + "css-loader": "^7.1.5", + "esbuild": "^0.25.12", + "html-webpack-plugin": "^5.6.7", + "less": "^4.9.1", + "less-loader": "12.3.3", + "pug": "^3.0.4", + "pug-plain-loader": "^1.1.0", + "style-loader": "^4.0.0", + "ts-loader": "^9.6.2", + "typescript": "^5.3.0", + "vite": "^6.0.11", + "vue-loader": "^17.4.2", + "vue-tsc": "^2.0.0", + "webpack": "^5.106.2", + "webpack-cli": "^7.0.2" + } +} diff --git a/plugins/lx-music-ztools/scripts/build-lx.mjs b/plugins/lx-music-ztools/scripts/build-lx.mjs new file mode 100644 index 000000000..333054312 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/build-lx.mjs @@ -0,0 +1,11 @@ +import webpack from 'webpack' +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) +const config = require('../build-config/ztools/webpack.config.cjs') +const compiler = webpack(config) +await new Promise((resolve, reject) => compiler.run((error, stats) => { + console.log(stats?.toString(config.stats) || '') + compiler.close(() => {}) + if (error || stats?.hasErrors()) reject(error || new Error('LX renderer build failed')) + else resolve() +})) diff --git a/plugins/lx-music-ztools/scripts/build-runtime.mjs b/plugins/lx-music-ztools/scripts/build-runtime.mjs new file mode 100644 index 000000000..ed8cc019d --- /dev/null +++ b/plugins/lx-music-ztools/scripts/build-runtime.mjs @@ -0,0 +1,61 @@ +import ts from 'typescript' +import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'node:fs' +import { resolve, dirname, relative, extname } from 'node:path' + +const source = resolve('vendor/lx/src') +const output = resolve('src-ztools/preload/upstream') +const built = new Set() +const dbOverride = resolve('src-ztools/preload/runtime/database.cjs') +function resolveSource(file) { + for (const candidate of [file, `${file}.ts`, `${file}.js`, `${file}/index.ts`, `${file}/index.js`]) if (existsSync(candidate) && statSync(candidate).isFile()) return resolve(candidate) + throw new Error(`Unresolved LX runtime dependency: ${file}`) +} +function compile(input) { + input = resolveSource(input) + const target = resolve(output, relative(source, input).replace(/\.(ts|js)$/, '.cjs')) + if (built.has(input)) return target + built.add(input) + mkdirSync(dirname(target), { recursive: true }) + if (extname(input) === '.json') { writeFileSync(target, readFileSync(input)); return target } + if (input === resolve(source, 'main/worker/dbService/db.ts')) { + writeFileSync(target, `// LX database backend adapted for ZTools.\nmodule.exports=require(${JSON.stringify(relative(dirname(target), dbOverride).replaceAll('\\', '/'))})\n`) + return target + } + if (input === resolve(source, 'common/mainIpc.ts')) { + const adapter = relative(dirname(target), resolve('src-ztools/preload/runtime/main-ipc.cjs')).replaceAll('\\', '/') + writeFileSync(target, `module.exports=require(${JSON.stringify(adapter)})\n`) + return target + } + if (input === resolve(source, 'main/utils/index.ts')) { + const adapter = relative(dirname(target), resolve('src-ztools/preload/runtime/app-utils.cjs')).replaceAll('\\', '/') + writeFileSync(target, `module.exports=require(${JSON.stringify(adapter)})\n`) + return target + } + if (input === resolve(source, 'renderer/store/index.ts')) { + const adapter = relative(dirname(target), resolve('src-ztools/preload/runtime/proxy.cjs')).replaceAll('\\', '/') + writeFileSync(target, `module.exports=require(${JSON.stringify(adapter)})\n`) + return target + } + const originalSource = readFileSync(input, 'utf8').replace("path.join(os.tmpdir(), 'lxmusic_temp')", 'global.lx.tempDir') + const text = ts.transpileModule(originalSource, { fileName: input, compilerOptions: { + module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true, removeComments: false, + } }).outputText + const result = text.replace(/require\((['"])([^'"]+)\1\)/g, (match, quote, specifier) => { + let dependency + if (specifier === 'electron-log/node') return `require(${JSON.stringify(relative(dirname(target), resolve('src-ztools/preload/runtime/log.cjs')).replaceAll('\\', '/'))})` + if (specifier.startsWith('@common/')) dependency = resolve(source, 'common', specifier.slice(8)) + else if (specifier.startsWith('@main/')) dependency = resolve(source, 'main', specifier.slice(6)) + else if (specifier.startsWith('@renderer/')) dependency = resolve(source, 'renderer', specifier.slice(10)) + else if (specifier.startsWith('.')) dependency = resolve(dirname(input), specifier) + else return match + const dependencyTarget = compile(dependency) + let spec = relative(dirname(target), dependencyTarget).replaceAll('\\', '/') + if (!spec.startsWith('.')) spec = './' + spec + return `require(${JSON.stringify(spec)})` + }) + const contextModule = relative(dirname(target), resolve('src-ztools/preload/runtime/context.cjs')).replaceAll('\\', '/') + writeFileSync(target, `// Readable LX source adaptation. Upstream 323e4bac; Apache-2.0.\nconst lxRuntimeContext=require(${JSON.stringify(contextModule)});\n${result.replace(/global\.lx\b/g, 'lxRuntimeContext.lx').replaceAll('process.versions.app', JSON.stringify('2.12.6')).replace("console.log('---start---', url);", '')}`) + return target +} +for (const entry of ['main/worker/dbService/modules/index.ts', 'main/worker/dbService/tables.ts', 'main/worker/dbService/migrate.ts', 'common/defaultSetting.ts', 'common/defaultHotKey.ts', 'common/utils/nodejs.ts', 'common/theme/index.json', 'common/ipcNames.ts', 'main/event/index.ts', 'main/modules/commonRenderers/list/rendererEvent.ts', 'main/modules/commonRenderers/list/winRendererEvent.ts', 'main/modules/commonRenderers/dislike/rendererEvent.ts', 'main/modules/commonRenderers/dislike/winRendererEvent.ts', 'main/modules/winMain/rendererEvent/music.ts', 'main/modules/winMain/rendererEvent/download.ts', 'renderer/utils/request.js', 'renderer/worker/main/common.ts', 'renderer/worker/main/list.ts', 'renderer/worker/main/music.ts']) compile(resolve(source, entry)) +console.log(`Adapted ${built.size} readable LX runtime modules (no bundling/minification)`) diff --git a/plugins/lx-music-ztools/scripts/build-worker.mjs b/plugins/lx-music-ztools/scripts/build-worker.mjs new file mode 100644 index 000000000..d63f3f9b8 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/build-worker.mjs @@ -0,0 +1,7 @@ +import { build } from 'esbuild' +await build({ entryPoints: { 'source-worker': 'src/source-worker.js', 'probe-worker': 'src/probe-worker.js' }, bundle: true, platform: 'browser', + outdir: 'src-ztools/dist', format: 'iife', minify: false, + alias: { stream: 'stream-browserify', events: 'events' }, define: { global: 'globalThis' }, + inject: ['src/worker-shims.js'], + banner: { js: 'try {' }, footer: { js: '} catch(error) { postMessage({type:"error",error:String(error.stack || error)}) }' }, +}) diff --git a/plugins/lx-music-ztools/scripts/check.mjs b/plugins/lx-music-ztools/scripts/check.mjs new file mode 100644 index 000000000..213c6f1d8 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/check.mjs @@ -0,0 +1,25 @@ +import { readFileSync, existsSync } from 'node:fs' +import { resolve, relative, isAbsolute } from 'node:path' +import assert from 'node:assert/strict' +import vm from 'node:vm' + +/** 检查最终安装目录内的入口、元信息与可读 preload。 */ +export function check(root = resolve('src-ztools')) { + const manifest = JSON.parse(readFileSync(resolve(root, 'plugin.json'), 'utf8')) + assert.match(manifest.name, /^[a-z][a-z0-9-]*[a-z0-9]$/) + assert.match(manifest.version, /^\d+\.\d+\.\d+$/) + for (const field of ['title', 'description', 'author']) assert.ok(manifest[field]) + for (const key of ['main', 'preload', 'logo']) { + const file = resolve(root, manifest[key]) + const rel = relative(root, file) + assert.ok(!rel.startsWith('..') && !isAbsolute(rel), `${key} 超出插件目录`) + assert.ok(existsSync(file), `缺少 ${key}: ${file}`) + } + assert.equal(readFileSync(resolve(root, manifest.logo)).subarray(0, 8).toString('hex'), '89504e470d0a1a0a') + assert.equal(JSON.parse(readFileSync(resolve(root, 'preload/package.json'), 'utf8')).type, 'commonjs') + new vm.Script(readFileSync(resolve(root, manifest.preload), 'utf8')) + assert.equal(new Set(manifest.features.map(x => x.code)).size, manifest.features.length) + console.log(`PASS 安装目录完整:${manifest.name} v${manifest.version}`) + return manifest +} +if (process.argv[1] && resolve(process.argv[1]) === import.meta.filename) check() diff --git a/plugins/lx-music-ztools/scripts/host-fixture.mjs b/plugins/lx-music-ztools/scripts/host-fixture.mjs new file mode 100644 index 000000000..b77730be3 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/host-fixture.mjs @@ -0,0 +1,73 @@ +import { _electron as electron, expect } from '@playwright/test' +import { mkdtempSync, mkdirSync, readdirSync, copyFileSync, readFileSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import assert from 'node:assert/strict' + +function copyDirectory(source, target) { + mkdirSync(target, { recursive: true }) + for (const entry of readdirSync(source, { withFileTypes: true })) { + if (entry.isDirectory()) copyDirectory(join(source, entry.name), join(target, entry.name)) + else copyFileSync(join(source, entry.name), join(target, entry.name)) + } +} + +/** Load the actual production plugin in a separate, disposable ZTools instance. */ +export async function startHost(entry, { zip = process.env.ZTOOLS_PLUGIN_ZIP } = {}) { + const data = mkdtempSync(join(tmpdir(), 'lx-ztools-host-')) + mkdirSync(join(data, 'legacy')) + const plugin = join(data, 'plugin') + copyDirectory(resolve('src-ztools'), plugin) + const manifest = JSON.parse(readFileSync(join(plugin, 'plugin.json'), 'utf8')) + delete manifest.development + if (entry) manifest.main = entry + entry = manifest.main + writeFileSync(join(plugin, 'plugin.json'), JSON.stringify(manifest)) + const app = await electron.launch({ executablePath: process.env.ZTOOLS_EXECUTABLE || resolve('../tmp/lx-electron-42/electron.exe'), + args: [process.env.ZTOOLS_APP || 'D:/Program Files/ZTools/resources/app.asar'], + env: { ...process.env, ZTOOLS_DATA_ROOT: data, ZTOOLS_LEGACY_USER_DATA_PATH: join(data, 'legacy'), ZTOOLS_E2E: '1' }, timeout: 45000 }) + try { + await app.evaluate(({ app, webContents }) => { + globalThis.__lxTestConsole = [] + const attach = wc => wc.on('console-message', (_event, details, message) => globalThis.__lxTestConsole.push(typeof details === 'object' ? details.message : String(message))) + webContents.getAllWebContents().forEach(attach) + app.on('web-contents-created', (_event, wc) => attach(wc)) + }) + const page = await app.firstWindow() + await page.waitForSelector('.search-input', { timeout: 30000 }) + const installed = await app.evaluate(async ({ ipcMain, BrowserWindow }, { file, zip }) => { + const sender = BrowserWindow.getAllWindows().find(x => x.getTitle() === 'ZTools').webContents + if (zip) return ipcMain._invokeHandlers.get('install-plugin-from-path')({ sender }, zip) + const registered = await ipcMain._invokeHandlers.get('upsert-dev-project-by-config-path')({ sender }, file) + if (!registered.success) throw new Error(JSON.stringify(registered)) + return ipcMain._invokeHandlers.get('install-dev-plugin')({ sender }, registered.pluginName) + }, { file: join(plugin, 'plugin.json'), zip: zip ? resolve(zip) : null }) + assert.equal(installed.success, true, JSON.stringify(installed)) + await page.locator('.search-input').fill('lxmusic') + await page.locator('.app-item').filter({ hasText: /lxmusic|洛雪音乐|LX Music/i }).first().click() + const suffix = '/' + entry + await expect.poll(() => app.evaluate(({ webContents }, suffix) => webContents.getAllWebContents().some(x => x.getURL().includes(suffix)), suffix), { timeout: 20000 }).toBe(true) + const evaluate = async (script, timeout = 35000) => { + let timer + try { + return await Promise.race([app.evaluate(({ webContents }, { suffix, script }) => { + const wc = webContents.getAllWebContents().find(x => x.getURL().includes(suffix)) + if (!wc) throw new Error('Plugin WebContents not found') + return wc.executeJavaScript(script) + }, { suffix, script }), new Promise((_resolve, reject) => { timer = setTimeout(() => reject(new Error('Plugin evaluation timed out')), timeout) })]) + } finally { clearTimeout(timer) } + } + const close = async () => { + // Only this launch's process: a blocked native dialog must not leave a test host running. + const timer = setTimeout(() => app.process()?.kill(), 5000) + try { await app.close() } finally { clearTimeout(timer) } + } + return { app, page, data, plugin, evaluate, logs: () => app.evaluate(() => globalThis.__lxTestConsole), close } + } catch (error) { + mkdirSync('artifacts', { recursive: true }) + const logs = await app.evaluate(() => globalThis.__lxTestConsole).catch(() => []) + const body = await (await app.firstWindow()).locator('body').innerText().catch(() => '') + writeFileSync('artifacts/host-last-failure.json', JSON.stringify({ data, error: String(error), body, logs }, null, 2)) + await app.close(); throw error + } +} diff --git a/plugins/lx-music-ztools/scripts/package.mjs b/plugins/lx-music-ztools/scripts/package.mjs new file mode 100644 index 000000000..cbaecad6f --- /dev/null +++ b/plugins/lx-music-ztools/scripts/package.mjs @@ -0,0 +1,12 @@ +import { mkdirSync } from 'node:fs' +import { execFileSync } from 'node:child_process' +import { resolve } from 'node:path' +import { check } from './check.mjs' +const manifest = check() +mkdirSync('../output/lx-music-ztools', { recursive: true }) +const output = resolve('../output/lx-music-ztools', `${manifest.name}-${manifest.version}.zip`) +// 包根目录直接包含 plugin.json,供 ZTools 本地安装。 +execFileSync('powershell.exe', ['-NoProfile', '-Command', 'Compress-Archive -Path (Join-Path $env:LX_PACKAGE_SOURCE "*") -DestinationPath $env:LX_PACKAGE_OUTPUT -Force'], { + env: { ...process.env, LX_PACKAGE_SOURCE: resolve('src-ztools'), LX_PACKAGE_OUTPUT: output }, stdio: 'inherit' +}) +console.log(output) diff --git a/plugins/lx-music-ztools/scripts/publish-platform.mjs b/plugins/lx-music-ztools/scripts/publish-platform.mjs new file mode 100644 index 000000000..4e1e26405 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/publish-platform.mjs @@ -0,0 +1,14 @@ +/** Quote the CLI's generated Git refs/paths for cmd.exe, rejecting shell expansion. */ +function quoteWindowsGitArgument(value) { + if (typeof value !== 'string' || /["%!\r\n\0]/.test(value)) throw new Error('Unsupported character in Windows Git argument') + // Windows argv parsing requires doubling backslashes immediately before the closing quote. + return '"' + value.replace(/\\+$/, tail => tail + tail) + '"' +} + +/** Fix CLI 1.3.2's POSIX-only shell quoting without changing its sync/protection logic. */ +export function adaptCliUpstream(source, platform = process.platform) { + if (platform !== 'win32') return source + const marker = 'function shellQuote(value) {' + if (!source.includes(marker)) throw new Error('CLI upstream quoting changed; inspect before publishing') + return source.replace(marker, `${marker}\n return (${quoteWindowsGitArgument.toString()})(value);`) +} diff --git a/plugins/lx-music-ztools/scripts/publish.mjs b/plugins/lx-music-ztools/scripts/publish.mjs new file mode 100644 index 000000000..de4b6a3ee --- /dev/null +++ b/plugins/lx-music-ztools/scripts/publish.mjs @@ -0,0 +1,58 @@ +import { execFileSync } from 'node:child_process' +import { registerHooks } from 'node:module' +import { homedir } from 'node:os' +import { join } from 'node:path' +import { existsSync, readFileSync } from 'node:fs' +import { adaptCliUpstream } from './publish-platform.mjs' + +// 复用用户已经登录的 GitHub CLI,不把凭据写入项目或 CLI 配置。 +const token = execFileSync('gh', ['auth', 'token'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() +process.env.ZTOOLS_CONFIG_DIR ||= join(homedir(), '.config', 'ztools', 'lx-music-ztools-sparse') +const cache = join(process.env.ZTOOLS_CONFIG_DIR, 'ZTools-plugins') +const write = process.stdout.write.bind(process.stdout) +const errorWrite = process.stderr.write.bind(process.stderr) +process.stdout.write = (chunk, ...args) => write(String(chunk).replaceAll(token, '[REDACTED]'), ...args) +process.stderr.write = (chunk, ...args) => errorWrite(String(chunk).replaceAll(token, '[REDACTED]'), ...args) +globalThis[Symbol.for('lx-publish-token')] = token +registerHooks({ + resolve(specifier, context, next) { + if (specifier === './auth.js' && context.parentURL?.includes('/@ztools-center/plugin-cli/dist/')) return { url: 'lx-auth:gh', shortCircuit: true } + return next(specifier, context) + }, + load(url, context, next) { + if (url.endsWith('/@ztools-center/plugin-cli/dist/upstream.js')) return { format: 'module', shortCircuit: true, source: adaptCliUpstream(readFileSync(new URL(url), 'utf8')) } + if (url === 'lx-auth:gh') return { format: 'module', shortCircuit: true, source: 'export async function ensureAuth(){ return globalThis[Symbol.for("lx-publish-token")] }' } + if (url.endsWith('/@ztools-center/plugin-cli/dist/github.js')) { + // Windows 当前网络下 Node HTTPS 直连失败;保留官方 API 业务逻辑,使用 gh 发送真实请求。 + let source = readFileSync(new URL(url), 'utf8') + const start = source.indexOf('function githubRequest(') + const end = source.indexOf('export async function getCurrentUser', start) + if (start < 0 || end < 0) throw new Error('CLI GitHub transport changed; inspect before publishing') + source = source.slice(0, start) + `async function githubRequest(path, accessToken, method = 'GET', body) { + const args = ['api', path, '--method', method]; + if (body !== undefined) args.push('--input', '-'); + const result = await import('node:child_process').then(({execFileSync}) => execFileSync('gh', args, { encoding: 'utf8', input: body === undefined ? undefined : JSON.stringify(body), stdio: ['pipe','pipe','pipe'] })); + return result.trim() ? JSON.parse(result) : null; + }\n` + source.slice(end) + return { format: 'module', shortCircuit: true, source } + } + return next(url, context) + } +}) +/** 移除发布缓存 remote URL 中的认证信息。 */ +function cleanCredentialURL() { + if (!existsSync(join(cache, '.git'))) return + try { + const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: cache, encoding: 'utf8' }).trim() + execFileSync('git', ['remote', 'set-url', 'origin', url.replace(/https:\/\/[^/]*@github\.com\//, 'https://github.com/')], { cwd: cache, stdio: 'ignore' }) + } catch { /* 不让清理错误遮蔽发布失败原因。 */ } +} +process.once('exit', cleanCredentialURL) +try { + const { publish } = await import('@ztools-center/plugin-cli/dist/publish.js') + await publish() +} finally { + // 官方 CLI 会将认证 URL 写入缓存 remote;完成后移除其中的凭据。 + cleanCredentialURL() + delete globalThis[Symbol.for('lx-publish-token')] +} diff --git a/plugins/lx-music-ztools/scripts/rehearse-publish.mjs b/plugins/lx-music-ztools/scripts/rehearse-publish.mjs new file mode 100644 index 000000000..45eb35a02 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/rehearse-publish.mjs @@ -0,0 +1,75 @@ +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, unlinkSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { execFileSync } from 'node:child_process' +import { registerHooks } from 'node:module' +import assert from 'node:assert/strict' +import { adaptCliUpstream } from './publish-platform.mjs' + +// 所有 Git 写入均指向新建临时仓库;GitHub 和认证接口由本文件替换为演练响应。 +const root = mkdtempSync(join(tmpdir(), 'lx-publish-')) +const project = join(root, 'project') +const seed = join(root, 'seed') +const remote = join(root, 'remote.git') +const cache = join(root, 'config') +const source = process.cwd() +const manifest = JSON.parse(readFileSync(join(source, 'src-ztools/plugin.json'), 'utf8')) +/** 在指定临时仓库运行 Git,失败时立即停止。 */ +function git(cwd, ...args) { return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() } +mkdirSync(seed) +git(seed, 'init', '-b', 'main') +git(seed, 'config', 'user.name', 'Local rehearsal') +git(seed, 'config', 'user.email', 'rehearsal@localhost') +writeFileSync(join(seed, 'README.md'), 'Local publish rehearsal\n') +git(seed, 'add', '.') +git(seed, 'commit', '-m', 'seed') +git(root, 'clone', '--bare', seed, remote) +git(root, 'clone', source, project) +process.env.ZTOOLS_CONFIG_DIR = cache +process.env.ZTOOLS_UPSTREAM_URL = remote +process.env.LX_REHEARSAL_REMOTE = remote +process.env.LX_REHEARSAL_RECORD = join(root, 'draft-pr.json') +process.chdir(project) +registerHooks({ + resolve(specifier, context, next) { + if (context.parentURL?.includes('/@ztools-center/plugin-cli/dist/')) { + if (specifier === './auth.js') return { url: 'lx-test:auth', shortCircuit: true } + if (specifier === './github.js') return { url: 'lx-test:github', shortCircuit: true } + } + return next(specifier, context) + }, + load(url, context, next) { + if (url.endsWith('/@ztools-center/plugin-cli/dist/upstream.js')) return { format: 'module', shortCircuit: true, source: adaptCliUpstream(readFileSync(new URL(url), 'utf8')) } + if (url === 'lx-test:auth') return { format: 'module', shortCircuit: true, source: 'export async function ensureAuth(){ return "local-test-only" }' } + if (url === 'lx-test:github') return { format: 'module', shortCircuit: true, source: ` + import fs from 'node:fs'; + export async function getCurrentUser(){ return {login:'local-rehearsal'} } + export async function ensureFork(){ return {clone_url:process.env.LX_REHEARSAL_REMOTE, html_url:'local://fork'} } + export async function syncForkMain(){ return {ok:true} } + export async function findOpenPullRequest(){ return fs.existsSync(process.env.LX_REHEARSAL_RECORD) ? JSON.parse(fs.readFileSync(process.env.LX_REHEARSAL_RECORD)) : null } + export async function pluginExistsUpstream(){ return false } + export async function createPullRequest(token,head,title,body){ const pr={number:1,html_url:'local://draft-pr/1',draft:true,head,title,body}; fs.writeFileSync(process.env.LX_REHEARSAL_RECORD,JSON.stringify(pr,null,2)); return pr } + ` } + return next(url, context) + } +}) +const { publish } = await import('@ztools-center/plugin-cli/dist/publish.js') +await publish() +const record = JSON.parse(readFileSync(process.env.LX_REHEARSAL_RECORD, 'utf8')) +assert.equal(record.draft, true) +assert.equal(record.title, `Add plugin ${manifest.title} v${manifest.version}`) +const tree = git(root, '--git-dir=' + remote, 'ls-tree', '-r', '--name-only', 'plugin/lx-music-ztools') +assert.ok(tree.includes('plugins/lx-music-ztools/src-ztools/plugin.json')) +assert.ok(!tree.includes('node_modules/')) +assert.ok(!tree.includes('src-ztools/dist/')) +assert.ok(!tree.includes('artifacts/')) +assert.ok(existsSync(join(project, '.git'))) +// Reproduce an existing PR on a machine without saved upstream-sync state. +// This forces the CLI to quote the already-published branch in git fetch. +unlinkSync(join(project, '.git', 'ztools-upstream-sync.json')) +await publish() +assert.equal(JSON.parse(readFileSync(process.env.LX_REHEARSAL_RECORD, 'utf8')).number, 1) +process.chdir(source) +mkdirSync('../output/lx-music-ztools', { recursive: true }) +writeFileSync('../output/lx-music-ztools/publish-rehearsal.json', JSON.stringify({ result: 'PASS', mode: 'local Git + mocked GitHub/auth', cli: '1.3.2', checks: ['first publish', 'repeat publish without saved upstream state', 'draft PR body', 'source file filtering'], evidenceRoot: root }, null, 2)) +console.log('PASS 官方 CLI 隔离发布演练;未请求 GitHub,未验证真实 OAuth/PR。') diff --git a/plugins/lx-music-ztools/scripts/test-host.mjs b/plugins/lx-music-ztools/scripts/test-host.mjs new file mode 100644 index 000000000..543076b38 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/test-host.mjs @@ -0,0 +1,62 @@ +import { _electron as electron, expect } from '@playwright/test' +import { createServer } from 'vite' +import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import assert from 'node:assert/strict' + +const data = mkdtempSync(join(tmpdir(), 'lx-ztools-e2e-')) +const evidence = resolve('../output/lx-music-ztools') +mkdirSync(join(data, 'legacy')) +mkdirSync(evidence, { recursive: true }) +const production = process.argv.includes('--production') +const server = production ? null : await createServer({ server: { host: '127.0.0.1', port: 5173, strictPort: true } }) +await server?.listen() +let app +try { + app = await electron.launch({ executablePath: process.env.ZTOOLS_EXECUTABLE || resolve('node_modules/electron/dist/electron.exe'), + args: [process.env.ZTOOLS_APP || 'D:/Program Files/ZTools/resources/app.asar'], + env: { ...process.env, ZTOOLS_DATA_ROOT: data, ZTOOLS_LEGACY_USER_DATA_PATH: join(data, 'legacy'), ZTOOLS_E2E: '1' }, timeout: 45000 }) + const page = await app.firstWindow() + await page.waitForSelector('.search-input', { timeout: 30000 }) + // 在隔离实例中调用宿主实际注册的处理器,避免弹出文件选择对话框。 + const registration = await app.evaluate(async ({ ipcMain, BrowserWindow }, options) => { + const sender = BrowserWindow.getAllWindows().find(x => x.getTitle() === 'ZTools').webContents + const handlers = ipcMain._invokeHandlers + if (options.production) { + const installed = await handlers.get('install-plugin-from-path')({ sender }, options.zip) + return { installed } + } + const result = await handlers.get('upsert-dev-project-by-config-path')({ sender }, options.file) + if (!result.success) throw new Error(JSON.stringify(result)) + const installed = await handlers.get('install-dev-plugin')({ sender }, result.pluginName || 'lx-music-ztools') + return { result, installed } + }, { production, file: resolve('src-ztools/plugin.json'), zip: resolve('../output/lx-music-ztools/lx-music-ztools-0.0.1.zip') }) + assert.equal(registration.installed.success, true) + await page.locator('.search-input').fill('洛雪音乐') + const result = page.locator('.app-item').filter({ hasText: '洛雪音乐' }).first() + try { await expect(result).toBeVisible({ timeout: 15000 }) } + catch (error) { console.log('SEARCH STATE', await page.locator('body').innerText()); throw error } + await result.click() + const match = production ? 'dist/index.html' : '127.0.0.1:5173' + await expect.poll(async () => app.evaluate(({ webContents }, match) => webContents.getAllWebContents().map(x => x.getURL()).find(x => x.includes(match)) || '', match), { timeout: 20000 }).toContain(match) + // WebContentsView 不一定出现在 windows() 中,用其实际 webContents 检查并截图。 + const snapshot = await app.evaluate(async ({ webContents }, match) => { + const wc = webContents.getAllWebContents().find(x => x.getURL().includes(match)) + const state = await wc.executeJavaScript(`({text:document.body.innerText,host:window.services.diagnose(),count:window.services.getLaunchCount(),storage:window.services.checkStorage()})`) + const image = await wc.capturePage() + return { ...state, png: image.toPNG().toString('base64') } + }, match) + assert.equal(snapshot.host.app, 'ZTools') + assert.ok(snapshot.count >= 1) + assert.equal(snapshot.storage, true) + assert.match(snapshot.text, /框架验证版/) + assert.match(snapshot.text, /music/) + writeFileSync(join(evidence, production ? 'host-production.png' : 'host-plugin.png'), Buffer.from(snapshot.png, 'base64')) + delete snapshot.png + writeFileSync(join(evidence, production ? 'host-production.json' : 'host-test.json'), JSON.stringify({ result: 'PASS', mode: production ? 'ZTools app.asar on Electron 42.3.3, isolated data, production ZIP' : 'ZTools app.asar on Electron 42.3.3, isolated data, development plugin', data, registration, snapshot }, null, 2)) + console.log('PASS ZTools 加载、搜索触发、生命周期、preload 和存储', snapshot.host) +} finally { + if (app) await app.close() + await server?.close() +} diff --git a/plugins/lx-music-ztools/scripts/test-install.mjs b/plugins/lx-music-ztools/scripts/test-install.mjs new file mode 100644 index 000000000..0e50f6069 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/test-install.mjs @@ -0,0 +1,32 @@ +import { startHost } from './host-fixture.mjs' +import { expect } from '@playwright/test' +import { mkdirSync, writeFileSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +// Exercise the same ZIP users import, without a development server or project registration. +const manifest = JSON.parse(readFileSync('src-ztools/plugin.json', 'utf8')) +const zip = resolve(process.env.ZTOOLS_PLUGIN_ZIP || resolve('../output/lx-music-ztools', `${manifest.name}-${manifest.version}.zip`)) +mkdirSync('artifacts/install', { recursive: true }) +let host +const report = { zip, status: 'FAIL', isolated: true } +try { + host = await startHost(undefined, { zip }) + await expect.poll(() => host.evaluate('typeof window.lxPlayer'), { timeout: 20000 }).toBe('object') + await expect.poll(() => host.evaluate('document.body.innerText')).toMatch(/搜索/) + report.host = await host.evaluate('window.services.diagnose()') + report.sources = await host.evaluate(`lxHost.invoke('winMain_get_user_api_list')`) + const png = await host.app.evaluate(async ({ webContents }) => { + const wc = webContents.getAllWebContents().find(x => x.getURL().includes('/dist/lx.html')) + return (await wc.capturePage()).toPNG().toString('base64') + }) + writeFileSync('artifacts/install/lxmusic.png', Buffer.from(png, 'base64')) + report.status = 'PASS' + console.log('PASS ZIP install → lxmusic search → original LX player, without development server') +} catch (error) { + report.error = String(error.stack || error) + report.logs = await host?.logs() + process.exitCode = 1 +} finally { + writeFileSync('artifacts/install/report.json', JSON.stringify(report, null, 2)) + await host?.close() +} diff --git a/plugins/lx-music-ztools/scripts/test-lx-renderer.mjs b/plugins/lx-music-ztools/scripts/test-lx-renderer.mjs new file mode 100644 index 000000000..8be183ba8 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/test-lx-renderer.mjs @@ -0,0 +1,39 @@ +import { startHost } from './host-fixture.mjs' +import { expect } from '@playwright/test' +import { mkdirSync, writeFileSync } from 'node:fs' +import assert from 'node:assert/strict' +mkdirSync('artifacts/renderer', { recursive: true }) +let host +try { + host = await startHost('dist/lx.html') + await expect.poll(() => host.evaluate('document.body.innerText'), { timeout: 20000 }).toMatch(/搜索/) + // Test data only: close the original agreement modal to exercise the actual page. + await host.evaluate(`lxHost.invoke('common_set_app_setting',{'common.isAgreePact':true,'common.showChangeLog':false})`) + await expect.poll(() => host.evaluate(`document.body.innerText.includes('在使用本软件前')`)).toBe(false) + await host.evaluate(`location.hash='/setting'`) + let stable = 0 + await expect.poll(async () => { + const visible = await host.evaluate(`location.hash==='#/setting' && !!document.querySelector('#basic')?.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})`) + stable = visible ? stable + 1 : 0 + return stable + }, { timeout: 15000, intervals: [200] }).toBeGreaterThanOrEqual(4) + const layout = await host.evaluate(`(()=>{ + const bounds=document.querySelector('#container').getBoundingClientRect(); + const windowButtons=[...document.querySelectorAll('button')].filter(button=>[...button.querySelectorAll('use')].some(use=>/^#icon-window-(close|minimize)/.test(use.getAttribute('xlink:href')||use.getAttribute('href')||''))); + return {x:bounds.x,y:bounds.y,width:bounds.width,height:bounds.height,viewport:{width:innerWidth,height:innerHeight},visibleWindowButtons:windowButtons.filter(button=>button.checkVisibility()).length,rootShadow:getComputedStyle(document.querySelector('#root')).boxShadow,rightRadius:getComputedStyle(document.querySelector('#right')).borderTopLeftRadius}; + })()`) + assert.equal(layout.visibleWindowButtons, 0, 'LX window controls must be absent from the plugin UI') + // Chromium can round zoomed CSS bounds to fractional pixels. + assert.ok(Math.abs(layout.x) < 0.5); assert.ok(Math.abs(layout.y) < 0.5) + assert.ok(Math.abs(layout.width - layout.viewport.width) < 0.5) + assert.ok(Math.abs(layout.height - layout.viewport.height) < 0.5) + assert.equal(layout.rootShadow, 'none'); assert.equal(layout.rightRadius, '0px') + const screenshot = await host.app.evaluate(async ({ webContents }) => { + const wc = webContents.getAllWebContents().find(x => x.getURL().includes('/dist/lx.html')) + return (await wc.capturePage()).toPNG().toString('base64') + }) + writeFileSync('artifacts/renderer/settings.png', Buffer.from(screenshot, 'base64')) + writeFileSync('artifacts/renderer/report.json', JSON.stringify({ status: 'PASS', data: host.data, layout, text: await host.evaluate('document.body.innerText'), logs: await host.logs() }, null, 2)) + console.log('PASS original LX renderer navigation and source settings') +} catch (error) { writeFileSync('artifacts/renderer/report.json', JSON.stringify({ status: 'FAIL', error: String(error.stack || error), logs: await host?.logs() }, null, 2)); throw error } +finally { await host?.close() } diff --git a/plugins/lx-music-ztools/scripts/test-p0.mjs b/plugins/lx-music-ztools/scripts/test-p0.mjs new file mode 100644 index 000000000..7dcf3b0f4 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/test-p0.mjs @@ -0,0 +1,107 @@ +import { _electron as electron, expect } from '@playwright/test' +import { mkdtempSync, mkdirSync, writeFileSync, readdirSync, copyFileSync, readFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { resolve, join } from 'node:path' +import assert from 'node:assert/strict' +import { generateKeyPairSync, publicEncrypt, constants } from 'node:crypto' + +const data = mkdtempSync(join(tmpdir(), 'lx-ztools-p0-')) +const evidence = resolve('artifacts/p0') +mkdirSync(join(data, 'legacy')) +mkdirSync(evidence, { recursive: true }) +const plugin = join(data, 'plugin') +function copyDirectory(source, target) { + mkdirSync(target, { recursive: true }) + for (const entry of readdirSync(source, { withFileTypes: true })) { + if (entry.isDirectory()) copyDirectory(join(source, entry.name), join(target, entry.name)) + else copyFileSync(join(source, entry.name), join(target, entry.name)) + } +} +copyDirectory(resolve('src-ztools'), plugin) +const manifest = JSON.parse(readFileSync(join(plugin, 'plugin.json'), 'utf8')) +const suffix = '/' + manifest.main +delete manifest.development +writeFileSync(join(plugin, 'plugin.json'), JSON.stringify(manifest, null, 2)) +let app +const result = { data, entry: manifest.main, time: new Date().toISOString(), checks: {} } +try { + app = await electron.launch({ + executablePath: process.env.ZTOOLS_EXECUTABLE || resolve('../tmp/lx-electron-42/electron.exe'), + args: [process.env.ZTOOLS_APP || 'D:/Program Files/ZTools/resources/app.asar'], + env: { ...process.env, ZTOOLS_DATA_ROOT: data, ZTOOLS_LEGACY_USER_DATA_PATH: join(data, 'legacy'), ZTOOLS_E2E: '1' }, + timeout: 45000, + }) + const page = await app.firstWindow() + await page.waitForSelector('.search-input', { timeout: 30000 }) + const registration = await app.evaluate(async ({ ipcMain, BrowserWindow }, file) => { + const sender = BrowserWindow.getAllWindows().find(x => x.getTitle() === 'ZTools').webContents + const registered = await ipcMain._invokeHandlers.get('upsert-dev-project-by-config-path')({ sender }, file) + if (!registered.success) throw new Error(JSON.stringify(registered)) + return ipcMain._invokeHandlers.get('install-dev-plugin')({ sender }, registered.pluginName) + }, join(plugin, 'plugin.json')) + assert.equal(registration.success, true) + // Production entry without a development server: the page must be installed locally. + await page.locator('.search-input').fill('洛雪音乐') + await page.locator('.app-item').filter({ hasText: '洛雪音乐' }).first().click() + await expect.poll(() => app.evaluate(({ webContents }, suffix) => webContents.getAllWebContents().some(x => x.getURL().includes(suffix)), suffix), { timeout: 20000 }).toBe(true) + const evaluate = code => app.evaluate(async ({ webContents }, { code, suffix }) => { + const wc = webContents.getAllWebContents().find(x => x.getURL().includes(suffix)) + return wc.executeJavaScript(code) + }, { code, suffix }) + assert.equal(await evaluate('typeof window.services.probeCapabilities'), 'function', 'P0 requires real source isolation and persistence probes') + result.checks = await evaluate('window.services.probeCapabilities()') + result.settings = await evaluate(`(async()=>{await lxHost.invoke('common_set_app_setting',{'player.volume':0.42,'common.isAgreePact':true,'common.showChangeLog':false});return lxHost.invoke('common_get_app_setting')})()`) + assert.equal(result.settings['player.volume'], 0.42) + assert.deepEqual(await evaluate(`lxHost.invoke('player_list_get')`), []) + const publicKey = generateKeyPairSync('rsa', { modulusLength: 1024 }).publicKey.export({ type: 'spki', format: 'pem' }) + result.pluginPreferences = await app.evaluate(({ webContents }, suffix) => webContents.getAllWebContents().filter(x => x.getURL().includes(suffix)).map(x => x.getLastWebPreferences()), suffix) + app.process().stderr.on('data', bytes => { writeFileSync(join(evidence, 'stderr.log'), bytes, { flag: 'a' }) }) + try { result.guest = await evaluate('window.services.probeSourceIsolation()') } + catch (error) { result.guestFailure = String(error); result.worker = await evaluate(`window.services.probeWorkerIsolation(${JSON.stringify(publicKey)})`) } + result.guestPreferences = await app.evaluate(({ webContents }) => webContents.getAllWebContents().filter(x => x.getType() === 'webview').map(x => ({ url: x.getURL(), preferences: x.getLastWebPreferences() }))) + assert.equal(result.checks.storage.roundtrip, true) + const isolation = result.worker || result.guest + assert.equal(isolation.node, 'undefined') + assert.equal(isolation.ztools, 'undefined') + assert.equal(isolation.hostFileReadable, false) + assert.equal(isolation.syncMd5, '900150983cd24fb0d6963f7d28e17f72') + if (result.worker) { + assert.equal(isolation.networkReadable, false); assert.equal(isolation.requests, 3); assert.equal(isolation.compressed, '中文') + assert.equal(isolation.aes, 'a9313268ef30a1d8ffb00161ce2a5977') + assert.equal(isolation.rsa, publicEncrypt({ key: publicKey, padding: constants.RSA_NO_PADDING }, Buffer.concat([Buffer.alloc(125), Buffer.from('abc')])).toString('hex')) + assert.equal(isolation.terminatedBusyWorker, true) + } + else { assert.equal(result.guestPreferences[0].preferences.contextIsolation, true); assert.equal(result.guestPreferences[0].preferences.webSecurity, true) } + assert.equal(result.checks.storage.stale.name, 'conflict') + assert.equal(result.checks.storage.size.error, true) + assert.equal(result.checks.storage.bulk[0].ok, true) + assert.equal(result.checks.storage.bulk[1].error, true) + assert.deepEqual(result.checks.localSQLite, { rollback: true, reopen: true }) + await expect.poll(() => evaluate(`!!document.querySelector('#container') || document.body.innerText.includes('原 LX 组件')`)).toBe(true) + const instance = await evaluate(`window.services.startLifecycleProbe()`) + await evaluate('window.ztools.outPlugin(false)') + await new Promise(resolve => setTimeout(resolve, 1500)) + const hidden = await evaluate('window.services.getLifecycleProbe()') + assert.equal(hidden.id, instance.id); assert.ok(hidden.ticks >= instance.ticks + 5) + await page.locator('.search-input').fill('洛雪音乐') + await page.locator('.app-item').filter({ hasText: '洛雪音乐' }).first().click() + const reentered = await evaluate('window.services.getLifecycleProbe()') + assert.equal(reentered.id, instance.id) + const detached = await page.evaluate(() => window.ztools.detachPlugin()) + assert.equal(detached.success, true) + const afterDetach = await evaluate('window.services.getLifecycleProbe()') + assert.equal(afterDetach.id, instance.id) + result.lifecycle = { hidden, reentered, detached, afterDetach } + await evaluate('window.ztools.outPlugin(true)') + await expect.poll(() => app.evaluate(({ webContents }, suffix) => webContents.getAllWebContents().filter(x => x.getURL().includes(suffix)).length, suffix)).toBe(0) + result.status = 'PASS' +} catch (error) { + if (app) result.webContentsOnFailure = await app.evaluate(({ webContents }) => webContents.getAllWebContents().map(x => ({ type: x.getType(), url: x.getURL(), preferences: x.getLastWebPreferences() }))).catch(() => []) + result.status = 'FAIL' + result.error = String(error.stack || error) + process.exitCode = 1 +} finally { + writeFileSync(join(evidence, 'report.json'), JSON.stringify(result, null, 2)) + console.log(JSON.stringify(result, null, 2)) + await app?.close() +} diff --git a/plugins/lx-music-ztools/scripts/test-player.mjs b/plugins/lx-music-ztools/scripts/test-player.mjs new file mode 100644 index 000000000..97838ee5a --- /dev/null +++ b/plugins/lx-music-ztools/scripts/test-player.mjs @@ -0,0 +1,45 @@ +import { startHost } from './host-fixture.mjs' +import { expect } from '@playwright/test' +import { readFileSync, mkdirSync, writeFileSync } from 'node:fs' +import assert from 'node:assert/strict' +import audioFixture from '../tests/ztools/fixtures/audio-server.cjs' +mkdirSync('artifacts/player', { recursive: true }) +const template = readFileSync('tests/ztools/fixtures/source-api-v2.js', 'utf8') +const fixture = await audioFixture.startAudioServer(template) +let host +const report = { evidence: 'Actual original LX audio engine; local source and generated WAV fixture' } +try { + host = await startHost('dist/lx.html') + const invoke = (channel, params) => host.evaluate(`lxHost.invoke(${JSON.stringify(channel)},${JSON.stringify(params)})`) + await expect.poll(() => host.evaluate('typeof window.lxPlayer'), { timeout: 15000 }).toBe('object') + const source = await invoke('winMain_import_user_api', template.replaceAll('__BASE_URL__', fixture.base)) + await invoke('common_set_app_setting', { 'common.isAgreePact': true, 'common.showChangeLog': false, 'common.apiSource': source.apiInfo.id, 'player.volume': 0.01 }) + await invoke('winMain_set_user_api', source.apiInfo.id) + await expect.poll(async () => (await invoke('winMain_get_user_api_status')).status).toBe(true) + const tracks = ['one', 'two'].map(id => ({ id: `kw_${id}`, source: 'kw', name: `Test ${id}`, singer: 'Local fixture', interval: '00:12', meta: { musicId: id, qualitys: [{ type: '128k', size: '512KB' }], _qualitys: { '128k': {} }, albumName: 'Test' } })) + await invoke('player_list_music_add', { id: 'default', musicInfos: tracks, addMusicLocationType: 'bottom' }) + await host.evaluate(`lxPlayer.dispatch({type:'playList',listId:'default',index:0})`) + await expect.poll(() => host.evaluate('lxPlayer.getState().progress'), { timeout: 20000 }).toBeGreaterThan(0.6) + const playing = await host.evaluate('lxPlayer.getState()') + assert.ok(playing.duration > 11 && playing.duration < 13) + assert.equal(playing.status, 'playing') + await host.evaluate(`document.querySelector('#player > div[aria-label]').click()`) + await expect.poll(() => host.evaluate(`([...document.querySelectorAll('button')].some(button=>button.checkVisibility() && [...button.querySelectorAll('use')].some(use=>use.getAttribute('xlink:href')==='#icon-window-hide')))`)).toBe(true) + const detailControls = await host.evaluate(`([...document.querySelectorAll('button')].filter(button=>button.checkVisibility() && [...button.querySelectorAll('use')].some(use=>/^#icon-window-(minimize|close)/.test(use.getAttribute('xlink:href')||''))).length)`) + assert.equal(detailControls, 0, 'Playback details must also omit desktop window buttons') + await host.evaluate(`([...document.querySelectorAll('button')].find(button=>button.checkVisibility() && [...button.querySelectorAll('use')].some(use=>use.getAttribute('xlink:href')==='#icon-window-hide'))).click()`) + await host.evaluate(`lxPlayer.dispatch({type:'pause'})`) + const paused = await host.evaluate('lxPlayer.getState().progress') + await new Promise(resolve => setTimeout(resolve, 400)) + assert.ok(Math.abs(await host.evaluate('lxPlayer.getState().progress') - paused) < 0.1) + await host.evaluate(`lxPlayer.dispatch({type:'play'})`) + await host.evaluate('ztools.outPlugin(false)') + await new Promise(resolve => setTimeout(resolve, 1000)) + assert.ok(await host.evaluate('lxPlayer.getState().progress') > paused + 0.5) + await host.evaluate(`lxPlayer.dispatch({type:'next'})`) + await expect.poll(() => host.evaluate('lxPlayer.getState().name'), { timeout: 10000 }).toBe('Test two') + await expect.poll(() => host.evaluate('lxPlayer.getState().progress')).toBeGreaterThan(0.3) + report.status = 'PASS'; report.playing = playing; report.requests = fixture.requests + console.log('PASS source-driven original audio playback, metadata/time, pause/resume, background, next track') +} catch (error) { report.status = 'FAIL'; report.error = String(error.stack || error); report.logs = await host?.logs(); process.exitCode = 1 } +finally { writeFileSync('artifacts/player/report.json', JSON.stringify(report, null, 2)); await host?.close(); await fixture.close() } diff --git a/plugins/lx-music-ztools/scripts/test-source-ui.mjs b/plugins/lx-music-ztools/scripts/test-source-ui.mjs new file mode 100644 index 000000000..267d479ed --- /dev/null +++ b/plugins/lx-music-ztools/scripts/test-source-ui.mjs @@ -0,0 +1,42 @@ +import { startHost } from './host-fixture.mjs' +import { expect } from '@playwright/test' +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' +import { join } from 'node:path' +import audioFixture from '../tests/ztools/fixtures/audio-server.cjs' +mkdirSync('artifacts/source-ui', { recursive: true }) +const template = readFileSync('tests/ztools/fixtures/source-api-v2.js', 'utf8') +const fixture = await audioFixture.startAudioServer(template + '\n// online source variant') +let host +const report = { evidence: 'Original LX controls in isolated ZTools. Native file picker alone returns a controlled fixture path.' } +try { + host = await startHost('dist/lx.html') + await expect.poll(() => host.evaluate('typeof window.lxPlayer')).toBe('object') + await host.evaluate(`window.__testSourceErrors=[];window.addEventListener('unhandledrejection',event=>window.__testSourceErrors.push(String(event.reason?.stack||event.reason)))`) + await host.evaluate(`lxHost.invoke('common_set_app_setting',{'common.isAgreePact':true,'common.showChangeLog':false})`) + const scriptPath = join(host.data, '音源测试.js') + writeFileSync(scriptPath, template.replaceAll('__BASE_URL__', fixture.base)) + await host.app.evaluate(({ dialog }, file) => { + dialog.showOpenDialogSync = () => [file] + dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [file] }) + }, scriptPath) + await host.evaluate(`location.hash='/setting'`) + await expect.poll(() => host.evaluate(`!!document.querySelector('#basic')?.checkVisibility()`)).toBe(true) + const click = text => host.evaluate(`(()=>{const button=[...document.querySelectorAll('button')].find(x=>x.textContent.trim()===${JSON.stringify(text)} && x.checkVisibility());if(!button)throw new Error('Button missing: '+${JSON.stringify(text)});button.scrollIntoView({block:'center'});button.click()})()`) + await click('自定义源管理') + await expect.poll(() => host.evaluate(`document.body.innerText.includes('本地导入')`)).toBe(true) + await click('本地导入') + await expect.poll(() => host.evaluate(`lxHost.invoke('winMain_get_user_api_list').then(x=>x.length)`), { timeout: 10000 }).toBe(1) + await click('在线导入') + await expect.poll(() => host.evaluate(`!!document.querySelector('input[type=url]')?.checkVisibility()`)).toBe(true) + await host.evaluate(`(()=>{const input=document.querySelector('input[type=url]');input.value=${JSON.stringify(fixture.base + '/source.js')};input.dispatchEvent(new Event('input',{bubbles:true}))})()`) + await click('导入') + await expect.poll(() => host.evaluate(`lxHost.invoke('winMain_get_user_api_list').then(x=>x.length)`), { timeout: 10000 }).toBe(2) + report.status = 'PASS'; report.sources = await host.evaluate(`lxHost.invoke('winMain_get_user_api_list')`) + const png = await host.app.evaluate(async ({ webContents }) => { + const wc = webContents.getAllWebContents().find(x => x.getURL().includes('/dist/lx.html')) + return (await wc.capturePage()).toPNG().toString('base64') + }) + writeFileSync('artifacts/source-ui/import.png', Buffer.from(png, 'base64')) + console.log('PASS original LX local file and URL source import controls') +} catch (error) { report.status = 'FAIL'; report.error = String(error.stack || error); report.logs = await host?.logs(); report.rejections = await host?.evaluate('window.__testSourceErrors'); process.exitCode = 1 } +finally { writeFileSync('artifacts/source-ui/report.json', JSON.stringify(report, null, 2)); await host?.close(); await fixture.close() } diff --git a/plugins/lx-music-ztools/scripts/test-source.mjs b/plugins/lx-music-ztools/scripts/test-source.mjs new file mode 100644 index 000000000..75f7c1065 --- /dev/null +++ b/plugins/lx-music-ztools/scripts/test-source.mjs @@ -0,0 +1,43 @@ +import { startHost } from './host-fixture.mjs' +import http from 'node:http' +import assert from 'node:assert/strict' +import { readFileSync, mkdirSync, writeFileSync } from 'node:fs' +mkdirSync('artifacts/source', { recursive: true }) +const server = http.createServer((req, res) => { + const url = new URL(req.url, 'http://127.0.0.1') + if (url.pathname !== '/resolve') { res.writeHead(404); res.end(); return } + assert.equal(req.headers['x-lx-hash'], '900150983cd24fb0d6963f7d28e17f72') + const send = () => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ url: base + '/audio.wav?id=' + url.searchParams.get('id') })) } + if (url.searchParams.get('id') === 'slow') setTimeout(send, 1000) + else send() +}) +await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) +const base = `http://127.0.0.1:${server.address().port}` +let host +const record = { evidence: 'Actual isolated ZTools; controlled local source and server only' } +try { + host = await startHost() + const invoke = (channel, params) => host.evaluate(`lxHost.invoke(${JSON.stringify(channel)},${JSON.stringify(params)})`) + const script = readFileSync('tests/ztools/fixtures/source-api-v2.js', 'utf8').replaceAll('__BASE_URL__', base) + const imported = await invoke('winMain_import_user_api', script) + assert.equal(imported.apiList.length, 1) + await invoke('winMain_set_user_api', imported.apiInfo.id) + const status = await invoke('winMain_get_user_api_status') + assert.equal(status.status, true) + assert.deepEqual(status.apiInfo.sources.kw.qualitys, ['128k', '320k']) + const input = (id, key = 'one') => ({ requestKey: key, data: { source: 'kw', action: 'musicUrl', info: { type: '128k', musicInfo: { id } } } }) + const response = await invoke('winMain_request_user_api', input('ok')) + assert.equal(response.data.url, base + '/audio.wav?id=ok') + await assert.rejects(invoke('winMain_request_user_api', { ...input('bad'), data: { ...input('bad').data, info: { type: 'unknown', musicInfo: { id: 'bad' } } } }), /音质|quality/) + const slow = invoke('winMain_request_user_api', input('slow', 'slow')) + const cancelled = assert.rejects(slow, /取消|切换|cancel|changed/i) + await new Promise(resolve => setTimeout(resolve, 100)) + await invoke('winMain_set_user_api', 'temp') + await cancelled + await invoke('winMain_set_user_api', imported.apiInfo.id) + assert.equal((await invoke('winMain_request_user_api', input('again', 'again'))).data.url, base + '/audio.wav?id=again') + await assert.rejects(invoke('winMain_import_user_api', 'not a valid script'), /无效|invalid/) + record.status = 'PASS'; record.source = { status, response }; record.data = host.data + console.log('PASS source import, init, synchronous crypto, HTTP callback, request, quality validation and switch cancellation') +} catch (error) { record.status = 'FAIL'; record.error = String(error.stack || error); record.logs = await host?.logs(); process.exitCode = 1 } +finally { writeFileSync('artifacts/source/report.json', JSON.stringify(record, null, 2)); await host?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)) } diff --git a/plugins/lx-music-ztools/src-ztools/LICENSE b/plugins/lx-music-ztools/src-ztools/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/lx-music-ztools/src-ztools/NOTICE b/plugins/lx-music-ztools/src-ztools/NOTICE new file mode 100644 index 000000000..995e53c6d --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/NOTICE @@ -0,0 +1,6 @@ +LX Music for ZTools — 第三方原生移植开发版 +源码与图标来源:lyswhut/lx-music-desktop (https://github.com/lyswhut/lx-music-desktop) +上游许可:Apache License 2.0,见同目录 LICENSE。 +上游源码基线:323e4bac,LX Music 2.12.6。 +宿主桥接、存储、构建及隔离音源适配由本项目实现;修改后的运行模块带有来源与改动标注。 +本版本支持已验证的音源导入与播放流程,下载、同步等完整功能仍在移植;不是 LX 官方发行版。 diff --git a/plugins/lx-music-ztools/src-ztools/logo.png b/plugins/lx-music-ztools/src-ztools/logo.png new file mode 100644 index 000000000..712f00b99 Binary files /dev/null and b/plugins/lx-music-ztools/src-ztools/logo.png differ diff --git a/plugins/lx-music-ztools/src-ztools/plugin.json b/plugins/lx-music-ztools/src-ztools/plugin.json new file mode 100644 index 000000000..8e668e02e --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/plugin.json @@ -0,0 +1,25 @@ +{ + "name": "lx-music-ztools", + "title": "LX Music", + "description": "LX Music 原生播放器开发版:支持导入和使用自定义音源,完整功能正在移植", + "author": "jiang66-so", + "version": "0.0.2", + "main": "dist/lx.html", + "preload": "preload/services.js", + "logo": "logo.png", + "platform": [ + "win32" + ], + "pluginSetting": { "backgroundRunning": true, "single": true }, + "features": [ + { + "code": "music", + "explain": "LX Music 音乐播放器", + "cmds": [ + "LX Music", + "洛雪音乐", + "lxmusic" + ] + } + ] +} diff --git a/plugins/lx-music-ztools/src-ztools/preload/package-lock.json b/plugins/lx-music-ztools/src-ztools/preload/package-lock.json new file mode 100644 index 000000000..8a297b100 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/package-lock.json @@ -0,0 +1,303 @@ +{ + "name": "preload", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "font-list": "^2.1.0", + "iconv-lite": "^0.7.3", + "jschardet": "^3.1.4", + "music-metadata": "^11.15.0", + "needle": "^3.3.1", + "tunnel": "^0.0.6" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/@borewit/text-codec/-/text-codec-0.2.3.tgz", + "integrity": "sha512-49CycTz1aAZmvFcwkeFyhud1HUmRIB5Tabf7ALV+VA4Iu85MTfz2cUwzPagMFAdVBvrWdkCxG67I30tUwwpKsw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmmirror.com/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/font-list": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/font-list/-/font-list-2.1.0.tgz", + "integrity": "sha512-GMDcD8Wymmhz7pxOzu+Gm/bbVzxvUHJJSiLPCF/rt02g2W7P08Nhz6XiaYjXo8xklXWhLoCiyi1cBPwwASC2fQ==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/jschardet": { + "version": "3.1.4", + "resolved": "https://registry.npmmirror.com/jschardet/-/jschardet-3.1.4.tgz", + "integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==", + "license": "LGPL-2.1+", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/media-typer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-2.0.0.tgz", + "integrity": "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/music-metadata": { + "version": "11.15.0", + "resolved": "https://registry.npmmirror.com/music-metadata/-/music-metadata-11.15.0.tgz", + "integrity": "sha512-TN+kO1/oOc8UzDW5N3vSDncBcv9WyNnQQe/NFMcFWq6G1+zVeYUkGvkYpQ/S8wb8vKtUD7i78dIaY0cv+cmPRA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.2", + "@tokenizer/token": "^0.3.0", + "content-type": "^2.1.0", + "debug": "^4.4.3", + "file-type": "^21.3.4", + "media-typer": "^2.0.0", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0", + "win-guid": "^0.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/strtok3": { + "version": "10.7.0", + "resolved": "https://registry.npmmirror.com/strtok3/-/strtok3-10.7.0.tgz", + "integrity": "sha512-iAqrCpp7UXD2FZiHWhLXqdJ6CmNeVzRkkHAyyhhKMVtbVm2KtPX3D/bJNonvtcyLTHS4rwqP2pz9fsjK1K4z0Q==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/token-types": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/token-types/-/token-types-6.2.0.tgz", + "integrity": "sha512-s9euYHM//T3loJtbNUYdbAzsMlvKBeRS+XCLFy+UGosMQH19Hwjap5k19i4PPAEJJBsVIpMpVTxl4fIqCuNfUg==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.3", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/uint8array-extras": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/win-guid": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/win-guid/-/win-guid-0.2.1.tgz", + "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==", + "license": "MIT" + } + } +} diff --git a/plugins/lx-music-ztools/src-ztools/preload/package.json b/plugins/lx-music-ztools/src-ztools/preload/package.json new file mode 100644 index 000000000..0d385ae9e --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/package.json @@ -0,0 +1,11 @@ +{ + "type": "commonjs", + "dependencies": { + "font-list": "^2.1.0", + "iconv-lite": "^0.7.3", + "jschardet": "^3.1.4", + "music-metadata": "^11.15.0", + "needle": "^3.3.1", + "tunnel": "^0.0.6" + } +} diff --git a/plugins/lx-music-ztools/src-ztools/preload/probe-guest.cjs b/plugins/lx-music-ztools/src-ztools/preload/probe-guest.cjs new file mode 100644 index 000000000..70a78279e --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/probe-guest.cjs @@ -0,0 +1,8 @@ +const { contextBridge } = require('electron') +const { createHash } = require('node:crypto') + +// This isolated world contains trusted code only; no Node handle crosses the bridge. +contextBridge.exposeInMainWorld('lxProbe', { + md5(value) { return createHash('md5').update(String(value)).digest('hex') }, +}) +window.addEventListener('beforeunload', event => { event.returnValue = false }) diff --git a/plugins/lx-music-ztools/src-ztools/preload/probes.cjs b/plugins/lx-music-ztools/src-ztools/preload/probes.cjs new file mode 100644 index 000000000..fb90c013c --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/probes.cjs @@ -0,0 +1,94 @@ +const { randomUUID } = require('node:crypto') +const { pathToFileURL } = require('node:url') +const path = require('node:path') +const { startWorkerServer } = require('./worker-server.cjs') +const { DatabaseSync } = require('node:sqlite') +const fs = require('node:fs') + +/** Characterize actual host storage results rather than assuming PouchDB semantics. */ +function probeCapabilities(ztools) { + const id = `lx:p0:${randomUUID()}` + const db = ztools.db + const initial = { _id: id, value: 'old' } + const put = db.put(initial) + const read = db.get(id) + const second = db.put({ _id: id, _rev: read?._rev, value: 'new' }) + const stale = db.put({ _id: id, _rev: read?._rev, value: 'stale' }) + const size = db.put({ _id: `${id}:large`, value: 'a'.repeat(1024 * 1024 + 1) }) + const bulk = db.bulkDocs([{ _id: `${id}:valid`, value: 1 }, { _id: `${id}:large`, value: 'a'.repeat(1024 * 1024 + 1) }]) + const partial = db.get(`${id}:valid`) + for (const key of [id, `${id}:large`, `${id}:valid`]) { + const doc = db.get(key) + if (doc) db.remove(doc) + } + const dbPath = path.join(ztools.getPath('pluginData'), `probe-${randomUUID()}.sqlite`) + fs.mkdirSync(path.dirname(dbPath), { recursive: true }) + let sqlite = new DatabaseSync(dbPath) + sqlite.exec('CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT); INSERT INTO test VALUES (1,\'old\')') + try { sqlite.exec("BEGIN; UPDATE test SET value='new'; INSERT INTO test VALUES (1,'duplicate'); COMMIT") } + catch { sqlite.exec('ROLLBACK') } + const rollback = sqlite.prepare('SELECT value FROM test WHERE id=1').get().value === 'old' + sqlite.close() + sqlite = new DatabaseSync(dbPath) + const reopen = sqlite.prepare('SELECT value FROM test WHERE id=1').get().value === 'old' + sqlite.close(); fs.unlinkSync(dbPath) + return { + localSQLite: { rollback, reopen }, + host: { name: ztools.getAppName(), version: ztools.getAppVersion(), runtime: { ...process.versions } }, + storage: { roundtrip: read?.value === 'old', put, second, stale, size, bulk, partial: Boolean(partial), callerIdMutated: initial._id !== id }, + paths: { pluginData: ztools.getPath('pluginData') }, + } +} + +/** Create an opaque-origin guest with a fresh session and synchronous restricted bridge. */ +async function probeSourceIsolation() { + await new Promise(resolve => setTimeout(resolve, 1000)) + const guest = document.createElement('webview') + guest.setAttribute('partition', `lx-source-probe-${randomUUID()}`) + guest.setAttribute('webpreferences', 'contextIsolation=yes,nodeIntegration=no,sandbox=no,webSecurity=yes,backgroundThrottling=no') + guest.setAttribute('preload', pathToFileURL(path.join(__dirname, 'probe-guest.cjs')).href) + guest.style.cssText = 'width:1px;height:1px;position:fixed;left:-100px;' + const ready = new Promise((resolve, reject) => { + guest.addEventListener('dom-ready', resolve, { once: true }) + guest.addEventListener('did-fail-load', event => reject(new Error(event.errorDescription)), { once: true }) + setTimeout(() => reject(new Error(`Guest initialization timed out: method=${typeof guest.getWebContentsId}, connected=${guest.isConnected}, html=${guest.outerHTML}`)), 8000) + }) + guest.setAttribute('src', 'data:text/html,' + encodeURIComponent(`Isolated source`)) + document.body.append(guest) + await ready + const result = await guest.executeJavaScript(`(async()=>({node:typeof require,ztools:typeof ztools,parentBridge:typeof parent.services,syncMd5:lxProbe.md5('abc'),hostFileReadable:await fetch('file:///C:/Windows/win.ini').then(()=>true,()=>false),origin:location.origin}))()`) + // Keep the guest alive for the runner to inspect actual webPreferences. + window.__lxProbeGuest = guest + return result +} +async function probeWorkerIsolation(publicKey) { + const server = await startWorkerServer('probe-worker.js') + const frame = document.createElement('iframe') + frame.style.display = 'none' + frame.setAttribute('sandbox', 'allow-scripts allow-same-origin') + let listener + let probeResult + try { + const result = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Worker probe timed out')), 8000) + listener = event => { + const { data } = event + if (event.source !== frame.contentWindow || data.channel !== server.token) return + if (data.type === 'ready') { frame.contentWindow.postMessage({ channel: server.token, type: 'probe', publicKey, url: server.url + '?denied' }, new URL(server.url).origin); return } + if (data.type === 'probe-result') { + probeResult = data.result + frame.contentWindow.postMessage({ channel: server.token, type: 'busy' }, new URL(server.url).origin) + setTimeout(() => frame.contentWindow.postMessage({ channel: server.token, type: 'dispose' }, new URL(server.url).origin), 200) + return + } + clearTimeout(timer) + data.type === 'error' ? reject(new Error(data.error)) : resolve({ ...probeResult, terminatedBusyWorker: data.type === 'stopped' }) + } + window.addEventListener('message', listener) + frame.src = server.url + document.body.append(frame) + }) + return { ...result, mode: 'browser-worker-csp', requests: server.getRequests() } + } finally { window.removeEventListener('message', listener); frame.remove(); await server.close() } +} +module.exports = { probeCapabilities, probeSourceIsolation, probeWorkerIsolation } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/app-utils.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/app-utils.cjs new file mode 100644 index 000000000..331892d92 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/app-utils.cjs @@ -0,0 +1,16 @@ +const context = require('./context.cjs') +const { CMMON_EVENT_NAME } = require('../upstream/common/ipcNames.cjs') +module.exports = { + updateSetting(patch = {}) { + const before = context.lx.settings.get() + const setting = context.lx.settings.update(patch) + const updatedSettingKeys = Object.keys(patch).filter(key => !Object.is(before[key], setting[key])) + const updatedSetting = Object.fromEntries(updatedSettingKeys.map(key => [key, setting[key]])) + context.bridge.setSnapshot(CMMON_EVENT_NAME.get_app_setting, setting) + return { setting, updatedSettingKeys, updatedSetting } + }, + saveAppHotKeyConfig(config) { + context.lx.appHotKeyConfig = structuredClone(config) + context.lx.database.prepare("INSERT INTO plugin_kv VALUES ('hotkey','config',?) ON CONFLICT(namespace,key) DO UPDATE SET value=excluded.value").run(JSON.stringify(config)) + }, +} diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/bridge.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/bridge.cjs new file mode 100644 index 000000000..262cd911a --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/bridge.cjs @@ -0,0 +1,36 @@ +const { EventEmitter } = require('node:events') + +/** Dispatch only explicitly registered LX business channels. */ +function createBridge(handlers = {}) { + const table = new Map(Object.entries(handlers)) + const events = new EventEmitter() + const snapshots = new Map() + let disposed = false + const error = (code, message) => Object.assign(new Error(message), { code }) + const bridge = { + async invoke(channel, params) { + if (disposed) throw error('DISPOSED', '插件服务已终止') + if (!table.has(channel)) throw error('UNKNOWN_CHANNEL', `未适配的业务通道:${channel}`) + return table.get(channel)(params) + }, + send(channel, params) { void bridge.invoke(channel, params).catch(cause => events.emit('bridge:error', { channel, code: cause.code, message: cause.message })) }, + register(channel, handler) { + if (table.has(channel)) throw error('DUPLICATE_CHANNEL', `重复业务通道:${channel}`) + table.set(channel, handler) + }, + setSnapshot(key, value) { snapshots.set(key, structuredClone(value)) }, + readSnapshot(key) { + if (!snapshots.has(key)) throw error('SNAPSHOT_UNREADY', `同步快照未准备:${key}`) + return structuredClone(snapshots.get(key)) + }, + on(channel, listener) { events.on(channel, listener) }, + once(channel, listener) { events.once(channel, listener) }, + off(channel, listener) { events.off(channel, listener) }, + offAll(channel) { events.removeAllListeners(channel) }, + subscribe(channel, listener) { events.on(channel, listener); return () => events.off(channel, listener) }, + emit(channel, params) { if (!disposed) events.emit(channel, params) }, + async dispose() { disposed = true; events.removeAllListeners(); table.clear(); snapshots.clear() }, + } + return bridge +} +module.exports = { createBridge } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/context.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/context.cjs new file mode 100644 index 000000000..71846914c --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/context.cjs @@ -0,0 +1,2 @@ +// Separate from renderer window.lx; original UI initialization must not overwrite services. +module.exports = { lx: null, bridge: null } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/data.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/data.cjs new file mode 100644 index 000000000..de155352c --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/data.cjs @@ -0,0 +1,26 @@ +const backend = require('./database.cjs') +const path = require('node:path') +const namespaces = ['list', 'download', 'dislike_list', 'lyric', 'music_url', 'music_other_source'] + +/** Preserve the upstream list/cache contracts and invalidate memory after SQL failures. */ +function createDataService(directory) { + backend.init(directory) + const modules = new Map() + const file = namespace => path.join(__dirname, '../upstream/main/worker/dbService/modules', namespace, 'index.cjs') + const invalidate = () => { for (const namespace of namespaces) delete require.cache[file(namespace)]; modules.clear() } + invalidate() + return { + call(namespace, method, args = []) { + if (!namespaces.includes(namespace)) throw new Error(`Unknown data namespace: ${namespace}`) + let module = modules.get(namespace) + if (!module) { module = require(file(namespace)); modules.set(namespace, module) } + if (!Object.hasOwn(module, method) || typeof module[method] !== 'function') throw new Error(`Unknown data method: ${namespace}.${method}`) + try { return structuredClone(module[method](...structuredClone(args))) } + catch (error) { invalidate(); throw error } + }, + transaction(fn) { try { return backend.getDB().transaction(fn)() } catch (error) { invalidate(); throw error } }, + getDatabase: backend.getDB, + close() { invalidate(); backend.close() }, + } +} +module.exports = { createDataService } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/database.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/database.cjs new file mode 100644 index 000000000..61ec87b0b --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/database.cjs @@ -0,0 +1,64 @@ +const { DatabaseSync } = require('node:sqlite') +const { mkdirSync } = require('node:fs') +const path = require('node:path') +let current + +/** Adapt the subset of better-sqlite3 used by LX without shipping native bindings. */ +function openDatabase(file) { + if (file !== ':memory:') mkdirSync(path.dirname(file), { recursive: true }) + const native = new DatabaseSync(file) + let depth = 0 + const normalize = value => value === undefined ? null : typeof value === 'boolean' ? Number(value) : value + const parameters = args => args.map(arg => arg && Object.getPrototypeOf(arg) === Object.prototype + ? Object.fromEntries(Object.entries(arg).map(([key, value]) => [key, normalize(value)])) : normalize(arg)) + const db = { + exec(sql) { native.exec(sql) }, + pragma(sql) { return native.prepare(`PRAGMA ${sql}`).all().map(row => ({ ...row })) }, + prepare(sql) { + const statement = native.prepare(sql) + statement.setAllowUnknownNamedParameters(true) + return { + run(...args) { return statement.run(...parameters(args)) }, + get(...args) { const row = statement.get(...parameters(args)); return row && { ...row } }, + all(...args) { return statement.all(...parameters(args)).map(row => ({ ...row })) }, + } + }, + transaction(fn) { + return (...args) => { + const savepoint = `lx_${depth++}` + native.exec(`SAVEPOINT ${savepoint}`) + try { + const result = fn(...args) + if (result && typeof result.then === 'function') throw new TypeError('SQL transaction callbacks must be synchronous') + native.exec(`RELEASE SAVEPOINT ${savepoint}`) + return result + } catch (error) { + native.exec(`ROLLBACK TO SAVEPOINT ${savepoint}; RELEASE SAVEPOINT ${savepoint}`) + throw error + } finally { depth-- } + } + }, + close() { native.close() }, + } + return db +} + +/** Initialize only plugin-local data; never open an existing desktop LX database. */ +function init(directory) { + const schema = require('../upstream/main/worker/dbService/tables.cjs') + const file = path.join(directory, 'lx-ztools.data.sqlite') + current = openDatabase(file) + current.pragma('journal_mode=WAL') + const exists = current.prepare("SELECT name FROM sqlite_master WHERE name='db_info'").get() + if (!exists) current.transaction(() => { + current.exec([...schema.default.values()].join('\n')) + current.prepare('INSERT INTO db_info (field_name,field_value) VALUES (?,?)').run('version', schema.DB_VERSION) + })() + current.exec('CREATE TABLE IF NOT EXISTS plugin_kv(namespace TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY(namespace,key))') + if (exists) require('../upstream/main/worker/dbService/migrate.cjs').default(current) + return Boolean(exists) +} +module.exports = { openDatabase, init, getDB: () => { + if (!current) throw new Error('Plugin database is not initialized') + return current +}, close: () => { current?.close(); current = undefined } } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/files.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/files.cjs new file mode 100644 index 000000000..0c4a5fd69 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/files.cjs @@ -0,0 +1,61 @@ +const fs = require('node:fs') +const path = require('node:path') +const { createKV } = require('./kv.cjs') + +/** Resolve existing ancestors so path traversal and symlinks cannot escape a grant. */ +function canonical(target) { + let current = path.resolve(target) + const suffix = [] + while (!fs.existsSync(current)) { + const parent = path.dirname(current) + if (parent === current) throw new Error('Cannot resolve file path') + suffix.unshift(path.basename(current)); current = parent + } + return path.join(fs.realpathSync.native(current), ...suffix) +} +function createFileScope(roots, grants = [], onChange) { + const normalize = value => process.platform === 'win32' ? value.toLowerCase() : value + const entries = [...roots.map(value => ({ path: canonical(value), directory: true })), ...grants] + return { + grant(target, directory = false) { + const value = { path: canonical(target), directory } + if (!entries.some(entry => normalize(entry.path) === normalize(value.path) && entry.directory === directory)) { entries.push(value); onChange?.(entries.slice(roots.length)) } + }, + authorize(target) { + if (typeof target !== 'string' || !target || target.includes('\0')) throw new TypeError('无效文件路径') + const value = canonical(target), key = normalize(value) + if (!entries.some(entry => key === normalize(entry.path) || entry.directory && key.startsWith(normalize(entry.path.endsWith(path.sep) ? entry.path : entry.path + path.sep)))) throw Object.assign(new Error('尚未授权此文件或目录,请重新选择'), { code: 'PATH_NOT_GRANTED' }) + return value + }, + } +} +function createFileService(database, directory) { + const store = createKV(database, 'files') + const scope = createFileScope([directory], store.get('grants', []), grants => store.set('grants', grants)) + const helpers = require('../upstream/common/utils/nodejs.cjs') + const paths = new Set(['checkPath', 'checkAndCreateDir', 'getFileStats', 'createDir', 'removeFile', 'readFile', 'saveLxConfigFile', 'readLxConfigFile', 'saveStrToFile', 'copyFile', 'moveFile']) + const pure = new Set(['gzipData', 'gunzipData']) + return { + scope, + async call(method, args) { + if (!paths.has(method) && !pure.has(method)) throw new Error(`Unknown file operation: ${method}`) + const values = [...args] + if (paths.has(method)) { + try { values[0] = scope.authorize(values[0]) } + catch (error) { if (method === 'checkPath') return false; throw error } + if (method === 'copyFile' || method === 'moveFile') values[1] = scope.authorize(values[1]) + } + if (method === 'readFile' && /\.js$/i.test(values[0]) && (await fs.promises.stat(values[0])).size > 32 * 1024 * 1024) throw new Error('音源脚本文件过大') + if (method === 'saveLxConfigFile') { + const target = values[0].endsWith('.lxmc') ? values[0] : values[0] + '.lxmc' + const content = await helpers.gzipData(JSON.stringify(values[1])) + await fs.promises.writeFile(target, content) + return + } + const result = await helpers[method](...values) + if (method === 'getFileStats' && result) return { size: result.size, mtimeMs: result.mtimeMs, ctimeMs: result.ctimeMs, mtime: result.mtime, ctime: result.ctime, isFile: result.isFile(), isDirectory: result.isDirectory() } + return result + }, + } +} +module.exports = { createFileScope, createFileService } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/host.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/host.cjs new file mode 100644 index 000000000..413498021 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/host.cjs @@ -0,0 +1,38 @@ +const fs = require('node:fs') +const path = require('node:path') +const { createKV } = require('./kv.cjs') +const { CMMON_EVENT_NAME, WIN_MAIN_RENDERER_EVENT_NAME: names } = require('../upstream/common/ipcNames.cjs') + +/** Translate native dialogs and selected-file capabilities through public ZTools APIs. */ +function bindHost({ bridge, files, database, directory, ztools, clipboard }) { + const host = () => { if (!ztools) throw new Error('ZTools host is unavailable'); return ztools } + const store = createKV(database, 'host') + bridge.register(names.show_select_dialog, async options => { + const paths = await host().showOpenDialog(options) + for (const file of paths || []) files.scope.grant(file, fs.statSync(file).isDirectory()) + return { canceled: !paths?.length, filePaths: paths || [] } + }) + bridge.register(names.show_save_dialog, async options => { + const filePath = await host().showSaveDialog(options) + if (filePath) files.scope.grant(filePath, false) + return { canceled: !filePath, filePath } + }) + bridge.register('host:openUrl', url => { + if (typeof url !== 'string' || !/^https?:\/\//.test(url)) throw new Error('只允许打开 HTTP(S) 链接') + return host().shellOpenExternal(url) + }) + bridge.register('host:clipboardWrite', text => { if (typeof text !== 'string') throw new TypeError('Clipboard text must be a string'); clipboard.writeText(text) }) + bridge.register(names.close, () => host().outPlugin(false)) + bridge.register(names.min, () => host().outPlugin(false)) + bridge.register(names.quit, () => host().outPlugin(true)) + bridge.register(names.set_app_name, title => { if (typeof title !== 'string') throw new TypeError('Invalid title'); bridge.emit('host:title', title) }) + bridge.register(names.set_window_size, ({ height }) => host().setExpendHeight(height)) + bridge.register(CMMON_EVENT_NAME.clear_env_params_deeplink, () => store.remove('deeplink')) + const keys = { + get_sound_effect_eq_preset: 'soundEffects:eq', save_sound_effect_eq_preset: 'soundEffects:eq', + get_sound_effect_convolution_preset: 'soundEffects:convolution', save_sound_effect_convolution_preset: 'soundEffects:convolution', + } + for (const [name, key] of Object.entries(keys)) bridge.register(names[name], name.startsWith('get_') ? () => store.get(key, []) : value => store.set(key, value)) + return { store } +} +module.exports = { bindHost } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/index.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/index.cjs new file mode 100644 index 000000000..c05728dd2 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/index.cjs @@ -0,0 +1,87 @@ +const { createDataService } = require('./data.cjs') +const { createSettings } = require('./settings.cjs') +const context = require('./context.cjs') +const { createAppEvent, createListEvent, createDislikeEvent } = require('../upstream/main/event/index.cjs') +const { CMMON_EVENT_NAME, WIN_MAIN_RENDERER_EVENT_NAME } = require('../upstream/common/ipcNames.cjs') +const modules = require('../upstream/main/worker/dbService/modules/index.cjs') +const registerList = require('../upstream/main/modules/commonRenderers/list/rendererEvent.cjs').default +const registerDislike = require('../upstream/main/modules/commonRenderers/dislike/rendererEvent.cjs').default +const registerMusic = require('../upstream/main/modules/winMain/rendererEvent/music.cjs').default +const registerDownload = require('../upstream/main/modules/winMain/rendererEvent/download.cjs').default +const listEvents = require('../upstream/main/modules/commonRenderers/list/winRendererEvent.cjs') +const dislikeEvents = require('../upstream/main/modules/commonRenderers/dislike/winRendererEvent.cjs') +const { createNetwork } = require('./network.cjs') +const dns = require('node:dns/promises') +const { createKV } = require('./kv.cjs') +const defaultHotKey = require('../upstream/common/defaultHotKey.cjs').default +const { createSources } = require('./source/manager.cjs') +const { createThemes } = require('./themes.cjs') +const { getFonts } = require('font-list') +const { bindPlayer } = require('./player.cjs') +const { createFileService } = require('./files.cjs') +const { callMainWorker } = require('./worker-main.cjs') +const path = require('node:path') +const { bindHost } = require('./host.cjs') + +/** Bind the original LX event/data handlers to plugin-local services. */ +function initializeRuntime({ bridge, directory, ztools, clipboard }) { + const data = createDataService(directory) + const settings = createSettings(data.getDatabase()) + const network = createNetwork() + const kv = createKV(data.getDatabase(), 'data') + const config = createKV(data.getDatabase(), 'host') + const themes = createThemes(data.getDatabase()) + const files = createFileService(data.getDatabase(), directory) + const sources = createSources({ database: data.getDatabase(), network, + onStatus: status => bridge.emit(WIN_MAIN_RENDERER_EVENT_NAME.user_api_status, status), + onAlert: alert => bridge.emit(WIN_MAIN_RENDERER_EVENT_NAME.user_api_show_update_alert, alert) }) + const dbService = {} + for (const [namespace, methods] of Object.entries(modules)) { + for (const method of Object.keys(methods)) dbService[method] = (...args) => Promise.resolve(data.call(namespace, method, args)) + } + const lx = context.lx = { database: data.getDatabase(), settings, tempDir: path.join(directory, 'cache/music'), worker: { dbService }, event_app: createAppEvent(), event_list: createListEvent(), event_dislike: createDislikeEvent(), appSetting: settings.get() } + context.bridge = bridge + bindPlayer({ bridge, lx, names: WIN_MAIN_RENDERER_EVENT_NAME }) + bindHost({ bridge, files, database: data.getDatabase(), directory, ztools, clipboard }) + bridge.register('runtime:ready', () => true) + bridge.register('network:fetch', options => network.request(options).result) + bridge.register('network:cancel', id => network.cancel(id)) + bridge.register('dns:lookup', ({ hostname, options }) => dns.lookup(hostname, options)) + bridge.register('files:call', ({ method, args }) => files.call(method, args)) + bridge.register('worker:main', ({ method, args }) => callMainWorker(files, method, args)) + bridge.register(CMMON_EVENT_NAME.get_env_params, () => ({ cmdParams: {}, deeplink: null })) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.get_data, key => kv.get(key)) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.save_data, ({ path, data }) => kv.set(path, data)) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.get_hot_key, () => config.get('hotkeys', defaultHotKey)) + bridge.register(CMMON_EVENT_NAME.get_system_fonts, () => getFonts()) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.get_themes, () => themes.getAll()) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.save_theme, theme => themes.save(theme)) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.remove_theme, id => themes.remove(id)) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.get_user_api_list, () => sources.list()) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.import_user_api, script => sources.importScript(script)) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.set_user_api, id => sources.activate(id)) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.get_user_api_status, () => sources.getStatus()) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.remove_user_api, async ids => { + const removingActive = ids.includes(sources.getStatus().apiInfo?.id) + await sources.remove(ids) + const remaining = sources.list() + if (removingActive) lx.event_app.update_config({ 'common.apiSource': remaining[0]?.id || 'temp' }) + return remaining + }) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api, input => sources.request(input)) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api_cancel, key => sources.cancel(key)) + bridge.register(WIN_MAIN_RENDERER_EVENT_NAME.user_api_set_allow_update_alert, ({ id, enable }) => sources.setAllowUpdate(id, enable)) + bridge.register(CMMON_EVENT_NAME.get_app_setting, () => settings.get()) + lx.event_app.on('updated_config', (_keys, patch) => bridge.emit(WIN_MAIN_RENDERER_EVENT_NAME.on_config_change, patch)) + bridge.register(CMMON_EVENT_NAME.set_app_setting, patch => lx.event_app.update_config(patch)) + bridge.setSnapshot(CMMON_EVENT_NAME.get_app_setting, lx.appSetting) + for (const register of [registerList, registerDislike, registerMusic, registerDownload]) register() + const unregisterList = listEvents.registerRendererEvents((channel, params) => bridge.emit(channel, params)) + const unregisterDislike = dislikeEvents.registerRendererEvents((channel, params) => bridge.emit(channel, params)) + return { data, settings, async dispose() { + unregisterList(); unregisterDislike() + lx.event_app.removeAllListeners(); lx.event_list.removeAllListeners(); lx.event_dislike.removeAllListeners() + await sources.dispose(); network.dispose(); data.close(); context.lx = null; context.bridge = null + } } +} +module.exports = { initializeRuntime } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/kv.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/kv.cjs new file mode 100644 index 000000000..6a138403c --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/kv.cjs @@ -0,0 +1,15 @@ +/** Store per-plugin JSON values inside the shared SQL transaction boundary. */ +function createKV(db, namespace) { + return { + get(key, fallback) { + const row = db.prepare('SELECT value FROM plugin_kv WHERE namespace=? AND key=?').get(namespace, key) + return row ? JSON.parse(row.value) : fallback + }, + set(key, value) { + if (value === undefined) { db.prepare('DELETE FROM plugin_kv WHERE namespace=? AND key=?').run(namespace, key); return } + db.prepare('INSERT INTO plugin_kv VALUES (?,?,?) ON CONFLICT(namespace,key) DO UPDATE SET value=excluded.value').run(namespace, key, JSON.stringify(value)) + }, + remove(key) { db.prepare('DELETE FROM plugin_kv WHERE namespace=? AND key=?').run(namespace, key) }, + } +} +module.exports = { createKV } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/lifecycle.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/lifecycle.cjs new file mode 100644 index 000000000..ae79e8007 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/lifecycle.cjs @@ -0,0 +1,21 @@ +/** Keep one service lifetime across entering, hiding and detaching. */ +function createLifecycle(hooks) { + let ready, stopping, stopped = false + return { + async enter(action) { + if (stopped) throw Object.assign(new Error('插件已终止'), { code: 'DISPOSED' }) + ready ||= Promise.resolve().then(() => hooks.initialize()) + await ready + if (!stopped) await hooks.onEnter?.(action) + }, + async out(isKill) { if (isKill) await this.dispose(); else await hooks.onHide?.() }, + async detach() { if (!stopped) await hooks.onDetach?.() }, + async dispose() { + if (stopping) return stopping + stopped = true + stopping = Promise.resolve(ready).catch(() => {}).then(() => hooks.dispose()) + return stopping + }, + } +} +module.exports = { createLifecycle } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/log.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/log.cjs new file mode 100644 index 000000000..c6577ab31 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/log.cjs @@ -0,0 +1 @@ +module.exports = console diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/main-ipc.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/main-ipc.cjs new file mode 100644 index 000000000..6aeca2661 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/main-ipc.cjs @@ -0,0 +1,5 @@ +const context = require('./context.cjs') +function mainHandle(name, listener) { + context.bridge.register(name, params => listener({ event: null, params })) +} +module.exports = { mainHandle, mainOn: mainHandle, mainOnce: mainHandle } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/network.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/network.cjs new file mode 100644 index 000000000..338cc6b4a --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/network.cjs @@ -0,0 +1,32 @@ +const { randomUUID } = require('node:crypto') +const original = require('../upstream/renderer/utils/request.cjs') + +/** Maintain scoped, cancellable LX HTTP requests with serializable responses. */ +function createNetwork() { + const pending = new Map() + return { + request({ url, id = randomUUID(), ...options }) { + let parsed + try { parsed = new URL(url); if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('Only HTTP(S) URLs are supported') } + catch (error) { return { id, result: Promise.reject(error), cancel() {} } } + if (pending.has(id) || pending.size >= 64) return { id, result: Promise.reject(new Error('HTTP request limit reached')), cancel() {} } + let rejectRequest, request, cancelled = false + const result = new Promise((resolve, reject) => { + rejectRequest = reject + request = original.httpFetch(parsed.href, { ...options, method: options.method || 'get', timeout: Math.min(options.timeout || 60000, 60000) }) + request.promise.then(response => { + if (cancelled) return + resolve({ statusCode: response.statusCode, statusMessage: response.statusMessage, + headers: response.headers, raw: Buffer.from(response.raw || []), bytes: response.bytes, + body: response.body }) + }, reject) + }).finally(() => pending.delete(id)) + const cancel = () => { if (cancelled) return; cancelled = true; request?.cancelHttp(); rejectRequest(Object.assign(new Error('取消http请求'), { code: 'CANCELLED' })) } + pending.set(id, cancel) + return { id, result, cancel } + }, + cancel(id) { pending.get(id)?.() }, + dispose() { for (const cancel of pending.values()) cancel(); pending.clear() }, + } +} +module.exports = { createNetwork } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/player.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/player.cjs new file mode 100644 index 000000000..51ac52576 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/player.cjs @@ -0,0 +1,12 @@ +const DEFAULT_STATE = { status: 'stoped', name: '', singer: '', albumName: '', picUrl: '', progress: 0, duration: 0, playbackRate: 1, lyricLineText: '', lyricLineAllText: '', lyric: '', tlyric: '', rlyric: '', lxlyric: '', collect: false, volume: 1, mute: false } +function bindPlayer({ bridge, lx, names }) { + lx.player_status = { ...DEFAULT_STATE } + bridge.setSnapshot('player:state', lx.player_status) + bridge.register(names.player_status, patch => { + const safe = Object.fromEntries(Object.entries(patch).filter(([key, value]) => Object.hasOwn(DEFAULT_STATE, key) && typeof value === typeof DEFAULT_STATE[key])) + lx.event_app.player_status(safe) + bridge.setSnapshot('player:state', lx.player_status) + }) + bridge.register(names.inited, () => { bridge.setSnapshot('player:ready', true); lx.event_app.main_window_inited() }) +} +module.exports = { bindPlayer } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/proxy.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/proxy.cjs new file mode 100644 index 000000000..4df8c0061 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/proxy.cjs @@ -0,0 +1,3 @@ +const context = require('./context.cjs') +const setting = key => context.lx?.appSetting?.[`network.proxy.${key}`] +exports.proxy = { get enable() { return setting('enable') }, get host() { return setting('host') }, get port() { return setting('port') } } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/settings.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/settings.cjs new file mode 100644 index 000000000..e56a7ca7e --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/settings.cjs @@ -0,0 +1,22 @@ +const defaults = require('../upstream/common/defaultSetting.cjs').default + +/** Persist validated LX settings in the same database used for lists and backups. */ +function createSettings(db) { + const read = () => { + const row = db.prepare("SELECT value FROM plugin_kv WHERE namespace='setting' AND key='app'").get() + return { ...defaults, ...(row ? JSON.parse(row.value) : {}) } + } + return { + get() { return structuredClone(read()) }, + update(patch) { + for (const [key, value] of Object.entries(patch)) { + if (!Object.hasOwn(defaults, key)) throw new Error(`Unknown setting: ${key}`) + if (value !== null && defaults[key] !== null && typeof value !== typeof defaults[key]) throw new TypeError(`Invalid setting type: ${key}`) + } + const next = { ...read(), ...patch } + db.prepare("INSERT INTO plugin_kv VALUES ('setting','app',?) ON CONFLICT(namespace,key) DO UPDATE SET value=excluded.value").run(JSON.stringify(next)) + return structuredClone(next) + }, + } +} +module.exports = { createSettings } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/source/manager.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/source/manager.cjs new file mode 100644 index 000000000..f47ac46b9 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/source/manager.cjs @@ -0,0 +1,73 @@ +const { createHash, randomUUID } = require('node:crypto') +const { createKV } = require('../kv.cjs') +const { SourceSession } = require('./session.cjs') + +const INFO_LIMITS = { name: 24, description: 36, author: 56, homepage: 1024, version: 36 } +function parseInfo(script) { + if (typeof script !== 'string' || script.length > 9_000_000) throw new Error('音源脚本超过 9_000_000 字符限制') + const header = /^\/\*[\s\S]+?\*\//.exec(script) + if (!header) throw new Error('无效的自定义源文件') + const info = { name: '', description: '', author: '', homepage: '', version: '' } + for (const line of header[0].split(/\r?\n/)) { + const match = /^\s?\*\s?@(\w+)\s(.+)$/.exec(line) + if (!match || !Object.hasOwn(INFO_LIMITS, match[1])) continue + info[match[1]] = match[2].trim().slice(0, INFO_LIMITS[match[1]]) + } + info.name ||= 'LX 自定义音源' + return info +} +/** Keep source data private and reject stale sessions when switching or removing scripts. */ +function createSources({ database, network, onStatus, onAlert }) { + const store = createKV(database, 'sources') + let entries = store.get('entries', []), selected, session, activation, generation = 0 + let status = { status: false, message: '' } + const list = () => entries.map(({ script, hash, ...info }) => ({ ...info })) + const commit = next => { store.set('entries', next); entries = next } + const publish = (next, notify = true) => { status = next; if (notify) onStatus?.(structuredClone(next)) } + return { + list, + importScript(script) { + const info = parseInfo(script) + const hash = createHash('sha256').update(script).digest('hex') + if (entries.some(entry => entry.hash === hash)) throw new Error('导入失败,脚本内容与已有音源相同') + const apiInfo = { id: `user_api_${randomUUID()}`, ...info, allowShowUpdateAlert: true } + commit([...entries, { ...apiInfo, script, hash }]) + return { apiInfo, apiList: list() } + }, + activate(id) { + if (selected === id && activation) return activation + selected = id + const current = ++generation + activation = (async () => { + await session?.close() + if (current !== generation) throw new Error('source changed') + const entry = entries.find(item => item.id === id) + if (!entry) { session = null; publish({ status: false, message: '' }); return } + const { script, hash, ...apiInfo } = entry + // LX resolves its initialization promise on false+apiInfo; emit only terminal results. + publish({ status: false, message: 'initing', apiInfo }, false) + const next = session = new SourceSession({ info: { ...apiInfo, script }, network, + onAlert: alert => { if (current === generation && entries.find(item => item.id === id)?.allowShowUpdateAlert) onAlert?.({ name: entry.name, ...alert }) } }) + try { + const sources = await next.start() + if (current !== generation) throw new Error('source changed') + publish({ status: true, message: '', apiInfo: { ...apiInfo, sources } }) + } catch (error) { + if (current === generation) { activation = null; publish({ status: false, message: error.message, apiInfo }) } + throw error + } + })() + return activation + }, + getStatus: () => structuredClone(status), + request: input => session ? session.request(input) : Promise.reject(new Error('没有可用的音源')), + cancel: key => session?.cancel(key), + async remove(ids) { + commit(entries.filter(entry => !ids.includes(entry.id))) + if (ids.includes(selected)) { ++generation; await session?.close(); session = null; activation = null; selected = null; publish({ status: false, message: '' }) } + }, + setAllowUpdate(id, enable) { if (entries.some(item => item.id === id)) commit(entries.map(entry => entry.id === id ? { ...entry, allowShowUpdateAlert: Boolean(enable) } : entry)) }, + async dispose() { ++generation; await session?.close(); session = null }, + } +} +module.exports = { createSources, parseInfo } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/source/protocol.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/source/protocol.cjs new file mode 100644 index 000000000..76b54690f --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/source/protocol.cjs @@ -0,0 +1,36 @@ +const SOURCE_IDS = ['kw', 'kg', 'tx', 'wy', 'mg', 'local'] +const QUALITYS = ['128k', '320k', 'flac', 'flac24bit'] +function normalizeSources(input) { + if (!input || typeof input !== 'object') throw new Error('Missing required parameter init info') + const result = {} + for (const source of SOURCE_IDS) { + const info = input[source] + if (!info || info.type !== 'music') continue + if (!Array.isArray(info.actions) || !Array.isArray(info.qualitys)) throw new Error('音源能力格式错误') + result[source] = { type: 'music', actions: (source === 'local' ? ['musicUrl', 'lyric', 'pic'] : ['musicUrl']).filter(action => info.actions.includes(action)), + qualitys: (source === 'local' ? [] : QUALITYS).filter(quality => info.qualitys.includes(quality)) } + } + return result +} +function validateRequest(sources, data) { + const source = sources?.[data?.source] + if (!source?.actions.includes(data?.action)) throw new Error('音源不支持此操作') + if (data.action === 'musicUrl' && data.source !== 'local' && !source.qualitys.includes(data.info?.type)) throw new Error('不支持的音质 quality') +} +function validateURL(value) { + if (typeof value !== 'string' || value.length > 2048 || !/^https?:/.test(value)) throw new Error('音源返回无效 URL') + if (!['http:', 'https:'].includes(new URL(value).protocol)) throw new Error('音源返回无效 URL') + return value +} +function normalizeResult(input, response) { + let data + if (input.action === 'musicUrl') data = { type: input.info.type, url: validateURL(response) } + else if (input.action === 'pic') data = validateURL(response) + else if (input.action === 'lyric') { + if (!response || typeof response.lyric !== 'string' || response.lyric.length > 51200) throw new Error('音源歌词格式错误') + data = { lyric: response.lyric } + for (const [key, limit] of [['tlyric', 5120], ['rlyric', 5120], ['lxlyric', 8192]]) data[key] = typeof response[key] === 'string' && response[key].length < limit ? response[key] : null + } else throw new Error('音源返回未知操作') + return { source: input.source, action: input.action, data } +} +module.exports = { normalizeSources, validateRequest, normalizeResult, validateURL } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/source/session.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/source/session.cjs new file mode 100644 index 000000000..d240f7046 --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/source/session.cjs @@ -0,0 +1,95 @@ +const { startWorkerServer } = require('../../worker-server.cjs') +const { normalizeSources, validateRequest, normalizeResult } = require('./protocol.cjs') + +/** Own one isolated source, its HTTP requests and every pending playback result. */ +class SourceSession { + constructor({ info, network, onAlert }) { + this.info = info; this.network = network; this.onAlert = onAlert + this.pending = new Map(); this.http = new Set(); this.closed = false + } + async start() { + this.server = await startWorkerServer() + if (this.closed) { await this.server.close(); throw new Error('source changed') } + this.origin = new URL(this.server.url).origin + const frame = this.frame = document.createElement('iframe') + frame.style.display = 'none' + frame.setAttribute('sandbox', 'allow-scripts allow-same-origin') + const initialized = new Promise((resolve, reject) => { + this.resolveInit = resolve; this.rejectInit = reject + this.initTimer = setTimeout(() => reject(new Error('音源初始化超时')), 8000) + }) + this.listener = event => { + if (this.closed || event.source !== frame.contentWindow || event.origin !== this.origin || event.data?.channel !== this.server.token) return + this.handle(event.data) + } + window.addEventListener('message', this.listener) + frame.src = this.server.url + document.body.append(frame) + try { return await initialized } + catch (error) { await this.close(error.message); throw error } + finally { clearTimeout(this.initTimer) } + } + send(data) { + if (!this.closed) this.frame.contentWindow.postMessage({ ...data, channel: this.server.token }, this.origin) + } + handle(message) { + try { + if (message.type === 'ready') this.send({ type: 'init', info: this.info }) + else if (message.type === 'inited') { this.sources = normalizeSources(message.sources); this.resolveInit(this.sources) } + else if (message.type === 'init-error') this.rejectInit(new Error(String(message.error).slice(0, 1024))) + else if (message.type === 'http') this.requestHTTP(message) + else if (message.type === 'http-cancel') this.network.cancel(`${this.server.token}:${message.id}`) + else if (message.type === 'response') { + const item = this.pending.get(message.requestKey) + if (!item) return + this.pending.delete(message.requestKey); clearTimeout(item.timer) + if (message.error) item.reject(new Error(String(message.error).slice(0, 1024))) + else { + // Revalidate outside untrusted code, using the original requested action. + const raw = item.input.data.action === 'musicUrl' ? message.result?.data?.url : message.result?.data + try { item.resolve(normalizeResult(item.input.data, raw)) } catch (error) { item.reject(error) } + } + } else if (message.type === 'updateAlert' && typeof message.data?.log === 'string') this.onAlert?.({ log: message.data.log.slice(0, 1024), updateUrl: /^https?:\/\//.test(message.data.updateUrl) ? message.data.updateUrl.slice(0, 1024) : undefined }) + } catch (error) { this.rejectInit?.(error) } + } + requestHTTP(message) { + if (typeof message.id !== 'string' || message.id.length > 100 || this.http.size >= 32) return + const id = `${this.server.token}:${message.id}` + if (this.http.has(id)) return + const options = message.options && typeof message.options === 'object' ? message.options : {} + const safe = Object.fromEntries(['method', 'headers', 'body', 'form', 'formData', 'timeout'].filter(key => Object.hasOwn(options, key)).map(key => [key, options[key]])) + const request = this.network.request({ ...safe, url: message.url, id }) + this.http.add(id) + request.result.then(response => this.send({ type: 'http-response', id: message.id, response }), error => this.send({ type: 'http-response', id: message.id, error: error.message })).finally(() => this.http.delete(id)) + } + request(input) { + if (this.closed || !this.sources) return Promise.reject(new Error('音源尚未就绪')) + try { validateRequest(this.sources, input?.data) } catch (error) { return Promise.reject(error) } + if (typeof input.requestKey !== 'string' || !input.requestKey || input.requestKey.length > 512 || this.pending.has(input.requestKey)) return Promise.reject(new Error('无效或重复的音源请求 ID')) + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { this.pending.delete(input.requestKey); reject(new Error('音源请求超时')) }, 30000) + this.pending.set(input.requestKey, { input, timer, resolve, reject }) + this.send({ type: 'request', input }) + }) + } + cancel(requestKey) { + const item = this.pending.get(requestKey) + if (!item) return + this.pending.delete(requestKey); clearTimeout(item.timer); item.reject(new Error('取消音源请求')) + } + async close(reason = 'source changed / 音源已切换') { + if (this.closed) return + this.closed = true + clearTimeout(this.initTimer); this.rejectInit?.(new Error(reason)) + for (const key of [...this.pending.keys()]) this.cancel(key) + for (const id of this.http) this.network.cancel(id) + this.http.clear() + if (this.listener) window.removeEventListener('message', this.listener) + if (this.frame) { + this.frame.contentWindow.postMessage({ channel: this.server.token, type: 'dispose' }, this.origin) + this.frame.remove() + } + await this.server?.close() + } +} +module.exports = { SourceSession } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/themes.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/themes.cjs new file mode 100644 index 000000000..4661eccee --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/themes.cjs @@ -0,0 +1,19 @@ +const builtin = require('../upstream/common/theme/index.json') +const { createKV } = require('./kv.cjs') +function createThemes(database) { + const store = createKV(database, 'themes') + return { + getAll() { return { themes: structuredClone(builtin), userThemes: store.get('user', []) } }, + save(theme) { + if (!theme || typeof theme.id !== 'string' || !theme.id || typeof theme.name !== 'string' || !theme.config || typeof theme.config !== 'object') throw new TypeError('无效的主题') + if (builtin.some(item => item.id === theme.id)) throw new Error('不能覆盖内置主题') + if (Buffer.byteLength(JSON.stringify(theme)) >= 256 * 1024) throw new Error('主题数据过大') + const user = store.get('user', []) + const index = user.findIndex(item => item.id === theme.id) + if (index < 0) user.push(theme); else user[index] = theme + store.set('user', user) + }, + remove(id) { store.set('user', store.get('user', []).filter(theme => theme.id !== id)) }, + } +} +module.exports = { createThemes } diff --git a/plugins/lx-music-ztools/src-ztools/preload/runtime/worker-main.cjs b/plugins/lx-music-ztools/src-ztools/preload/runtime/worker-main.cjs new file mode 100644 index 000000000..fa6c45d8c --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/runtime/worker-main.cjs @@ -0,0 +1,15 @@ +const common = require('../upstream/renderer/worker/main/common.cjs') +const list = require('../upstream/renderer/worker/main/list.cjs') +const music = require('../upstream/renderer/worker/main/music.cjs') +const methods = { ...common, ...list, ...music } + +/** Run original main-Worker business methods with native file operations scoped. */ +function callMainWorker(files, method, args) { + if (!Object.hasOwn(methods, method) || typeof methods[method] !== 'function') throw new Error(`Unknown main worker method: ${method}`) + const values = [...args] + if (['getMusicFilePic', 'getMusicFileLyric', 'exportPlayListToText', 'exportPlayListToCSV', 'saveLxConfigFile', 'readLxConfigFile', 'saveStrToFile'].includes(method)) values[0] = files.scope.authorize(values[0]) + if (method === 'createLocalMusicInfos') values[0] = values[0].map(file => files.scope.authorize(file)) + if (['saveLxConfigFile', 'readLxConfigFile', 'saveStrToFile'].includes(method)) return files.call(method, values) + return methods[method](...values) +} +module.exports = { callMainWorker } diff --git a/plugins/lx-music-ztools/src-ztools/preload/services.js b/plugins/lx-music-ztools/src-ztools/preload/services.js new file mode 100644 index 000000000..a6bc350cf --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/services.js @@ -0,0 +1,56 @@ +const PREFIX = 'lx-music:framework:' +const probes = require('./probes.cjs') +const { randomUUID } = require('node:crypto') +const { createBridge } = require('./runtime/bridge.cjs') +const { createLifecycle } = require('./runtime/lifecycle.cjs') +const { initializeRuntime } = require('./runtime/index.cjs') +const os = require('node:os') +const { clipboard } = require('electron') +const bridge = createBridge() +bridge.setSnapshot('host:env', { platform: process.platform, release: os.release(), homedir: os.homedir(), tmpdir: os.tmpdir(), arch: os.arch(), cpus: os.cpus().map(({ model }) => ({ model })) }) +const runtime = initializeRuntime({ bridge, directory: window.ztools.getPath('pluginData'), ztools: window.ztools, clipboard }) +window.lxHost = Object.fromEntries(['invoke', 'send', 'readSnapshot', 'on', 'once', 'off', 'offAll', 'subscribe'].map(key => [key, bridge[key]])) +let lifecycleProbe +window.services = { + clipboardReadText() { return clipboard.readText() }, + subscribe(channel, listener) { return bridge.subscribe(channel, listener) }, + startLifecycleProbe() { + if (!lifecycleProbe) { + lifecycleProbe = { id: randomUUID(), ticks: 0 } + setInterval(() => lifecycleProbe.ticks++, 100) + } + return { ...lifecycleProbe } + }, + getLifecycleProbe() { return { ...lifecycleProbe } }, + probeCapabilities() { return probes.probeCapabilities(window.ztools) }, + probeSourceIsolation() { return probes.probeSourceIsolation() }, + probeWorkerIsolation(publicKey) { return probes.probeWorkerIsolation(publicKey) }, + /** 返回当前宿主与 Node 运行环境。 */ + diagnose() { return { app: window.ztools.getAppName(), version: window.ztools.getAppVersion(), node: process.versions.node } }, + /** 读取插件进入计数。 */ + getLaunchCount() { return Number(window.ztools.dbStorage.getItem(PREFIX + 'launches')) || 0 }, + /** 记录进入事件并返回累计次数。 */ + recordLaunch() { + const next = this.getLaunchCount() + 1 + window.ztools.dbStorage.setItem(PREFIX + 'launches', next) + return next + }, + /** 验证插件存储写入与回读,随后清理临时键。 */ + checkStorage() { + const key = PREFIX + 'probe' + const value = require('node:crypto').randomUUID() + try { window.ztools.dbStorage.setItem(key, value); return window.ztools.dbStorage.getItem(key) === value } + finally { window.ztools.dbStorage.removeItem(key) } + } +} +const lifecycle = createLifecycle({ + initialize: async () => {}, + onEnter: action => { window.services.recordLaunch(); bridge.emit('lifecycle:enter', action) }, + onHide: () => bridge.emit('lifecycle:hide'), + onDetach: () => bridge.emit('lifecycle:detach'), + dispose: async () => { await runtime.dispose(); await bridge.dispose() }, +}) +const report = error => bridge.emit('bridge:error', { code: error.code, message: error.message }) +window.ztools.onPluginEnter(action => { void lifecycle.enter(action).catch(report) }) +window.ztools.onPluginOut(isKill => { void lifecycle.out(isKill).catch(report) }) +window.ztools.onPluginDetach(() => { void lifecycle.detach().catch(report) }) diff --git a/plugins/lx-music-ztools/src-ztools/preload/worker-server.cjs b/plugins/lx-music-ztools/src-ztools/preload/worker-server.cjs new file mode 100644 index 000000000..42ed4d82b --- /dev/null +++ b/plugins/lx-music-ztools/src-ztools/preload/worker-server.cjs @@ -0,0 +1,32 @@ +const http = require('node:http') +const fs = require('node:fs') +const path = require('node:path') +const { randomUUID } = require('node:crypto') + +/** Serve only the trusted browser Worker with an enforced network-denying CSP. */ +async function startWorkerServer(scriptName = 'source-worker.js') { + const token = randomUUID() + if (!['source-worker.js', 'probe-worker.js'].includes(scriptName)) throw new Error('Unknown Worker entry') + const script = fs.readFileSync(path.join(__dirname, '../dist', scriptName)) + let requests = 0 + const server = http.createServer((req, res) => { + requests++ + if (req.method !== 'GET') { res.writeHead(404); res.end(); return } + const base = `/${token}` + const isWorker = req.url === `${base}/worker.js` + const isFrame = req.url === `${base}/index.html` + const isBootstrap = req.url === `${base}/frame.js` + if (!isWorker && !isFrame && !isBootstrap) { res.writeHead(404); res.end(); return } + res.writeHead(200, { 'Content-Type': isFrame ? 'text/html; charset=utf-8' : 'application/javascript', + 'Content-Security-Policy': isWorker ? "default-src 'none'; script-src 'unsafe-eval'; connect-src 'none'; worker-src 'none'; object-src 'none'" : "default-src 'none'; script-src 'self'; worker-src 'self'; frame-src 'none'", + 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', 'Access-Control-Allow-Origin': '*', + }) + if (isWorker) res.end(script) + else if (isFrame) res.end(``) + else res.end(`const worker=new Worker('worker.js');worker.onmessage=e=>parent.postMessage({...e.data,channel:${JSON.stringify(token)}},'*');worker.onerror=e=>parent.postMessage({channel:${JSON.stringify(token)},type:'error',error:e.message},'*');addEventListener('message',e=>{if(e.source!==parent||e.data.channel!==${JSON.stringify(token)})return;if(e.data.type==='dispose'){worker.terminate();parent.postMessage({channel:${JSON.stringify(token)},type:'stopped'},'*')}else worker.postMessage(e.data)});parent.postMessage({channel:${JSON.stringify(token)},type:'ready'},'*');`) + }) + await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve) }) + return { token, url: `http://127.0.0.1:${server.address().port}/${token}/index.html`, + getRequests: () => requests, close: () => new Promise(resolve => { server.close(resolve); server.closeAllConnections() }) } +} +module.exports = { startWorkerServer } diff --git a/plugins/lx-music-ztools/src/App.vue b/plugins/lx-music-ztools/src/App.vue new file mode 100644 index 000000000..e7b7b55a7 --- /dev/null +++ b/plugins/lx-music-ztools/src/App.vue @@ -0,0 +1,43 @@ + + diff --git a/plugins/lx-music-ztools/src/env.d.ts b/plugins/lx-music-ztools/src/env.d.ts new file mode 100644 index 000000000..078dbf439 --- /dev/null +++ b/plugins/lx-music-ztools/src/env.d.ts @@ -0,0 +1,13 @@ +/// +/// +declare global { + interface Window { services: { + clipboardReadText(): string + subscribe(channel: string, listener: (params: any) => void): () => void + diagnose(): { app: string; version: string; node: string } + getLaunchCount(): number + recordLaunch(): number + checkStorage(): boolean + } } +} +export {} diff --git a/plugins/lx-music-ztools/src/main.css b/plugins/lx-music-ztools/src/main.css new file mode 100644 index 000000000..79c9d31ec --- /dev/null +++ b/plugins/lx-music-ztools/src/main.css @@ -0,0 +1,13 @@ + :root { font: 14px/1.6 "Microsoft YaHei", system-ui, sans-serif; color: #24374a; color-scheme: light dark; --line: #8091a433; --muted: #728192; --accent: #157a74; } +* { box-sizing: border-box; } html, body, #app { margin: 0; background: transparent; min-width: 320px; } +main { max-width: 820px; margin: auto; padding: 32px; } header { display: flex; align-items: center; gap: 16px; } +header img { width: 54px; height: 54px; border-radius: 12px; } h1 { font-size: 26px; margin: 0; font-weight: 600; } +.eyebrow { font-size: 10px; letter-spacing: .12em; color: var(--muted); margin: 0; } +.tag { margin-left: auto; font-size: 12px; border: 1px solid var(--line); padding: 3px 10px; border-radius: 20px; } +.intro { color: var(--muted); margin: 22px 0; } dl { border-top: 1px solid var(--line); margin: 0 0 24px; } +dl div { display: flex; padding: 12px 0; border-bottom: 1px solid var(--line); gap: 18px; } +dt { width: 90px; flex-shrink: 0; color: var(--muted); } dd { margin: 0; overflow-wrap: anywhere; } +.actions { display: flex; gap: 12px; } button { font: inherit; cursor: pointer; border: 1px solid var(--accent); background: transparent; color: var(--accent); border-radius: 6px; padding: 8px 14px; } +button:disabled { opacity: .4; cursor: default; } button:hover:enabled { background: #157a7410; } +[role=status] { min-height: 24px; color: var(--accent); } footer { color: var(--muted); font-size: 12px; } +@media (prefers-color-scheme: dark) { :root { color: #d9e4ee; --muted: #9aabba; --accent: #63c6b6; } } diff --git a/plugins/lx-music-ztools/src/main.ts b/plugins/lx-music-ztools/src/main.ts new file mode 100644 index 000000000..8a3c77bc5 --- /dev/null +++ b/plugins/lx-music-ztools/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './main.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/plugins/lx-music-ztools/src/probe-worker.js b/plugins/lx-music-ztools/src/probe-worker.js new file mode 100644 index 000000000..0a2651a44 --- /dev/null +++ b/plugins/lx-music-ztools/src/probe-worker.js @@ -0,0 +1,18 @@ +import { tools } from './source-tools' +import { Buffer } from 'buffer' +const reply = globalThis.postMessage.bind(globalThis) +globalThis.onmessage = async ({ data }) => { + if (data.type === 'busy') { while (true) {} } + if (data.type !== 'probe') return + try { + reply({ type: 'probe-result', result: { + node: typeof globalThis.require, ztools: typeof globalThis.ztools, + syncMd5: tools.crypto.md5('abc'), origin: globalThis.origin, + hostFileReadable: await fetch('file:///C:/Windows/win.ini').then(() => true, () => false), + networkReadable: await fetch(data.url).then(() => true, () => false), + aes: tools.buffer.bufToString(tools.crypto.aesEncrypt(Buffer.from('test'), 'aes-128-cbc', Buffer.alloc(16), Buffer.alloc(16)), 'hex'), + compressed: tools.buffer.bufToString(await tools.zlib.inflate(await tools.zlib.deflate(Buffer.from('中文'))), 'utf8'), + rsa: tools.buffer.bufToString(tools.crypto.rsaEncrypt(Buffer.from('abc'), data.publicKey), 'hex'), + } }) + } catch (error) { reply({ type: 'error', error: error.message }) } +} diff --git a/plugins/lx-music-ztools/src/source-tools.js b/plugins/lx-music-ztools/src/source-tools.js new file mode 100644 index 000000000..317493869 --- /dev/null +++ b/plugins/lx-music-ztools/src/source-tools.js @@ -0,0 +1,20 @@ +import crypto from 'crypto-browserify' +import { Buffer } from 'buffer' +import { inflate, deflate } from 'pako' + +export const tools = { + crypto: { + md5: value => crypto.createHash('md5').update(value).digest('hex'), + aesEncrypt(buffer, mode, key, iv) { + const cipher = crypto.createCipheriv(mode, key, iv) + return Buffer.concat([cipher.update(buffer), cipher.final()]) + }, + rsaEncrypt(buffer, key) { + buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]) + return crypto.publicEncrypt({ key, padding: 3 }, buffer) + }, + randomBytes: size => crypto.randomBytes(size), + }, + buffer: { from: (...args) => Buffer.from(...args), bufToString: (buf, format) => Buffer.from(buf, 'binary').toString(format) }, + zlib: { inflate: async buf => Buffer.from(inflate(buf)), deflate: async buf => Buffer.from(deflate(buf)) }, +} diff --git a/plugins/lx-music-ztools/src/source-worker.js b/plugins/lx-music-ztools/src/source-worker.js new file mode 100644 index 000000000..d9dd9d5d6 --- /dev/null +++ b/plugins/lx-music-ztools/src/source-worker.js @@ -0,0 +1,64 @@ +import { tools } from './source-tools' +import { Buffer } from 'buffer' +import protocol from '../src-ztools/preload/runtime/source/protocol.cjs' + +// Only this Worker evaluates user code. CSP denies raw network, files and nested workers. +const reply = globalThis.postMessage.bind(globalThis) +const callbacks = new Map() +let initialized = false, started = false, alertSent = false, requestHandler, nextHttpId = 0 +const EVENT_NAMES = Object.freeze({ request: 'request', inited: 'inited', updateAlert: 'updateAlert' }) +function createAPI(info) { + const lx = { + EVENT_NAMES, version: '2.0.0', env: 'desktop', utils: tools, + currentScriptInfo: { name: info.name, description: info.description, version: info.version, author: info.author, homepage: info.homepage, rawScript: info.script }, + on(event, handler) { + if (event !== 'request' || typeof handler !== 'function') return Promise.reject(new Error('The event is not supported: ' + event)) + requestHandler = handler + return Promise.resolve() + }, + async send(event, data) { + if (event === 'inited') { + if (initialized) throw new Error('Script is inited') + const sources = protocol.normalizeSources(data?.sources) + initialized = true + reply({ type: 'inited', sources }) + } else if (event === 'updateAlert') { + if (alertSent) throw new Error('The update alert can only be called once.') + if (!data || typeof data.log !== 'string' || !data.log) throw new Error('log is required.') + alertSent = true + reply({ type: 'updateAlert', data: { log: data.log.slice(0, 1024), updateUrl: /^https?:\/\//.test(data.updateUrl) ? data.updateUrl.slice(0, 1024) : undefined } }) + } else throw new Error('The event is not supported: ' + event) + }, + request(url, options = {}, callback) { + if (typeof callback !== 'function') throw new TypeError('HTTP callback is required') + const id = String(++nextHttpId) + callbacks.set(id, callback) + reply({ type: 'http', id, url, options }) + return () => { if (!callbacks.has(id)) return; callbacks.delete(id); reply({ type: 'http-cancel', id }) } + }, + } + return Object.freeze(lx) +} +globalThis.onmessage = async ({ data }) => { + try { + if (data.type === 'init') { + if (started) return + started = true + Object.defineProperty(globalThis, 'lx', { value: createAPI(data.info), writable: false, configurable: false }) + ;(0, eval)(data.info.script) + } else if (data.type === 'http-response') { + const callback = callbacks.get(data.id) + if (!callback) return + callbacks.delete(data.id) + if (data.error) callback.call(lx, new Error(data.error), null, null) + else { data.response.raw = Buffer.from(data.response.raw); callback.call(lx, null, data.response, data.response.body) } + } else if (data.type === 'request') { + if (!initialized || !requestHandler) throw new Error('Request event is not defined') + try { + const response = await requestHandler.call(lx, data.input.data) + reply({ type: 'response', requestKey: data.input.requestKey, result: protocol.normalizeResult(data.input.data, response) }) + } catch (error) { reply({ type: 'response', requestKey: data.input.requestKey, error: String(error.message || error) }) } + } + } catch (error) { reply({ type: initialized ? 'error' : 'init-error', error: String(error.message || error) }) } +} +globalThis.addEventListener('unhandledrejection', event => { if (!initialized) reply({ type: 'init-error', error: String(event.reason?.message || event.reason) }) }) diff --git a/plugins/lx-music-ztools/src/worker-shims.js b/plugins/lx-music-ztools/src/worker-shims.js new file mode 100644 index 000000000..6313a5f42 --- /dev/null +++ b/plugins/lx-music-ztools/src/worker-shims.js @@ -0,0 +1,2 @@ +export { default as process } from 'process/browser' +export { Buffer } from 'buffer' diff --git a/plugins/lx-music-ztools/src/ztools/contracts.ts b/plugins/lx-music-ztools/src/ztools/contracts.ts new file mode 100644 index 000000000..a30c58bff --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/contracts.ts @@ -0,0 +1,12 @@ +export interface Bridge { + invoke(channel: string, params?: unknown): Promise + send(channel: string, params?: unknown): void + readSnapshot(key: string): T + on(channel: string, listener: (params: any) => void): void + once(channel: string, listener: (params: any) => void): void + off(channel: string, listener: (params: any) => void): void + offAll(channel: string): void + subscribe(channel: string, listener: (params: any) => void): () => void + dispose(): Promise +} +declare global { interface Window { lxHost: Bridge } } diff --git a/plugins/lx-music-ztools/src/ztools/dns.ts b/plugins/lx-music-ztools/src/ztools/dns.ts new file mode 100644 index 000000000..1cf058ef7 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/dns.ts @@ -0,0 +1,6 @@ +import './contracts' +export function lookup(hostname, options, callback?) { + if (typeof options === 'function') { callback = options; options = {} } + window.lxHost.invoke('dns:lookup', { hostname, options }).then(result => callback(null, result.address, result.family), error => callback(error)) +} +export default { lookup } diff --git a/plugins/lx-music-ztools/src/ztools/electron.ts b/plugins/lx-music-ztools/src/ztools/electron.ts new file mode 100644 index 000000000..6eeb6db62 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/electron.ts @@ -0,0 +1,6 @@ +import './contracts' +export const openDirInExplorer = (path: string) => window.lxHost.send('host:openDirectory', path) +export const openUrl = (url: string) => window.lxHost.invoke('host:openUrl', url) +export const clipboardWriteText = (text: string) => window.lxHost.send('host:clipboardWrite', text) +export const clipboardReadText = (): string => window.services.clipboardReadText() +export const encodePath = (path: string) => path.replaceAll('%', '%25').replaceAll('#', '%23') diff --git a/plugins/lx-music-ztools/src/ztools/entry.ts b/plugins/lx-music-ztools/src/ztools/entry.ts new file mode 100644 index 000000000..5c162c135 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/entry.ts @@ -0,0 +1,7 @@ +import './contracts' +import './shell.css' +document.documentElement.classList.add('lx-ztools-shell') +void window.lxHost.invoke('runtime:ready').then(async () => { + await import('../../vendor/lx/src/renderer/main') + await import('./player.js') +}) diff --git a/plugins/lx-music-ztools/src/ztools/errors.ts b/plugins/lx-music-ztools/src/ztools/errors.ts new file mode 100644 index 000000000..af3155f71 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/errors.ts @@ -0,0 +1,2 @@ +window.addEventListener('error', event => console.error('LX plugin error', event.error || event.message)) +window.addEventListener('unhandledrejection', event => console.error('LX plugin rejected request', event.reason)) diff --git a/plugins/lx-music-ztools/src/ztools/logger.ts b/plugins/lx-music-ztools/src/ztools/logger.ts new file mode 100644 index 000000000..f35358cc3 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/logger.ts @@ -0,0 +1 @@ +export default console diff --git a/plugins/lx-music-ztools/src/ztools/metadata.ts b/plugins/lx-music-ztools/src/ztools/metadata.ts new file mode 100644 index 000000000..051a859b8 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/metadata.ts @@ -0,0 +1,2 @@ +import './contracts' +export const parseFile = (...args: any[]) => window.lxHost.invoke('metadata:parseFile', args) diff --git a/plugins/lx-music-ztools/src/ztools/nodejs.ts b/plugins/lx-music-ztools/src/ztools/nodejs.ts new file mode 100644 index 000000000..b43383a23 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/nodejs.ts @@ -0,0 +1,28 @@ +import path from 'path-browserify' +import crypto from 'crypto-browserify' +import { Buffer } from 'buffer' +import './contracts' +const invoke = (method: string, args: unknown[]) => window.lxHost.invoke('files:call', { method, args }) +export const joinPath = (...args: string[]) => path.join(...args) +export const extname = path.extname +export const basename = path.basename +export const dirname = path.dirname +export const toMD5 = (text: string) => crypto.createHash('md5').update(text).digest('hex') +export const b64DecodeUnicode = (text: string) => Buffer.from(text, 'base64').toString('utf8') +export const checkPath = (...args: any[]) => invoke('checkPath', args) +export const checkAndCreateDir = (...args: any[]) => invoke('checkAndCreateDir', args) +export const getFileStats = async (...args: any[]) => { + const stats = await invoke('getFileStats', args) + return stats && { ...stats, isFile: () => stats.isFile, isDirectory: () => stats.isDirectory } +} +export const createDir = (...args: any[]) => invoke('createDir', args) +export const removeFile = (...args: any[]) => invoke('removeFile', args) +export const readFile = async (...args: any[]) => Buffer.from(await invoke('readFile', args)) +export const gzipData = async (...args: any[]) => Buffer.from(await invoke('gzipData', args)) +export const gunzipData = (...args: any[]) => invoke('gunzipData', args) +export const saveLxConfigFile = (...args: any[]) => invoke('saveLxConfigFile', args) +export const readLxConfigFile = (...args: any[]) => invoke('readLxConfigFile', args) +export const saveStrToFile = (...args: any[]) => invoke('saveStrToFile', args) +export const copyFile = (...args: any[]) => invoke('copyFile', args) +export const moveFile = (...args: any[]) => invoke('moveFile', args) +export const getAddress = () => window.lxHost.readSnapshot('host:addresses') diff --git a/plugins/lx-music-ztools/src/ztools/os.ts b/plugins/lx-music-ztools/src/ztools/os.ts new file mode 100644 index 000000000..f8e16388e --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/os.ts @@ -0,0 +1,9 @@ +import './contracts' +const env = () => window.lxHost.readSnapshot('host:env') +export const release = () => env().release +export const homedir = () => env().homedir +export const tmpdir = () => env().tmpdir +export const arch = () => env().arch +export const platform = () => env().platform +export const cpus = () => env().cpus +export default { release, homedir, tmpdir, arch, platform, cpus } diff --git a/plugins/lx-music-ztools/src/ztools/player.js b/plugins/lx-music-ztools/src/ztools/player.js new file mode 100644 index 000000000..a5ee606e7 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/player.js @@ -0,0 +1,48 @@ +import * as actions from '@renderer/core/player' +import * as audio from '@renderer/plugins/player' +import { getListMusics } from '@renderer/store/list/action' +import { musicInfo, isPlay } from '@renderer/store/player/state' +import { watch } from '@common/utils/vueTools' + +// createAudio is idempotent and shares the original engine's single audio element. +audio.createAudio() +export function getState() { + const previous = window.lxHost.readSnapshot('player:state') + return { ...previous, status: isPlay.value ? 'playing' : audio.isEmpty() ? 'stoped' : 'paused', + name: musicInfo.name, singer: musicInfo.singer, albumName: musicInfo.album, + picUrl: musicInfo.pic || '', lyric: musicInfo.lrc || '', tlyric: musicInfo.tlrc || '', + rlyric: musicInfo.rlrc || '', lxlyric: musicInfo.lxlrc || '', + progress: audio.getCurrentTime(), duration: audio.getDuration(), playbackRate: audio.getPlaybackRate(), mute: audio.getMute() } +} +export async function dispatch(command) { + switch (command.type) { + case 'playList': + if (typeof command.listId !== 'string' || !Number.isInteger(command.index) || command.index < 0) throw new Error('Invalid playback position') + if (!(await getListMusics(command.listId))[command.index]) throw new Error('Playlist track does not exist') + actions.playList(command.listId, command.index); break + case 'play': actions.play(); break + case 'pause': actions.pause(); break + case 'playPause': actions.togglePlay(); break + case 'prev': await actions.playPrev(); break + case 'next': await actions.playNext(); break + case 'collect': actions.collectMusic(); break + case 'unCollect': actions.uncollectMusic(); break + case 'seek': + if (!Number.isFinite(command.value) || command.value < 0 || command.value > audio.getDuration()) throw new Error('Invalid seek position') + window.app_event.setProgress(command.value); break + case 'volume': + if (!Number.isFinite(command.value) || command.value < 0 || command.value > 1) throw new Error('Invalid volume') + window.app_event.setVolume(command.value); break + case 'mute': + if (typeof command.value !== 'boolean') throw new Error('Invalid mute state') + window.app_event.setVolumeIsMute(command.value); break + default: throw new Error('Unknown player command') + } +} +export function subscribe(listener) { + const notify = () => listener(getState()) + const disposers = [audio.onTimeupdate(notify), audio.onPlaying(notify), audio.onPause(notify), audio.onLoadeddata(notify), watch(musicInfo, notify)] + notify() + return () => disposers.forEach(dispose => dispose()) +} +window.lxPlayer = { getState, dispatch, subscribe } diff --git a/plugins/lx-music-ztools/src/ztools/rendererIpc.ts b/plugins/lx-music-ztools/src/ztools/rendererIpc.ts new file mode 100644 index 000000000..1a346a939 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/rendererIpc.ts @@ -0,0 +1,25 @@ +import './contracts' +const wrappers = new Map void>>() +export function rendererInvoke(name: string, params?: T): Promise { return window.lxHost.invoke(name, params) } +export function rendererSend(name: string, params?: T): void { window.lxHost.send(name, params) } +export function rendererSendSync(name: string, _params?: T): void { window.lxHost.readSnapshot(name) } +export function rendererOn(name: string, listener: Function): void { + const byListener = wrappers.get(name) || new Map() + const wrapper = (params: any) => listener({ event: null, params }) + if (byListener.has(listener)) return + byListener.set(listener, wrapper); wrappers.set(name, byListener) + window.lxHost.on(name, wrapper) +} +export function rendererOnce(name: string, listener: Function): void { + const wrapper = (params: any) => { rendererOff(name, listener); listener({ event: null, params }) } + const byListener = wrappers.get(name) || new Map() + byListener.set(listener, wrapper); wrappers.set(name, byListener) + window.lxHost.once(name, wrapper) +} +export function rendererOff(name: string, listener: Function): void { + const byListener = wrappers.get(name) + const wrapper = byListener?.get(listener) + if (wrapper) { window.lxHost.off(name, wrapper); byListener.delete(listener) } + if (!byListener?.size) wrappers.delete(name) +} +export function rendererOffAll(name: string): void { window.lxHost.offAll(name); wrappers.delete(name) } diff --git a/plugins/lx-music-ztools/src/ztools/request.ts b/plugins/lx-music-ztools/src/ztools/request.ts new file mode 100644 index 000000000..19bbedc12 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/request.ts @@ -0,0 +1,29 @@ +import './contracts' +import { Buffer } from 'buffer' +export function httpFetch(url: string, options: any = {}) { + const id = crypto.randomUUID() + const cancelHttp = () => window.lxHost.send('network:cancel', id) + const promise = window.lxHost.invoke('network:fetch', { ...options, id, url }).then(response => ({ ...response, raw: Buffer.from(response.raw) })) + return { promise, cancelHttp, abort: cancelHttp } +} +export const cancelHttp = request => request?.cancelHttp ? request.cancelHttp() : request?.abort?.() +export function http(url, options, callback) { + if (typeof options === 'function') { callback = options; options = {} } + const request = httpFetch(url, options) + request.promise.then(response => callback(null, response, response.body), error => callback(error, null, null)) + return request +} +export const httpGet = (url, options, callback?) => http(url, options, callback) +export const httpPost = (url, data, options, callback?) => { + if (typeof options === 'function') { callback = options; options = {} } + return http(url, { ...options, method: 'post', body: data }, callback) +} +export const http_jsonp = (url, options, callback) => { + const callbackName = 'jsonpCallback' + return http(`${url}${url.includes('?') ? '&' : '?'}${options.jsonpCallback}=${callbackName}`, { ...options, format: 'script' }, (error, response, body) => { + if (error) return callback(error, response, body) + try { callback(null, response, JSON.parse(body.replace(/^jsonpCallback\(({.*})\)$/, '$1'))) } + catch (cause) { callback(cause, response, null) } + }) +} +export const checkUrl = async (url, options = {}) => { const response = await httpFetch(url, { ...options, method: 'head' }).promise; if (response.statusCode !== 200) throw new Error(String(response.statusCode)) } diff --git a/plugins/lx-music-ztools/src/ztools/shell.css b/plugins/lx-music-ztools/src/ztools/shell.css new file mode 100644 index 000000000..d5518b1ad --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/shell.css @@ -0,0 +1,32 @@ +/* ZTools owns the outer window; LX fills its content view without desktop chrome. */ +html.lx-ztools-shell, +html.lx-ztools-shell body, +html.lx-ztools-shell #root, +html.lx-ztools-shell #container { + width: 100%; + height: 100%; + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + overflow: hidden; +} + +html.lx-ztools-shell #right { + min-width: 0; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; +} + +/* Collapse the complete toolbar/aside control group, including its reserved space. */ +html.lx-ztools-shell :is(#toolbar, #left) > :has(> button use[*|href^="#icon-window-close"]) { + display: none !important; +} + +/* Also cover playback details and either control-button position/language. */ +html.lx-ztools-shell button:has(use[*|href^="#icon-window-minimize"]), +html.lx-ztools-shell button:has(use[*|href^="#icon-window-close"]) { + display: none !important; +} diff --git a/plugins/lx-music-ztools/src/ztools/url.ts b/plugins/lx-music-ztools/src/ztools/url.ts new file mode 100644 index 000000000..631f10fae --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/url.ts @@ -0,0 +1,7 @@ +export const URL = globalThis.URL +export const URLSearchParams = globalThis.URLSearchParams +export function pathToFileURL(path: string) { + const normalized = path.replaceAll('\\', '/') + const prefix = normalized.startsWith('//') ? 'file:' : 'file:///' + return new URL(prefix + encodeURI(normalized).replaceAll('#', '%23').replaceAll('?', '%3F')) +} diff --git a/plugins/lx-music-ztools/src/ztools/worker.ts b/plugins/lx-music-ztools/src/ztools/worker.ts new file mode 100644 index 000000000..0072d7800 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/worker.ts @@ -0,0 +1,9 @@ +import './contracts' +const remote = (kind: string) => new Proxy({}, { get(_target, method: string) { + if (method === 'then') return undefined + return (...args: any[]) => window.lxHost.invoke(`worker:${kind}`, { method, args }) +} }) +export const createMainWorker = () => remote('main') +export const createDownloadWorker = () => remote('download') +export const proxyCallback = (callback: T): T => callback +export default () => ({ main: createMainWorker(), download: createDownloadWorker() }) diff --git a/plugins/lx-music-ztools/src/ztools/zlib.ts b/plugins/lx-music-ztools/src/ztools/zlib.ts new file mode 100644 index 000000000..33679b803 --- /dev/null +++ b/plugins/lx-music-ztools/src/ztools/zlib.ts @@ -0,0 +1,13 @@ +import * as pako from 'pako' +import { Buffer } from 'buffer' +export const constants = { Z_SYNC_FLUSH: 2 } +function callback(method: string, data: any, options: any, cb?: Function) { + if (typeof options === 'function') { cb = options; options = undefined } + try { const result = Buffer.from(pako[method](data, options)); queueMicrotask(() => cb(null, result)) } + catch (error) { queueMicrotask(() => cb(error)) } +} +export const inflate = (data, options, cb?) => callback('inflate', data, options, cb) +export const deflate = (data, options, cb?) => callback('deflate', data, options, cb) +export const deflateRaw = (data, options, cb?) => callback('deflateRaw', data, options, cb) +export const gzip = (data, options, cb?) => callback('gzip', data, options, cb) +export const gunzip = (data, options, cb?) => callback('ungzip', data, options, cb) diff --git a/plugins/lx-music-ztools/tests/ztools/bridge.test.cjs b/plugins/lx-music-ztools/tests/ztools/bridge.test.cjs new file mode 100644 index 000000000..b06fcc4fd --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/bridge.test.cjs @@ -0,0 +1,25 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const { createBridge } = require('../../src-ztools/preload/runtime/bridge.cjs') + +test('subscriptions remove the same callback and once fires once', async () => { + const bridge = createBridge({ double: value => value * 2 }) + let seen = 0 + const listener = value => { seen += value } + bridge.on('change', listener); bridge.once('change', listener) + bridge.emit('change', 1); bridge.emit('change', 1) + bridge.off('change', listener); bridge.emit('change', 5) + assert.equal(seen, 3) + assert.equal(await bridge.invoke('double', 6), 12) + await assert.rejects(bridge.invoke('__proto__'), { code: 'UNKNOWN_CHANNEL' }) + assert.throws(() => bridge.readSnapshot('unready'), { code: 'SNAPSHOT_UNREADY' }) + bridge.setSnapshot('setting', { volume: 0.5 }) + const setting = bridge.readSnapshot('setting'); setting.volume = 0 + assert.equal(bridge.readSnapshot('setting').volume, 0.5) +}) + +test('dispose clears subscriptions and prevents new work', async () => { + const bridge = createBridge({ ping: () => 'pong' }) + await bridge.dispose() + await assert.rejects(bridge.invoke('ping'), { code: 'DISPOSED' }) +}) diff --git a/plugins/lx-music-ztools/tests/ztools/files.test.cjs b/plugins/lx-music-ztools/tests/ztools/files.test.cjs new file mode 100644 index 000000000..91410a3f3 --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/files.test.cjs @@ -0,0 +1,18 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const { mkdtempSync, writeFileSync, mkdirSync } = require('node:fs') +const { tmpdir } = require('node:os') +const { join } = require('node:path') +const { createFileScope } = require('../../src-ztools/preload/runtime/files.cjs') + +test('local file capabilities allow chosen paths and reject traversal outside roots', () => { + const root = mkdtempSync(join(tmpdir(), 'lx-files-')) + const inside = join(root, 'chosen'); mkdirSync(inside) + const secret = join(root, 'other.txt'); writeFileSync(secret, 'outside') + const scope = createFileScope([inside]) + assert.equal(scope.authorize(join(inside, 'new.txt')), join(inside, 'new.txt')) + assert.throws(() => scope.authorize(join(inside, '..', 'other.txt')), { code: 'PATH_NOT_GRANTED' }) + scope.grant(secret, false) + assert.equal(scope.authorize(secret), secret) + assert.throws(() => scope.authorize(join(root, 'unselected.txt')), { code: 'PATH_NOT_GRANTED' }) +}) diff --git a/plugins/lx-music-ztools/tests/ztools/fixtures/audio-server.cjs b/plugins/lx-music-ztools/tests/ztools/fixtures/audio-server.cjs new file mode 100644 index 000000000..228150446 --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/fixtures/audio-server.cjs @@ -0,0 +1,37 @@ +const http = require('node:http') +function createWav() { + const rate = 22050, seconds = 12, bytes = rate * seconds * 2 + const file = Buffer.alloc(44 + bytes) + file.write('RIFF', 0); file.writeUInt32LE(36 + bytes, 4); file.write('WAVEfmt ', 8) + file.writeUInt32LE(16, 16); file.writeUInt16LE(1, 20); file.writeUInt16LE(1, 22) + file.writeUInt32LE(rate, 24); file.writeUInt32LE(rate * 2, 28); file.writeUInt16LE(2, 32); file.writeUInt16LE(16, 34) + file.write('data', 36); file.writeUInt32LE(bytes, 40) + for (let index = 0; index < rate * seconds; index++) file.writeInt16LE(Math.round(2048 * Math.sin(2 * Math.PI * 440 * index / rate)), 44 + index * 2) + return file +} +async function startAudioServer(scriptTemplate) { + const audio = createWav(), requests = [] + let base + const server = http.createServer((req, res) => { + const url = new URL(req.url, 'http://127.0.0.1') + requests.push({ path: url.pathname, range: req.headers.range }) + res.setHeader('Access-Control-Allow-Origin', '*') + if (url.pathname === '/source.js') { res.setHeader('Content-Type', 'text/javascript'); res.end(scriptTemplate.replaceAll('__BASE_URL__', base)); return } + if (url.pathname === '/resolve') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ url: base + '/audio.wav?id=' + url.searchParams.get('id') })); return } + if (url.pathname === '/audio.wav') { + res.setHeader('Content-Type', 'audio/wav'); res.setHeader('Accept-Ranges', 'bytes') + const range = /^bytes=(\d+)-(\d*)$/.exec(req.headers.range || '') + if (range) { + const start = Number(range[1]), end = Math.min(Number(range[2]) || audio.length - 1, audio.length - 1) + if (start >= audio.length || start > end) { res.writeHead(416); res.end(); return } + res.writeHead(206, { 'Content-Range': `bytes ${start}-${end}/${audio.length}`, 'Content-Length': end - start + 1 }); res.end(audio.subarray(start, end + 1)) + } else { res.setHeader('Content-Length', audio.length); res.end(audio) } + return + } + res.writeHead(404); res.end() + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + base = `http://127.0.0.1:${server.address().port}` + return { base, audio, requests, close: () => new Promise(resolve => { server.close(resolve); server.closeAllConnections() }) } +} +module.exports = { startAudioServer } diff --git a/plugins/lx-music-ztools/tests/ztools/fixtures/source-api-v2.js b/plugins/lx-music-ztools/tests/ztools/fixtures/source-api-v2.js new file mode 100644 index 000000000..fc8b324a4 --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/fixtures/source-api-v2.js @@ -0,0 +1,21 @@ +/** + * @name Controlled LX API test + * @description Local fixture; not a real music source + * @version 1.0.0 + * @author ZTools port tests + */ +const { EVENT_NAMES, request, utils } = globalThis.lx +lx.on(EVENT_NAMES.request, ({ source, action, info }) => new Promise((resolve, reject) => { + if (action === 'lyric') return resolve({ lyric: '[00:00.00]测试歌词' }) + request('__BASE_URL__/resolve?id=' + encodeURIComponent(info.musicInfo.id), { + method: 'get', headers: { 'X-LX-Hash': utils.crypto.md5('abc') }, timeout: 3000, + }, (error, response, body) => { + if (error) return reject(error) + if (response.statusCode !== 200 || typeof body.url !== 'string') return reject(new Error('bad fixture response')) + resolve(body.url) + }) +})) +lx.send(EVENT_NAMES.inited, { sources: { + kw: { type: 'music', actions: ['musicUrl'], qualitys: ['128k', '320k'] }, + local: { type: 'music', actions: ['lyric'], qualitys: [] }, +} }) diff --git a/plugins/lx-music-ztools/tests/ztools/lifecycle.test.cjs b/plugins/lx-music-ztools/tests/ztools/lifecycle.test.cjs new file mode 100644 index 000000000..8c8459eaa --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/lifecycle.test.cjs @@ -0,0 +1,14 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const { createLifecycle } = require('../../src-ztools/preload/runtime/lifecycle.cjs') + +test('enter initializes once and hide keeps services alive until kill', async () => { + let initialized = 0, disposed = 0 + const life = createLifecycle({ initialize: async () => { initialized++ }, dispose: async () => { disposed++ } }) + await Promise.all([life.enter({ code: 'music' }), life.enter({ code: 'music' })]) + await life.out(false); await life.detach() + assert.equal(initialized, 1); assert.equal(disposed, 0) + await life.out(true); await life.dispose() + assert.equal(disposed, 1) + await assert.rejects(life.enter({ code: 'music' }), { code: 'DISPOSED' }) +}) diff --git a/plugins/lx-music-ztools/tests/ztools/list-contract.test.cjs b/plugins/lx-music-ztools/tests/ztools/list-contract.test.cjs new file mode 100644 index 000000000..ee10fca5c --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/list-contract.test.cjs @@ -0,0 +1,28 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const { mkdtempSync } = require('node:fs') +const { tmpdir } = require('node:os') +const { join } = require('node:path') +const backend = require('../../src-ztools/preload/runtime/database.cjs') +const { createDataService } = require('../../src-ztools/preload/runtime/data.cjs') + +test('original LX list methods preserve deduplication, move order and SQL rollback', () => { + const dir = mkdtempSync(join(tmpdir(), 'lx-list-test-')) + const data = createDataService(dir) + const info = id => ({ id, name: id, source: null, sourceListId: null, locationUpdateTime: null }) + const music = (id, source) => ({ id, source, name: id, singer: 'test', interval: '00:01', meta: { musicId: 'same-platform-id', qualitys: [], _qualitys: {} } }) + const one = music('kw_1', 'kw'), two = music('tx_1', 'tx') + data.call('list', 'createUserLists', [-1, [info('a'), info('b')]]) + data.call('list', 'musicsAdd', ['a', [one, two], 'bottom']) + data.call('list', 'musicsAdd', ['a', [one], 'bottom']) + assert.deepEqual(data.call('list', 'getListMusics', ['a']).map(x => x.id), ['kw_1', 'tx_1']) + data.call('list', 'musicsMove', ['a', 'b', [one], 'top']) + assert.deepEqual(data.call('list', 'getListMusics', ['a']).map(x => x.id), ['tx_1']) + assert.deepEqual(data.call('list', 'getListMusics', ['b']).map(x => x.id), ['kw_1']) + assert.throws(() => data.call('list', 'createUserLists', [-1, [info('c'), info('c')]])) + assert.deepEqual(data.call('list', 'getAllUserList', []).map(x => x.id), ['a', 'b']) + data.close() + const reopened = createDataService(dir) + assert.deepEqual(reopened.call('list', 'getListMusics', ['b']).map(x => x.id), ['kw_1']) + reopened.close() +}) diff --git a/plugins/lx-music-ztools/tests/ztools/network.test.cjs b/plugins/lx-music-ztools/tests/ztools/network.test.cjs new file mode 100644 index 000000000..4d09ea572 --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/network.test.cjs @@ -0,0 +1,28 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const http = require('node:http') +const { createNetwork } = require('../../src-ztools/preload/runtime/network.cjs') + +test('LX transport preserves JSON/form/headers and cancels pending requests', async () => { + const server = http.createServer((req, res) => { + if (req.url === '/wait') return + let body = '' + req.on('data', chunk => body += chunk) + req.on('end', () => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ method: req.method, body, tag: req.headers['x-tag'] })) }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const network = createNetwork() + const base = `http://127.0.0.1:${server.address().port}` + try { + const [a, b] = await Promise.all([ + network.request({ url: base, headers: { 'x-tag': 'a' } }).result, + network.request({ url: base, method: 'post', form: { message: 'hello world' }, headers: { 'x-tag': 'b' } }).result, + ]) + assert.equal(a.body.tag, 'a'); assert.equal(b.body.tag, 'b') + assert.equal(b.body.method, 'POST'); assert.equal(b.body.body, 'message=hello%20world') + const pending = network.request({ url: base + '/wait' }) + pending.cancel() + await assert.rejects(pending.result, /取消|cancel/i) + await assert.rejects(network.request({ url: 'file:///C:/Windows/win.ini' }).result, /HTTP/) + } finally { network.dispose(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)) } +}) diff --git a/plugins/lx-music-ztools/tests/ztools/runtime.test.cjs b/plugins/lx-music-ztools/tests/ztools/runtime.test.cjs new file mode 100644 index 000000000..bccaedc44 --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/runtime.test.cjs @@ -0,0 +1,37 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const { mkdtempSync } = require('node:fs') +const { tmpdir } = require('node:os') +const { join } = require('node:path') +const { createBridge } = require('../../src-ztools/preload/runtime/bridge.cjs') +const { initializeRuntime } = require('../../src-ztools/preload/runtime/index.cjs') +const names = require('../../src-ztools/preload/upstream/common/ipcNames.cjs') + +test('original LX IPC handlers persist settings and publish committed list operations', async () => { + const bridge = createBridge() + const runtime = initializeRuntime({ bridge, directory: mkdtempSync(join(tmpdir(), 'lx-runtime-')) }) + await bridge.invoke(names.CMMON_EVENT_NAME.set_app_setting, { 'player.volume': 0.35 }) + assert.equal((await bridge.invoke(names.CMMON_EVENT_NAME.get_app_setting))['player.volume'], 0.35) + const notices = [] + bridge.subscribe(names.PLAYER_EVENT_NAME.list_add, value => notices.push(value)) + const info = { id: 'mine', name: 'My list', source: null, sourceListId: null, locationUpdateTime: null } + await bridge.invoke(names.PLAYER_EVENT_NAME.list_add, { position: 0, listInfos: [info] }) + assert.deepEqual((await bridge.invoke(names.PLAYER_EVENT_NAME.list_get)).map(x => x.id), ['mine']) + assert.equal(notices.length, 1) + await assert.rejects(bridge.invoke(names.PLAYER_EVENT_NAME.list_add, { position: 0, listInfos: [info] })) + assert.equal(notices.length, 1, 'failed SQL must not publish a list event') + const lx = require('../../src-ztools/preload/runtime/context.cjs').lx + const changes = [] + lx.event_app.on('updated_config', keys => changes.push(keys)) + lx.event_app.update_config({ 'player.volume': 0.6 }) + assert.equal((await bridge.invoke(names.CMMON_EVENT_NAME.get_app_setting))['player.volume'], 0.6) + assert.deepEqual(changes, [['player.volume']]) + const themes = await bridge.invoke(names.WIN_MAIN_RENDERER_EVENT_NAME.get_themes) + assert.ok(themes.themes.length > 0) + const custom = { ...themes.themes[0], id: 'user_test', name: 'Test theme', isCustom: true } + await bridge.invoke(names.WIN_MAIN_RENDERER_EVENT_NAME.save_theme, custom) + assert.equal((await bridge.invoke(names.WIN_MAIN_RENDERER_EVENT_NAME.get_themes)).userThemes.length, 1) + await bridge.invoke(names.WIN_MAIN_RENDERER_EVENT_NAME.remove_theme, custom.id) + assert.equal((await bridge.invoke(names.WIN_MAIN_RENDERER_EVENT_NAME.get_themes)).userThemes.length, 0) + await runtime.dispose() +}) diff --git a/plugins/lx-music-ztools/tests/ztools/settings.test.cjs b/plugins/lx-music-ztools/tests/ztools/settings.test.cjs new file mode 100644 index 000000000..4531b0ab3 --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/settings.test.cjs @@ -0,0 +1,16 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const { openDatabase } = require('../../src-ztools/preload/runtime/database.cjs') +const { createSettings } = require('../../src-ztools/preload/runtime/settings.cjs') + +test('settings persist atomically and do not accept unknown or mismatched keys', () => { + const db = openDatabase(':memory:') + db.exec('CREATE TABLE plugin_kv(namespace TEXT,key TEXT,value TEXT,PRIMARY KEY(namespace,key))') + const settings = createSettings(db) + settings.update({ 'player.volume': 0.25 }) + assert.equal(createSettings(db).get()['player.volume'], 0.25) + assert.throws(() => settings.update({ 'player.volume': 'bad' })) + assert.equal(settings.get()['player.volume'], 0.25) + assert.throws(() => settings.update({ arbitrary: 'bad' })) + db.close() +}) diff --git a/plugins/lx-music-ztools/tests/ztools/source-contract.test.cjs b/plugins/lx-music-ztools/tests/ztools/source-contract.test.cjs new file mode 100644 index 000000000..707ae261c --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/source-contract.test.cjs @@ -0,0 +1,24 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const { normalizeSources, validateRequest, normalizeResult } = require('../../src-ztools/preload/runtime/source/protocol.cjs') +const { parseInfo } = require('../../src-ztools/preload/runtime/source/manager.cjs') + +test('source capability filtering rejects unsupported actions and qualities', () => { + const sources = normalizeSources({ kw: { type: 'music', actions: ['musicUrl', 'fileRead'], qualitys: ['128k', 'unknown'] } }) + assert.deepEqual(sources, { kw: { type: 'music', actions: ['musicUrl'], qualitys: ['128k'] } }) + assert.throws(() => validateRequest(sources, { source: 'kw', action: 'fileRead' })) + assert.throws(() => validateRequest(sources, { source: 'kw', action: 'musicUrl', info: { type: 'flac' } })) +}) + +test('source results reject file URLs and oversize lyrics before reaching player', () => { + const input = { source: 'kw', action: 'musicUrl', info: { type: '128k' } } + assert.throws(() => normalizeResult(input, 'file:///C:/Windows/win.ini')) + assert.deepEqual(normalizeResult(input, 'https://example.com/audio.mp3'), { source: 'kw', action: 'musicUrl', data: { type: '128k', url: 'https://example.com/audio.mp3' } }) + assert.throws(() => normalizeResult({ source: 'local', action: 'lyric' }, { lyric: 'a'.repeat(51201) })) +}) + +test('source metadata imports require an LX header and enforce size limit', () => { + assert.throws(() => parseInfo('alert(1)'), /无效/) + assert.throws(() => parseInfo('a'.repeat(9_000_001)), /9_000_000/) + assert.equal(parseInfo('/**\n * @name Test source\n * @version 1.0\n */').name, 'Test source') +}) diff --git a/plugins/lx-music-ztools/tests/ztools/source-storage.test.cjs b/plugins/lx-music-ztools/tests/ztools/source-storage.test.cjs new file mode 100644 index 000000000..628426df2 --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/source-storage.test.cjs @@ -0,0 +1,14 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const { openDatabase } = require('../../src-ztools/preload/runtime/database.cjs') +const { createSources } = require('../../src-ztools/preload/runtime/source/manager.cjs') +const script = '/**\n * @name SQL source\n */\nlx.send("inited",{sources:{}})' + +test('failed source persistence does not expose an uncommitted imported script', () => { + const db = openDatabase(':memory:') + db.exec('CREATE TABLE plugin_kv(namespace TEXT,key TEXT,value TEXT,PRIMARY KEY(namespace,key)); CREATE TRIGGER fail_source BEFORE INSERT ON plugin_kv WHEN NEW.namespace=\'sources\' BEGIN SELECT RAISE(ABORT,\'full\'); END;') + const sources = createSources({ database: db }) + assert.throws(() => sources.importScript(script), /full/) + assert.equal(sources.list().length, 0) + db.close() +}) diff --git a/plugins/lx-music-ztools/tests/ztools/storage.test.cjs b/plugins/lx-music-ztools/tests/ztools/storage.test.cjs new file mode 100644 index 000000000..caf434dca --- /dev/null +++ b/plugins/lx-music-ztools/tests/ztools/storage.test.cjs @@ -0,0 +1,29 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const { mkdtempSync } = require('node:fs') +const { tmpdir } = require('node:os') +const { join } = require('node:path') +const { openDatabase } = require('../../src-ztools/preload/runtime/database.cjs') + +test('SQL transactions roll back all writes and survive reopening', () => { + const path = join(mkdtempSync(join(tmpdir(), 'lx-db-test-')), 'test.sqlite') + let db = openDatabase(path) + db.exec('CREATE TABLE items (id TEXT PRIMARY KEY, value TEXT)') + db.prepare('INSERT INTO items VALUES (?,?)').run('one', 'old') + assert.throws(db.transaction(() => { + db.prepare('UPDATE items SET value=? WHERE id=?').run('new', 'one') + db.prepare('INSERT INTO items VALUES (?,?)').run('one', 'duplicate') + })) + assert.equal(db.prepare('SELECT value FROM items').get().value, 'old') + db.close(); db = openDatabase(path) + assert.equal(db.prepare('SELECT value FROM items').get().value, 'old') + db.close() +}) + +test('LX named parameters accept optional fields and surplus music metadata', () => { + const db = openDatabase(':memory:') + db.exec('CREATE TABLE items (id TEXT PRIMARY KEY, value TEXT)') + db.prepare('INSERT INTO items VALUES (@id,@value)').run({ id: 'one', value: undefined, unrelated: 'ignored' }) + assert.deepEqual(db.prepare('SELECT * FROM items').get(), { id: 'one', value: null }) + db.close() +}) diff --git a/plugins/lx-music-ztools/tsconfig.json b/plugins/lx-music-ztools/tsconfig.json new file mode 100644 index 000000000..f08504387 --- /dev/null +++ b/plugins/lx-music-ztools/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": false, + "noImplicitAny": false, + "types": ["@ztools-center/ztools-api-types"] + }, + "include": ["src"], + "exclude": ["src/ztools/entry.ts"] +} diff --git a/plugins/lx-music-ztools/vendor/lx/LICENSE b/plugins/lx-music-ztools/vendor/lx/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/lx-music-ztools/vendor/lx/UPSTREAM.md b/plugins/lx-music-ztools/vendor/lx/UPSTREAM.md new file mode 100644 index 000000000..0444d0e83 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/UPSTREAM.md @@ -0,0 +1,5 @@ +# 上游来源 + +来自 https://github.com/lyswhut/lx-music-desktop ,基线 `323e4bac`,版本 2.12.6,Apache-2.0。`src/`、`package.json`、`tsconfig.json` 和 LICENSE 由该 Git 对象完整导出,作为只读上游基线,不依赖旁边的桌面版工作目录。 + +适配集中在本项目 `src/ztools`、`src-ztools/preload/runtime` 和构建脚本。运行服务逐模块去除 TS 类型并输出可读 CJS,保留注释,不 bundle/minify。脚本明确替换数据库、mainIpc、AppEvent 使用的主应用设置工具,将 `global.lx` 引用改为独立 runtime/context.cjs,避免原 renderer 初始化 window.lx 时覆盖服务状态。 diff --git a/plugins/lx-music-ztools/vendor/lx/package.json b/plugins/lx-music-ztools/vendor/lx/package.json new file mode 100644 index 000000000..9fa1f0cee --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/package.json @@ -0,0 +1,223 @@ +{ + "name": "lx-music-desktop", + "version": "2.12.6", + "description": "一个免费的音乐查找助手", + "main": "./dist/main.js", + "scripts": { + "pack": "node build-config/pack.js && npm run pack:win:setup:x64", + "pack:win": "node build-config/pack.js && npm run pack:win:setup:x64 && npm run pack:win:setup:x86 && npm run pack:win:setup:arm64 && npm run pack:win:setup:x86_64 && npm run pack:win:7z", + "pack:win:setup:x86_64": "node build-config/build-pack.js target=win arch=x86_64 type=setup", + "pack:win:setup:x64": "node build-config/build-pack.js target=win arch=x64 type=setup", + "pack:win:setup:x86": "node build-config/build-pack.js target=win arch=x86 type=setup", + "pack:win:setup:arm64": "node build-config/build-pack.js target=win arch=arm64 type=setup", + "pack:win:portable": "npm run pack:win:portable:x86_64 && npm run pack:win:portable:x64 && npm run pack:win:portable:x86", + "pack:win:portable:x86_64": "node build-config/build-pack.js target=win arch=x86_64 type=portable", + "pack:win:portable:x64": "node build-config/build-pack.js target=win arch=x64 type=portable", + "pack:win:portable:x86": "node build-config/build-pack.js target=win arch=x86 type=portable", + "pack:win:7z": "npm run pack:win:7z:x64", + "pack:win:7z:x64": "node build-config/build-pack.js target=win arch=x64 type=green", + "pack:win:7z:arm64": "node build-config/build-pack.js target=win arch=arm64 type=green", + "pack:win7:setup:x64": "node build-config/build-pack.js target=win arch=x64 type=win7_setup", + "pack:win7:7z:x64": "node build-config/build-pack.js target=win arch=x64 type=win7_green", + "pack:win7:7z:x86": "node build-config/build-pack.js target=win arch=x86 type=win7_green", + "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": "node build-config/build-pack.js target=linux arch=x64 type=appImage", + "pack:linux:deb": "npm run pack:linux:deb:amd64 && npm run pack:linux:deb:arm64 && npm run pack:linux:deb:armv7l", + "pack:linux:deb:amd64": "node build-config/build-pack.js target=linux arch=x64 type=deb", + "pack:linux:deb:arm64": "node build-config/build-pack.js target=linux arch=arm64 type=deb", + "pack:linux:deb:armv7l": "node build-config/build-pack.js target=linux arch=armv7l type=deb", + "pack:linux:rpm": "node build-config/build-pack.js target=linux arch=x64 type=rpm", + "pack:linux:pacman": "node build-config/build-pack.js target=linux arch=x64 type=pacman", + "pack:mac": "node build-config/pack.js && npm run pack:mac:dmg && npm run pack:mac:dmg:arm64", + "pack:mac:dmg": "node build-config/build-pack.js target=mac arch=x64 type=dmg", + "pack:mac:dmg:arm64": "node build-config/build-pack.js target=mac arch=arm64 type=dmg", + "pack:dir": "node build-config/pack.js && node build-config/build-pack.js target=dir", + "publish": "node publish", + "publish:win:setup:x64": "node build-config/build-pack.js target=win arch=x64 type=setup publish=always", + "publish:win:setup:x86": "node build-config/build-pack.js target=win arch=x86 type=setup publish=always", + "publish:win:setup:arm64": "node build-config/build-pack.js target=win arch=arm64 type=setup publish=always", + "publish:win:setup:x86_64": "node build-config/build-pack.js target=win arch=x86_64 type=setup publish=always", + "publish:win:portable": "npm run publish:win:portable:x86_64 && npm run publish:win:portable:x64 && npm run publish:win:portable:x86", + "publish:win:portable:x86_64": "node build-config/build-pack.js target=win arch=x86_64 type=portable publish=always", + "publish:win:portable:x64": "node build-config/build-pack.js target=win arch=x64 type=portable publish=always", + "publish:win:portable:x86": "node build-config/build-pack.js target=win arch=x86 type=portable publish=always", + "publish:win:7z:x64": "node build-config/build-pack.js target=win arch=x64 type=green publish=always", + "publish:win:7z:arm64": "node build-config/build-pack.js target=win arch=arm64 type=green publish=always", + "publish:win7:setup:x64": "node build-config/build-pack.js target=win arch=x64 type=win7_setup publish=always", + "publish:win7:7z:x64": "node build-config/build-pack.js target=win arch=x64 type=win7_green publish=always", + "publish:win7:7z:x86": "node build-config/build-pack.js target=win arch=x86 type=win7_green publish=always", + "publish:mac:dmg": "node build-config/build-pack.js target=mac arch=x64 type=dmg publish=always", + "publish:mac:dmg:arm64": "node build-config/build-pack.js target=mac arch=arm64 type=dmg publish=always", + "publish:linux:deb:amd64": "node build-config/build-pack.js target=linux arch=x64 type=deb publish=always", + "publish:linux:deb:arm64": "node build-config/build-pack.js target=linux arch=arm64 type=deb publish=always", + "publish:linux:deb:armv7l": "node build-config/build-pack.js target=linux arch=armv7l type=deb publish=always", + "publish:linux:appImage": "node build-config/build-pack.js target=linux arch=x64 type=appImage publish=always", + "publish:linux:rpm": "node build-config/build-pack.js target=linux arch=x64 type=rpm publish=always", + "publish:linux:pacman": "node build-config/build-pack.js target=linux arch=x64 type=pacman publish=always", + "dev": "cross-env NODE_OPTIONS=--max-http-header-size=200000 node build-config/runner-dev.js", + "build:theme": "node src/common/theme/createThemes.js", + "build": "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:renderer-scripts": "cross-env NODE_ENV=production webpack --config build-config/renderer-scripts/webpack.config.prod.js --progress", + "lint": "eslint --ext .ts,.js,.vue -f node_modules/eslint-formatter-friendly src", + "lint:fix": "eslint --ext .ts,.js,.vue -f node_modules/eslint-formatter-friendly --fix src", + "postinstall": "node build-config/postinstall.js", + "dp": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://127.0.0.1:2081 npm run pack", + "up": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://127.0.0.1:2081 npm i" + }, + "browserslist": [ + "Electron 22.3.0" + ], + "engines": { + "node": ">= 22", + "npm": ">=8.5.2" + }, + "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" + }, + "keywords": [ + "music-player", + "electron-app", + "vuejs3" + ], + "author": { + "name": "lyswhut", + "email": "lyswhut@qq.com" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/lyswhut/lx-music-desktop/issues" + }, + "homepage": "https://github.com/lyswhut/lx-music-desktop#readme", + "devDependencies": { + "@tsconfig/recommended": "^1.0.13", + "@types/better-sqlite3": "^7.6.13", + "@types/needle": "^3.3.0", + "@types/node": "^20.19.43", + "@types/tunnel": "^0.0.7", + "@types/ws": "8.5.4", + "@vue/language-plugin-pug": "^3.3.11", + "browserslist": "^4.28.9", + "chalk": "^4.1.2", + "changelog-parser": "3.0.1", + "copy-webpack-plugin": "^14.0.0", + "core-js": "^3.50.0", + "cross-env": "^10.1.0", + "css-loader": "^7.1.5", + "css-minimizer-webpack-plugin": "^8.0.0", + "del": "^6.1.1", + "electron": "42.11.6", + "electron-builder": "^26.15.7", + "electron-debug": "^3.2.0", + "electron-devtools-installer": "github:lyswhut/electron-devtools-installer#64596d615c1fc891eefd8aef1dfcb2c87aaadf03", + "electron-to-chromium": "^1.5.427", + "electron-updater": "6.8.9", + "eslint": "^8.57.1", + "eslint-config-standard": "^17.1.0", + "eslint-config-standard-with-typescript": "^43.0.1", + "eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53", + "eslint-plugin-html": "^8.2.0", + "eslint-plugin-vue": "^9.33.0", + "eslint-plugin-vue-pug": "^0.6.2", + "eslint-webpack-plugin": "^4.2.0", + "html-webpack-plugin": "^5.6.8", + "less": "^4.9.1", + "less-loader": "^13.0.0", + "mini-css-extract-plugin": "^2.10.2", + "node-loader": "^2.1.0", + "postcss": "^8.5.28", + "postcss-loader": "^8.2.1", + "postcss-pxtorem": "^6.1.0", + "pug": "^3.0.4", + "pug-plain-loader": "^1.1.0", + "rimraf": "^6.1.3", + "spinnies": "github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d", + "svg-sprite-loader": "^6.0.11", + "svg-transform-loader": "^2.0.13", + "svgo-loader": "^5.0.0", + "terser": "^5.51.2", + "terser-webpack-plugin": "^5.6.1", + "tree-kill": "^1.2.2", + "ts-loader": "^9.6.2", + "typescript": "5.9.3", + "vue-eslint-parser": "^9.4.3", + "vue-loader": "^17.4.2", + "webpack": "5.106.2", + "webpack-cli": "^7.2.3", + "webpack-dev-server": "5.2.6", + "webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1", + "webpack-merge": "^6.0.1" + }, + "dependencies": { + "@simonwep/pickr": "1.9.1", + "better-sqlite3": "^13.0.3", + "bufferutil": "^4.1.0", + "comlink": "~4.3.1", + "crypto-js": "^4.2.0", + "electron-log": "^5.4.4", + "font-list": "^2.1.0", + "iconv-lite": "^0.7.3", + "image-size": "^1.1.0", + "jschardet": "^3.1.4", + "long": "^5.3.2", + "message2call": "^0.1.3", + "music-metadata": "^11.15.0", + "needle": "github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060", + "node-id3": "^0.2.9", + "sortablejs": "^1.15.7", + "tunnel": "^0.0.6", + "undici": "^7.29.1", + "utf-8-validate": "^6.0.6", + "vue": "~3.3.13", + "vue-router": "~4.5.1", + "ws": "^8.21.3" + }, + "overrides": { + "got": "^11", + "json5": "latest", + "node-abi": "latest", + "minimatch": "latest", + "semver": "latest", + "posthtml": "latest", + "node-gyp": "latest", + "sockjs": { + "uuid": "^12" + }, + "svg-transform-loader": { + "postcss": "^8" + }, + "svg-sprite-loader": { + "postcss": "^8" + }, + "svg-baker": { + "postcss": "^8" + }, + "braces": "latest", + "node-gyp-build": "latest", + "micromatch": "latest", + "http-cache-semantics": "latest" + }, + "allowScripts": { + "bufferutil": false, + "core-js": false, + "electron-winstaller": false, + "utf-8-validate": false + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/.eslintrc.cjs b/plugins/lx-music-ztools/vendor/lx/src/common/.eslintrc.cjs new file mode 100644 index 000000000..b86028ebf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/.eslintrc.cjs @@ -0,0 +1,15 @@ +/* eslint-env node */ +const { base, typescript } = require('../../.eslintrc.base.cjs') + +module.exports = { + root: true, + ...base, + overrides: [ + { + ...typescript, + parserOptions: { + project: './tsconfig.json', + }, + }, + ], +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/config.ts b/plugins/lx-music-ztools/vendor/lx/src/common/config.ts new file mode 100644 index 000000000..492fa4a07 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/config.ts @@ -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 + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/constants.ts b/plugins/lx-music-ztools/vendor/lx/src/common/constants.ts new file mode 100644 index 000000000..5323dbed5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/constants.ts @@ -0,0 +1,84 @@ +export const URL_SCHEME_RXP = /^lxmusic:\/\// + +export const SPLIT_CHAR = { + DISLIKE_NAME: '@', + DISLIKE_NAME_ALIAS: '#', +} as const + +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', + SOUND_EFFECT: 'sound_effect', +} 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', + PLAY_LATER: null, +} 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', + + lastStartInfo: 'lastStartInfo', +} as const + +export const DEFAULT_SETTING = { + leaderboard: { + source: 'kw', + boardId: 'kw__16', + }, + + songList: { + source: 'kw', + sortId: 'new', + 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 + +export const TRAY_AUTO_ID = -1 diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/constants_sync.ts b/plugins/lx-music-ztools/vendor/lx/src/common/constants_sync.ts new file mode 100644 index 000000000..bb28093a6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/constants_sync.ts @@ -0,0 +1,74 @@ +export const ENV_PARAMS = [ + 'PORT', + 'BIND_IP', + 'CONFIG_PATH', + 'LOG_PATH', + 'DATA_PATH', + 'PROXY_HEADER', + 'MAX_SNAPSHOT_NUM', + 'LIST_ADD_MUSIC_LOCATION_TYPE', + 'LX_USER_', +] as const + + +export const LIST_IDS = { + DEFAULT: 'default', + LOVE: 'love', + TEMP: 'temp', + DOWNLOAD: 'download', + PLAY_LATER: null, +} as const + +export const SYNC_CODE = { + helloMsg: 'Hello~::^-^::~v4~', + idPrefix: 'OjppZDo6', + authMsg: 'lx-music auth::', + msgAuthFailed: 'Auth failed', + msgBlockedIp: 'Blocked IP', + msgConnect: 'lx-music connect', + + + authFailed: 'Auth failed', + missingAuthCode: 'Missing auth code', + getServiceIdFailed: 'Get service id failed', + connectServiceFailed: 'Connect service failed', + connecting: 'Connecting...', + unknownServiceAddress: 'Unknown service address', +} as const + +export const SYNC_CLOSE_CODE = { + normal: 1000, + failed: 4100, +} as const + +export const TRANS_MODE: Readonly> = { + merge_local_remote: 'merge_remote_local', + merge_remote_local: 'merge_local_remote', + overwrite_local_remote: 'overwrite_remote_local', + overwrite_remote_local: 'overwrite_local_remote', + overwrite_local_remote_full: 'overwrite_remote_local_full', + overwrite_remote_local_full: 'overwrite_local_remote_full', + cancel: 'cancel', +} as const + +export const File = { + serverDataPath: 'sync/server', + clientDataPath: 'sync/client', + + serverInfoJSON: 'serverInfo.json', + userDir: 'users', + userDevicesJSON: 'devices.json', + listDir: 'list', + listSnapshotDir: 'snapshot', + listSnapshotInfoJSON: 'snapshotInfo.json', + dislikeDir: 'dislike', + dislikeSnapshotDir: 'snapshot', + dislikeSnapshotInfoJSON: 'snapshotInfo.json', + + syncAuthKeysJSON: 'syncAuthKey.json', +} as const + +export const FeaturesList = [ + 'list', + 'dislike', +] as const diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/defaultHotKey.ts b/plugins/lx-music-ztools/vendor/lx/src/common/defaultHotKey.ts new file mode 100644 index 000000000..434325f0e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/defaultHotKey.ts @@ -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, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/defaultSetting.ts b/plugins/lx-music-ztools/vendor/lx/src/common/defaultSetting.ts new file mode 100644 index 000000000..e4ce730f6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/defaultSetting.ts @@ -0,0 +1,171 @@ +import path from 'node:path' +import os from 'node:os' + +const isMac = process.platform == 'darwin' +const isWin = process.platform == 'win32' + +const defaultSetting: LX.AppSetting = { + version: '2.1.0', + + 'common.windowSizeId': 3, + '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': isMac ? 'left' : 'right', + 'common.playBarProgressStyle': 'mini', + 'common.transparentWindow': !isMac, + 'common.tryAutoUpdate': true, + 'common.showChangeLog': true, + + 'player.startupAutoPlay': false, + 'player.togglePlayMethod': 'listLoop', + 'player.playQuality': '128k', + 'player.isShowTaskProgess': true, + 'player.isShowStatusBarLyric': false, + 'player.volume': 1, + 'player.powerSaveBlocker': true, + 'player.isMute': false, + 'player.playbackRate': 1, + 'player.preservesPitch': true, + 'player.isMaxOutputChannelCount': false, + 'player.mediaDeviceId': 'default', + 'player.isMediaDeviceRemovedStopPlay': false, + 'player.isShowLyricTranslation': false, + 'player.isShowLyricRoma': false, + 'player.isSwapLyricTranslationAndRoma': false, + 'player.isS2t': false, + 'player.isPlayLxlrc': !isMac, + 'player.isSavePlayTime': false, + 'player.audioVisualization': false, + 'player.waitPlayEndStop': true, + 'player.waitPlayEndStopTime': '', + 'player.autoSkipOnError': true, + 'player.isAutoCleanPlayedList': false, + 'player.soundEffect.convolution.fileName': '', + 'player.soundEffect.convolution.mainGain': 10, + 'player.soundEffect.convolution.sendGain': 0, + 'player.soundEffect.biquadFilter.hz31': 0, + 'player.soundEffect.biquadFilter.hz62': 0, + 'player.soundEffect.biquadFilter.hz125': 0, + 'player.soundEffect.biquadFilter.hz250': 0, + 'player.soundEffect.biquadFilter.hz500': 0, + 'player.soundEffect.biquadFilter.hz1000': 0, + 'player.soundEffect.biquadFilter.hz2000': 0, + 'player.soundEffect.biquadFilter.hz4000': 0, + 'player.soundEffect.biquadFilter.hz8000': 0, + 'player.soundEffect.biquadFilter.hz16000': 0, + 'player.soundEffect.panner.enable': false, + 'player.soundEffect.panner.soundR': 5, + 'player.soundEffect.panner.speed': 25, + 'player.soundEffect.pitchShifter.playbackRate': 1, + + 'playDetail.isZoomActiveLrc': false, + 'playDetail.isShowLyricProgressSetting': false, + 'playDetail.style.fontSize': 140, + 'playDetail.style.align': 'center', + 'playDetail.isDelayScroll': true, + + 'desktopLyric.enable': false, + 'desktopLyric.isLock': false, + 'desktopLyric.isAlwaysOnTop': false, + 'desktopLyric.isAlwaysOnTopLoop': false, + 'desktopLyric.isShowTaskbar': false, + 'desktopLyric.audioVisualization': false, + 'desktopLyric.fullscreenHide': true, + 'desktopLyric.pauseHide': true, + 'desktopLyric.width': 450, + 'desktopLyric.height': 300, + 'desktopLyric.x': null, + 'desktopLyric.y': null, + 'desktopLyric.isLockScreen': isWin, + 'desktopLyric.isDelayScroll': true, + 'desktopLyric.scrollAlign': 'center', + 'desktopLyric.isHoverHide': false, + 'desktopLyric.direction': 'horizontal', + 'desktopLyric.style.align': 'center', + 'desktopLyric.style.font': '', + 'desktopLyric.style.fontSize': 20, + 'desktopLyric.style.lineGap': 15, + 'desktopLyric.style.lyricUnplayColor': 'rgba(255, 255, 255, 1)', + 'desktopLyric.style.lyricPlayedColor': 'rgba(7, 197, 86, 1)', + 'desktopLyric.style.lyricShadowColor': 'rgba(0, 0, 0, 0.18)', + // 'desktopLyric.style.fontWeight': false, + 'desktopLyric.style.opacity': 95, + 'desktopLyric.style.ellipsis': false, + 'desktopLyric.style.isZoomActiveLrc': false, + 'desktopLyric.style.isFontWeightFont': true, + 'desktopLyric.style.isFontWeightLine': true, + 'desktopLyric.style.isFontWeightExtended': true, + + 'list.isClickPlayList': false, + 'list.isShowSource': true, + 'list.isSaveScrollLocation': true, + 'list.addMusicLocationType': 'top', + 'list.actionButtonsVisible': false, + + 'download.enable': false, + 'download.isSavePathGroupByListName': false, + 'download.savePath': path.join(os.homedir(), 'Desktop'), + 'download.fileName': '歌名 - 歌手', + 'download.maxDownloadNum': 3, + 'download.skipExistFile': true, + 'download.isDownloadLrc': false, + 'download.isDownloadLxLrc': true, + 'download.isDownloadTLrc': false, + 'download.isDownloadRLrc': false, + 'download.lrcFormat': 'utf8', + 'download.isEmbedPic': true, + 'download.isEmbedLyric': false, + 'download.isEmbedLyricLx': true, + 'download.isEmbedLyricT': false, + 'download.isEmbedLyricR': false, + 'download.isUseOtherSource': false, + + 'search.isShowHotSearch': false, + 'search.isShowHistorySearch': false, + 'search.isFocusSearchBox': false, + + 'network.proxy.enable': false, + 'network.proxy.host': '', + 'network.proxy.port': '', + + 'tray.enable': false, + // 'tray.isToTray': false, + 'tray.themeId': 0, + + 'sync.mode': 'server', + 'sync.enable': false, + 'sync.server.port': '23332', + 'sync.server.maxSsnapshotNum': 5, + 'sync.client.host': '', + + 'openAPI.enable': false, + 'openAPI.port': '23330', + 'openAPI.bindLan': false, + + // '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, 57, 71, 1)' +} + + +export default defaultSetting + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/error.ts b/plugins/lx-music-ztools/vendor/lx/src/common/error.ts new file mode 100644 index 000000000..5a25f597e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/error.ts @@ -0,0 +1,18 @@ +import { log } from './utils' + +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) +}) +process.on('unhandledRejection', (reason, p) => { + console.error('Unhandled Rejection at: Promise ', p) + console.error(' reason: ', reason) + log.error(reason) +}) diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/hotKey.ts b/plugins/lx-music-ztools/vendor/lx/src/common/hotKey.ts new file mode 100644 index 000000000..fd68c4679 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/hotKey.ts @@ -0,0 +1,126 @@ +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: '', + }, + seekbackward: { + name: 'seekbackward', + action: 'seekbackward', + type: '', + }, + seekforward: { + name: 'seekforward', + action: 'seekforward', + 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: '', + }, + music_love: { + name: 'music_love', + action: 'music_love', + type: '', + }, + music_unlove: { + name: 'music_unlove', + action: 'music_unlove', + type: '', + }, + music_dislike: { + name: 'music_dislike', + action: 'music_dislike', + 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: '', + }, + }, +} + +for (const type of Object.keys(hotKey) as Array) { + let keys = hotKey[type] + for (const key of Object.keys(keys) as Array) { + const keyInfo: LX.HotKey = keys[key] + keyInfo.action = `${type}_${keyInfo.action}` + keyInfo.name = `${type}_${keyInfo.name}` + keyInfo.type = keyName[type] as keyof typeof hotKey + } +} + +export const HOTKEY_COMMON = hotKey.common +export const HOTKEY_PLAYER = hotKey.player +export const HOTKEY_DESKTOP_LYRIC = hotKey.desktop_lyric diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/ipcNames.ts b/plugins/lx-music-ztools/vendor/lx/src/common/ipcNames.ts new file mode 100644 index 000000000..123a426e2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/ipcNames.ts @@ -0,0 +1,195 @@ +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', + }, + dislike: { + get_dislike_music_infos: 'get_dislike_music_infos', + add_dislike_music_infos: 'add_dislike_music_infos', + overwrite_dislike_music_infos: 'overwrite_dislike_music_infos', + clear_dislike_music_infos: 'clear_dislike_music_infos', + }, + 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_dir_in_explorer: 'open_dir_in_explorer', + open_dev_tools: 'open_dev_tools', + set_power_save_blocker: 'set_power_save_blocker', + + player_status: 'player_status', + change_tray: 'change_tray', + quit_update: 'quit_update', + update_check: 'update_check', + update_download_update: 'update_download_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_window_size: 'set_window_size', + + handle_request: 'handle_request', + cancel_request: 'cancel_request', + + + restart_window: 'restart_window', + + 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_sound_effect_eq_preset: 'get_sound_effect_eq_preset', + save_sound_effect_eq_preset: 'save_sound_effect_eq_preset', + get_sound_effect_convolution_preset: 'get_sound_effect_convolution_preset', + save_sound_effect_convolution_preset: 'save_sound_effect_convolution_preset', + // get_sound_effect_pitch_shifter_preset: 'get_sound_effect_pitch_shifter_preset', + // save_sound_effect_pitch_shifter_preset: 'save_sound_effect_pitch_shifter_preset', + get_hot_key: 'get_hot_key', + + import_user_api: 'import_user_api', + 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', + + open_api_action: 'open_api_action', + sync_action: 'sync_action', + sync_get_server_devices: 'sync_get_server_devices', + sync_remove_server_device: 'sync_remove_server_device', + + 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', + set_win_resizeable: 'set_win_resizeable', + key_down: 'key_down', + request_main_window_channel: 'request_main_window_channel', + provide_main_window_channel: 'provide_main_window_channel', + mouse_enter_leave: 'mouse_enter_leave', + }, + hotKey: { + enable: 'enable', + status: 'status', + set_config: 'set_config', + }, +} + + +for (const moduleName of Object.keys(modules) as Array) { + let eventNames = modules[moduleName] + for (const eventName of Object.keys(eventNames) as Array) { + eventNames[eventName] = `${moduleName}_${eventName as string}` as never + } +} + +// for (const moduleName of Object.keys(modules) as Array) { +// 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 DISLIKE_EVENT_NAME = modules.dislike +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 diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/mainIpc.ts b/plugins/lx-music-ztools/vendor/lx/src/common/mainIpc.ts new file mode 100644 index 000000000..e81572082 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/mainIpc.ts @@ -0,0 +1,54 @@ +import { ipcMain } from 'electron' + +export function mainOn(name: string, listener: LX.IpcMainEventListener): void +export function mainOn(name: string, listener: LX.IpcMainEventListenerParams): void +export function mainOn(name: string, listener: LX.IpcMainEventListenerParams): void { + ipcMain.on(name, (event, params) => { + listener({ event, params }) + }) +} + +export function mainOnce(name: string, listener: LX.IpcMainEventListener): void +export function mainOnce(name: string, listener: LX.IpcMainEventListenerParams): void +export function mainOnce(name: string, listener: LX.IpcMainEventListenerParams): 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(name: string, listener: LX.IpcMainInvokeEventListenerParams): void +export function mainHandle(name: string, listener: LX.IpcMainInvokeEventListenerValue): void +export function mainHandle(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue): void +export function mainHandle(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue): void { + ipcMain.handle(name, async(event, params) => { + return listener({ event, params }) + }) +} + +export function mainHandleOnce(name: string, listener: LX.IpcMainInvokeEventListener): void +export function mainHandleOnce(name: string, listener: LX.IpcMainInvokeEventListenerParams): void +export function mainHandleOnce(name: string, listener: LX.IpcMainInvokeEventListenerValue): void +export function mainHandleOnce(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue): void +export function mainHandleOnce(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue): void { + ipcMain.handleOnce(name, async(event, params) => { + return listener({ event, params }) + }) +} +export const mainHandleRemove = (name: string) => { + ipcMain.removeHandler(name) +} + +export function mainSend(window: Electron.BrowserWindow, name: string): void +export function mainSend(window: Electron.BrowserWindow, name: string, params: T): void +export function mainSend(window: Electron.BrowserWindow, name: string, params?: T): void { + window.webContents.send(name, params) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/rendererIpc.ts b/plugins/lx-music-ztools/vendor/lx/src/common/rendererIpc.ts new file mode 100644 index 000000000..5b662206c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/rendererIpc.ts @@ -0,0 +1,45 @@ +import { ipcRenderer } from 'electron' + +export function rendererSend(name: string): void +export function rendererSend(name: string, params: T): void +export function rendererSend(name: string, params?: T): void { + ipcRenderer.send(name, params) +} + +export function rendererSendSync(name: string): void +export function rendererSendSync(name: string, params: T): void +export function rendererSendSync(name: string, params?: T): void { + ipcRenderer.sendSync(name, params) +} + +export async function rendererInvoke(name: string): Promise +export async function rendererInvoke(name: string): Promise +export async function rendererInvoke(name: string, params: T): Promise +export async function rendererInvoke(name: string, params: T): Promise +export async function rendererInvoke (name: string, params?: T): Promise { + return ipcRenderer.invoke(name, params) +} + +export function rendererOn(name: string, listener: LX.IpcRendererEventListener): void +export function rendererOn(name: string, listener: LX.IpcRendererEventListenerParams): void +export function rendererOn(name: string, listener: LX.IpcRendererEventListenerParams): void { + ipcRenderer.on(name, (event, params) => { + listener({ event, params }) + }) +} + +export function rendererOnce(name: string, listener: LX.IpcRendererEventListener): void +export function rendererOnce(name: string, listener: LX.IpcRendererEventListenerParams): void +export function rendererOnce(name: string, listener: LX.IpcRendererEventListenerParams): 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) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/theme/colorUtils.js b/plugins/lx-music-ztools/vendor/lx/src/common/theme/colorUtils.js new file mode 100644 index 000000000..cff9fa790 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/theme/colorUtils.js @@ -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)})` +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/theme/createThemes.js b/plugins/lx-music-ztools/vendor/lx/src/common/theme/createThemes.js new file mode 100644 index 000000000..30e556468 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/theme/createThemes.js @@ -0,0 +1,384 @@ +//! 更新默认主题配置后,需要执行 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, + isDarkFont: false, + config: { + primary: 'rgb(77, 175, 124)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(52, 152, 219)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(77, 131, 175)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(245, 171, 53)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(214, 69, 65)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(241, 130, 141)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(155, 89, 182)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(108, 122, 137)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(51, 110, 123)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(79, 98, 208)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(150, 150, 150)', + font: 'rgb(229, 229, 229)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(74, 55, 82)', + font: 'rgb(33, 33, 33)', + '--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, + isDarkFont: false, + config: { + primary: 'rgb(87, 144, 167)', + font: 'rgb(33, 33, 33)', + '--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: 'china_ink', + name: '近墨者黑', + isDark: false, + isDarkFont: false, + config: { + primary: 'rgba(47, 47, 47, 1)', + font: 'rgb(33, 33, 33)', + '--color-app-background': 'rgba(255, 255, 255, 0)', + '--color-main-background': 'rgba(255, 255, 255, 0.8)', + '--color-nav-font': 'var(--color-primary)', + '--background-image': 'url(./theme_images/china_ink.jpg)', + '--background-image-position': 'center', + '--background-image-size': 'cover', + + + '--color-btn-hide': 'rgba(183, 212, 208, 1)', + '--color-btn-min': 'rgba(200, 214, 183, 1)', + '--color-btn-close': 'rgba(218, 195, 188, 1)', + + '--color-badge-primary': 'rgba(137, 70, 70, 1)', + '--color-badge-secondary': 'rgba(67, 139, 65, 1)', + '--color-badge-tertiary': 'rgba(132, 135, 65, 1)', + }, + }, + { + id: 'happy_new_year', + name: '新年快乐', + isDark: false, + isDarkFont: false, + config: { + primary: 'rgb(192, 57, 43)', + font: 'rgb(33, 33, 33)', + '--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, font, ...extInfo }, ...themeInfo }) => { + return { + ...themeInfo, + isCustom: false, + config: { + themeColors: createThemeColors(primary, font, themeInfo.isDark), + extInfo, + }, + } +}) + +fs.writeFileSync(path.join(__dirname, 'index.json'), JSON.stringify(themes, null, 2)) + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/china_ink.jpg b/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/china_ink.jpg new file mode 100644 index 000000000..4a6f21846 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/china_ink.jpg differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/jqbg.jpg b/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/jqbg.jpg new file mode 100644 index 000000000..589be584d Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/jqbg.jpg differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/landingMoon.png b/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/landingMoon.png new file mode 100644 index 000000000..14fc434b1 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/landingMoon.png differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/myzcbg.jpg b/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/myzcbg.jpg new file mode 100644 index 000000000..a618a0635 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/myzcbg.jpg differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/xnkl.png b/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/xnkl.png new file mode 100644 index 000000000..13681d798 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/common/theme/images/xnkl.png differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/theme/index.json b/plugins/lx-music-ztools/vendor/lx/src/common/theme/index.json new file mode 100644 index 000000000..5c30e05db --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/theme/index.json @@ -0,0 +1,3857 @@ +[ + { + "id": "green", + "name": "绿意盎然", + "isDark": false, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(77, 175, 124)", + "--color-primary-dark-100": "rgb(69,158,112)", + "--color-primary-dark-100-alpha-100": "rgba(69, 158, 112, 0.90)", + "--color-primary-alpha-100": "rgba(77, 175, 124, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(69, 158, 112, 0.80)", + "--color-primary-alpha-200": "rgba(77, 175, 124, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(69, 158, 112, 0.70)", + "--color-primary-alpha-300": "rgba(77, 175, 124, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(69, 158, 112, 0.60)", + "--color-primary-alpha-400": "rgba(77, 175, 124, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(69, 158, 112, 0.50)", + "--color-primary-alpha-500": "rgba(77, 175, 124, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(69, 158, 112, 0.40)", + "--color-primary-alpha-600": "rgba(77, 175, 124, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(69, 158, 112, 0.30)", + "--color-primary-alpha-700": "rgba(77, 175, 124, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(69, 158, 112, 0.20)", + "--color-primary-alpha-800": "rgba(77, 175, 124, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(69, 158, 112, 0.10)", + "--color-primary-alpha-900": "rgba(77, 175, 124, 0.10)", + "--color-primary-dark-200": "rgb(62,142,101)", + "--color-primary-dark-200-alpha-100": "rgba(62, 142, 101, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(62, 142, 101, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(62, 142, 101, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(62, 142, 101, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(62, 142, 101, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(62, 142, 101, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(62, 142, 101, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(62, 142, 101, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(62, 142, 101, 0.10)", + "--color-primary-dark-300": "rgb(56,128,91)", + "--color-primary-dark-300-alpha-100": "rgba(56, 128, 91, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(56, 128, 91, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(56, 128, 91, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(56, 128, 91, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(56, 128, 91, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(56, 128, 91, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(56, 128, 91, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(56, 128, 91, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(56, 128, 91, 0.10)", + "--color-primary-dark-400": "rgb(50,115,82)", + "--color-primary-dark-400-alpha-100": "rgba(50, 115, 82, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(50, 115, 82, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(50, 115, 82, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(50, 115, 82, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(50, 115, 82, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(50, 115, 82, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(50, 115, 82, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(50, 115, 82, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(50, 115, 82, 0.10)", + "--color-primary-dark-500": "rgb(45,104,74)", + "--color-primary-dark-500-alpha-100": "rgba(45, 104, 74, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(45, 104, 74, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(45, 104, 74, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(45, 104, 74, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(45, 104, 74, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(45, 104, 74, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(45, 104, 74, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(45, 104, 74, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(45, 104, 74, 0.10)", + "--color-primary-dark-600": "rgb(41,94,67)", + "--color-primary-dark-600-alpha-100": "rgba(41, 94, 67, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(41, 94, 67, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(41, 94, 67, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(41, 94, 67, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(41, 94, 67, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(41, 94, 67, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(41, 94, 67, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(41, 94, 67, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(41, 94, 67, 0.10)", + "--color-primary-dark-700": "rgb(37,85,60)", + "--color-primary-dark-700-alpha-100": "rgba(37, 85, 60, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(37, 85, 60, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(37, 85, 60, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(37, 85, 60, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(37, 85, 60, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(37, 85, 60, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(37, 85, 60, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(37, 85, 60, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(37, 85, 60, 0.10)", + "--color-primary-dark-800": "rgb(33,77,54)", + "--color-primary-dark-800-alpha-100": "rgba(33, 77, 54, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(33, 77, 54, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(33, 77, 54, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(33, 77, 54, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(33, 77, 54, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(33, 77, 54, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(33, 77, 54, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(33, 77, 54, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(33, 77, 54, 0.10)", + "--color-primary-dark-900": "rgb(30,69,49)", + "--color-primary-dark-900-alpha-100": "rgba(30, 69, 49, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(30, 69, 49, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(30, 69, 49, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(30, 69, 49, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(30, 69, 49, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(30, 69, 49, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(30, 69, 49, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(30, 69, 49, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(30, 69, 49, 0.10)", + "--color-primary-dark-1000": "rgb(27,62,44)", + "--color-primary-dark-1000-alpha-100": "rgba(27, 62, 44, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(27, 62, 44, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(27, 62, 44, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(27, 62, 44, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(27, 62, 44, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(27, 62, 44, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(27, 62, 44, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(27, 62, 44, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(27, 62, 44, 0.10)", + "--color-primary-light-100": "rgb(113,191,150)", + "--color-primary-light-100-alpha-100": "rgba(113, 191, 150, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(113, 191, 150, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(113, 191, 150, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(113, 191, 150, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(113, 191, 150, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(113, 191, 150, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(113, 191, 150, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(113, 191, 150, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(113, 191, 150, 0.10)", + "--color-primary-light-200": "rgb(141,204,171)", + "--color-primary-light-200-alpha-100": "rgba(141, 204, 171, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(141, 204, 171, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(141, 204, 171, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(141, 204, 171, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(141, 204, 171, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(141, 204, 171, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(141, 204, 171, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(141, 204, 171, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(141, 204, 171, 0.10)", + "--color-primary-light-300": "rgb(164,214,188)", + "--color-primary-light-300-alpha-100": "rgba(164, 214, 188, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(164, 214, 188, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(164, 214, 188, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(164, 214, 188, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(164, 214, 188, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(164, 214, 188, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(164, 214, 188, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(164, 214, 188, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(164, 214, 188, 0.10)", + "--color-primary-light-400": "rgb(182,222,201)", + "--color-primary-light-400-alpha-100": "rgba(182, 222, 201, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(182, 222, 201, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(182, 222, 201, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(182, 222, 201, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(182, 222, 201, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(182, 222, 201, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(182, 222, 201, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(182, 222, 201, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(182, 222, 201, 0.10)", + "--color-primary-light-500": "rgb(197,229,212)", + "--color-primary-light-500-alpha-100": "rgba(197, 229, 212, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(197, 229, 212, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(197, 229, 212, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(197, 229, 212, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(197, 229, 212, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(197, 229, 212, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(197, 229, 212, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(197, 229, 212, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(197, 229, 212, 0.10)", + "--color-primary-light-600": "rgb(209,234,221)", + "--color-primary-light-600-alpha-100": "rgba(209, 234, 221, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(209, 234, 221, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(209, 234, 221, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(209, 234, 221, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(209, 234, 221, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(209, 234, 221, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(209, 234, 221, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(209, 234, 221, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(209, 234, 221, 0.10)", + "--color-primary-light-700": "rgb(218,238,228)", + "--color-primary-light-700-alpha-100": "rgba(218, 238, 228, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(218, 238, 228, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(218, 238, 228, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(218, 238, 228, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(218, 238, 228, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(218, 238, 228, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(218, 238, 228, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(218, 238, 228, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(218, 238, 228, 0.10)", + "--color-primary-light-800": "rgb(225,241,233)", + "--color-primary-light-800-alpha-100": "rgba(225, 241, 233, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(225, 241, 233, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(225, 241, 233, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(225, 241, 233, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(225, 241, 233, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(225, 241, 233, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(225, 241, 233, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(225, 241, 233, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(225, 241, 233, 0.10)", + "--color-primary-light-900": "rgb(231,244,237)", + "--color-primary-light-900-alpha-100": "rgba(231, 244, 237, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(231, 244, 237, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(231, 244, 237, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(231, 244, 237, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(231, 244, 237, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(231, 244, 237, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(231, 244, 237, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(231, 244, 237, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(231, 244, 237, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(77, 175, 124)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(52, 152, 219)", + "--color-primary-dark-100": "rgb(47,137,197)", + "--color-primary-dark-100-alpha-100": "rgba(47, 137, 197, 0.90)", + "--color-primary-alpha-100": "rgba(52, 152, 219, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(47, 137, 197, 0.80)", + "--color-primary-alpha-200": "rgba(52, 152, 219, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(47, 137, 197, 0.70)", + "--color-primary-alpha-300": "rgba(52, 152, 219, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(47, 137, 197, 0.60)", + "--color-primary-alpha-400": "rgba(52, 152, 219, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(47, 137, 197, 0.50)", + "--color-primary-alpha-500": "rgba(52, 152, 219, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(47, 137, 197, 0.40)", + "--color-primary-alpha-600": "rgba(52, 152, 219, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(47, 137, 197, 0.30)", + "--color-primary-alpha-700": "rgba(52, 152, 219, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(47, 137, 197, 0.20)", + "--color-primary-alpha-800": "rgba(52, 152, 219, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(47, 137, 197, 0.10)", + "--color-primary-alpha-900": "rgba(52, 152, 219, 0.10)", + "--color-primary-dark-200": "rgb(42,123,177)", + "--color-primary-dark-200-alpha-100": "rgba(42, 123, 177, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(42, 123, 177, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(42, 123, 177, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(42, 123, 177, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(42, 123, 177, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(42, 123, 177, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(42, 123, 177, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(42, 123, 177, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(42, 123, 177, 0.10)", + "--color-primary-dark-300": "rgb(38,111,159)", + "--color-primary-dark-300-alpha-100": "rgba(38, 111, 159, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(38, 111, 159, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(38, 111, 159, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(38, 111, 159, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(38, 111, 159, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(38, 111, 159, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(38, 111, 159, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(38, 111, 159, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(38, 111, 159, 0.10)", + "--color-primary-dark-400": "rgb(34,100,143)", + "--color-primary-dark-400-alpha-100": "rgba(34, 100, 143, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(34, 100, 143, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(34, 100, 143, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(34, 100, 143, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(34, 100, 143, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(34, 100, 143, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(34, 100, 143, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(34, 100, 143, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(34, 100, 143, 0.10)", + "--color-primary-dark-500": "rgb(31,90,129)", + "--color-primary-dark-500-alpha-100": "rgba(31, 90, 129, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(31, 90, 129, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(31, 90, 129, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(31, 90, 129, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(31, 90, 129, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(31, 90, 129, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(31, 90, 129, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(31, 90, 129, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(31, 90, 129, 0.10)", + "--color-primary-dark-600": "rgb(28,81,116)", + "--color-primary-dark-600-alpha-100": "rgba(28, 81, 116, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(28, 81, 116, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(28, 81, 116, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(28, 81, 116, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(28, 81, 116, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(28, 81, 116, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(28, 81, 116, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(28, 81, 116, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(28, 81, 116, 0.10)", + "--color-primary-dark-700": "rgb(25,73,104)", + "--color-primary-dark-700-alpha-100": "rgba(25, 73, 104, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(25, 73, 104, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(25, 73, 104, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(25, 73, 104, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(25, 73, 104, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(25, 73, 104, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(25, 73, 104, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(25, 73, 104, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(25, 73, 104, 0.10)", + "--color-primary-dark-800": "rgb(23,66,94)", + "--color-primary-dark-800-alpha-100": "rgba(23, 66, 94, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(23, 66, 94, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(23, 66, 94, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(23, 66, 94, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(23, 66, 94, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(23, 66, 94, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(23, 66, 94, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(23, 66, 94, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(23, 66, 94, 0.10)", + "--color-primary-dark-900": "rgb(21,59,85)", + "--color-primary-dark-900-alpha-100": "rgba(21, 59, 85, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(21, 59, 85, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(21, 59, 85, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(21, 59, 85, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(21, 59, 85, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(21, 59, 85, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(21, 59, 85, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(21, 59, 85, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(21, 59, 85, 0.10)", + "--color-primary-dark-1000": "rgb(19,53,77)", + "--color-primary-dark-1000-alpha-100": "rgba(19, 53, 77, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(19, 53, 77, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(19, 53, 77, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(19, 53, 77, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(19, 53, 77, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(19, 53, 77, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(19, 53, 77, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(19, 53, 77, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(19, 53, 77, 0.10)", + "--color-primary-light-100": "rgb(93,173,226)", + "--color-primary-light-100-alpha-100": "rgba(93, 173, 226, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(93, 173, 226, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(93, 173, 226, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(93, 173, 226, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(93, 173, 226, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(93, 173, 226, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(93, 173, 226, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(93, 173, 226, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(93, 173, 226, 0.10)", + "--color-primary-light-200": "rgb(125,189,232)", + "--color-primary-light-200-alpha-100": "rgba(125, 189, 232, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(125, 189, 232, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(125, 189, 232, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(125, 189, 232, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(125, 189, 232, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(125, 189, 232, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(125, 189, 232, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(125, 189, 232, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(125, 189, 232, 0.10)", + "--color-primary-light-300": "rgb(151,202,237)", + "--color-primary-light-300-alpha-100": "rgba(151, 202, 237, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(151, 202, 237, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(151, 202, 237, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(151, 202, 237, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(151, 202, 237, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(151, 202, 237, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(151, 202, 237, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(151, 202, 237, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(151, 202, 237, 0.10)", + "--color-primary-light-400": "rgb(172,213,241)", + "--color-primary-light-400-alpha-100": "rgba(172, 213, 241, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(172, 213, 241, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(172, 213, 241, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(172, 213, 241, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(172, 213, 241, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(172, 213, 241, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(172, 213, 241, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(172, 213, 241, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(172, 213, 241, 0.10)", + "--color-primary-light-500": "rgb(189,221,244)", + "--color-primary-light-500-alpha-100": "rgba(189, 221, 244, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(189, 221, 244, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(189, 221, 244, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(189, 221, 244, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(189, 221, 244, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(189, 221, 244, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(189, 221, 244, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(189, 221, 244, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(189, 221, 244, 0.10)", + "--color-primary-light-600": "rgb(202,228,246)", + "--color-primary-light-600-alpha-100": "rgba(202, 228, 246, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(202, 228, 246, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(202, 228, 246, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(202, 228, 246, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(202, 228, 246, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(202, 228, 246, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(202, 228, 246, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(202, 228, 246, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(202, 228, 246, 0.10)", + "--color-primary-light-700": "rgb(213,233,248)", + "--color-primary-light-700-alpha-100": "rgba(213, 233, 248, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(213, 233, 248, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(213, 233, 248, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(213, 233, 248, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(213, 233, 248, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(213, 233, 248, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(213, 233, 248, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(213, 233, 248, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(213, 233, 248, 0.10)", + "--color-primary-light-800": "rgb(221,237,249)", + "--color-primary-light-800-alpha-100": "rgba(221, 237, 249, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(221, 237, 249, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(221, 237, 249, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(221, 237, 249, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(221, 237, 249, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(221, 237, 249, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(221, 237, 249, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(221, 237, 249, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(221, 237, 249, 0.10)", + "--color-primary-light-900": "rgb(228,241,250)", + "--color-primary-light-900-alpha-100": "rgba(228, 241, 250, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(228, 241, 250, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(228, 241, 250, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(228, 241, 250, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(228, 241, 250, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(228, 241, 250, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(228, 241, 250, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(228, 241, 250, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(228, 241, 250, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(52, 152, 219)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(77, 131, 175)", + "--color-primary-dark-100": "rgb(69,118,158)", + "--color-primary-dark-100-alpha-100": "rgba(69, 118, 158, 0.90)", + "--color-primary-alpha-100": "rgba(77, 131, 175, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(69, 118, 158, 0.80)", + "--color-primary-alpha-200": "rgba(77, 131, 175, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(69, 118, 158, 0.70)", + "--color-primary-alpha-300": "rgba(77, 131, 175, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(69, 118, 158, 0.60)", + "--color-primary-alpha-400": "rgba(77, 131, 175, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(69, 118, 158, 0.50)", + "--color-primary-alpha-500": "rgba(77, 131, 175, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(69, 118, 158, 0.40)", + "--color-primary-alpha-600": "rgba(77, 131, 175, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(69, 118, 158, 0.30)", + "--color-primary-alpha-700": "rgba(77, 131, 175, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(69, 118, 158, 0.20)", + "--color-primary-alpha-800": "rgba(77, 131, 175, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(69, 118, 158, 0.10)", + "--color-primary-alpha-900": "rgba(77, 131, 175, 0.10)", + "--color-primary-dark-200": "rgb(62,106,142)", + "--color-primary-dark-200-alpha-100": "rgba(62, 106, 142, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(62, 106, 142, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(62, 106, 142, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(62, 106, 142, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(62, 106, 142, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(62, 106, 142, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(62, 106, 142, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(62, 106, 142, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(62, 106, 142, 0.10)", + "--color-primary-dark-300": "rgb(56,95,128)", + "--color-primary-dark-300-alpha-100": "rgba(56, 95, 128, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(56, 95, 128, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(56, 95, 128, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(56, 95, 128, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(56, 95, 128, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(56, 95, 128, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(56, 95, 128, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(56, 95, 128, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(56, 95, 128, 0.10)", + "--color-primary-dark-400": "rgb(50,86,115)", + "--color-primary-dark-400-alpha-100": "rgba(50, 86, 115, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(50, 86, 115, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(50, 86, 115, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(50, 86, 115, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(50, 86, 115, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(50, 86, 115, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(50, 86, 115, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(50, 86, 115, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(50, 86, 115, 0.10)", + "--color-primary-dark-500": "rgb(45,77,104)", + "--color-primary-dark-500-alpha-100": "rgba(45, 77, 104, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(45, 77, 104, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(45, 77, 104, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(45, 77, 104, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(45, 77, 104, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(45, 77, 104, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(45, 77, 104, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(45, 77, 104, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(45, 77, 104, 0.10)", + "--color-primary-dark-600": "rgb(41,69,94)", + "--color-primary-dark-600-alpha-100": "rgba(41, 69, 94, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(41, 69, 94, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(41, 69, 94, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(41, 69, 94, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(41, 69, 94, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(41, 69, 94, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(41, 69, 94, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(41, 69, 94, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(41, 69, 94, 0.10)", + "--color-primary-dark-700": "rgb(37,62,85)", + "--color-primary-dark-700-alpha-100": "rgba(37, 62, 85, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(37, 62, 85, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(37, 62, 85, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(37, 62, 85, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(37, 62, 85, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(37, 62, 85, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(37, 62, 85, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(37, 62, 85, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(37, 62, 85, 0.10)", + "--color-primary-dark-800": "rgb(33,56,77)", + "--color-primary-dark-800-alpha-100": "rgba(33, 56, 77, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(33, 56, 77, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(33, 56, 77, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(33, 56, 77, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(33, 56, 77, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(33, 56, 77, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(33, 56, 77, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(33, 56, 77, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(33, 56, 77, 0.10)", + "--color-primary-dark-900": "rgb(30,50,69)", + "--color-primary-dark-900-alpha-100": "rgba(30, 50, 69, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(30, 50, 69, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(30, 50, 69, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(30, 50, 69, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(30, 50, 69, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(30, 50, 69, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(30, 50, 69, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(30, 50, 69, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(30, 50, 69, 0.10)", + "--color-primary-dark-1000": "rgb(27,45,62)", + "--color-primary-dark-1000-alpha-100": "rgba(27, 45, 62, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(27, 45, 62, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(27, 45, 62, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(27, 45, 62, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(27, 45, 62, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(27, 45, 62, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(27, 45, 62, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(27, 45, 62, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(27, 45, 62, 0.10)", + "--color-primary-light-100": "rgb(113,156,191)", + "--color-primary-light-100-alpha-100": "rgba(113, 156, 191, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(113, 156, 191, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(113, 156, 191, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(113, 156, 191, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(113, 156, 191, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(113, 156, 191, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(113, 156, 191, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(113, 156, 191, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(113, 156, 191, 0.10)", + "--color-primary-light-200": "rgb(141,176,204)", + "--color-primary-light-200-alpha-100": "rgba(141, 176, 204, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(141, 176, 204, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(141, 176, 204, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(141, 176, 204, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(141, 176, 204, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(141, 176, 204, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(141, 176, 204, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(141, 176, 204, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(141, 176, 204, 0.10)", + "--color-primary-light-300": "rgb(164,192,214)", + "--color-primary-light-300-alpha-100": "rgba(164, 192, 214, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(164, 192, 214, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(164, 192, 214, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(164, 192, 214, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(164, 192, 214, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(164, 192, 214, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(164, 192, 214, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(164, 192, 214, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(164, 192, 214, 0.10)", + "--color-primary-light-400": "rgb(182,205,222)", + "--color-primary-light-400-alpha-100": "rgba(182, 205, 222, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(182, 205, 222, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(182, 205, 222, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(182, 205, 222, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(182, 205, 222, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(182, 205, 222, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(182, 205, 222, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(182, 205, 222, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(182, 205, 222, 0.10)", + "--color-primary-light-500": "rgb(197,215,229)", + "--color-primary-light-500-alpha-100": "rgba(197, 215, 229, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(197, 215, 229, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(197, 215, 229, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(197, 215, 229, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(197, 215, 229, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(197, 215, 229, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(197, 215, 229, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(197, 215, 229, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(197, 215, 229, 0.10)", + "--color-primary-light-600": "rgb(209,223,234)", + "--color-primary-light-600-alpha-100": "rgba(209, 223, 234, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(209, 223, 234, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(209, 223, 234, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(209, 223, 234, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(209, 223, 234, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(209, 223, 234, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(209, 223, 234, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(209, 223, 234, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(209, 223, 234, 0.10)", + "--color-primary-light-700": "rgb(218,229,238)", + "--color-primary-light-700-alpha-100": "rgba(218, 229, 238, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(218, 229, 238, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(218, 229, 238, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(218, 229, 238, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(218, 229, 238, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(218, 229, 238, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(218, 229, 238, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(218, 229, 238, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(218, 229, 238, 0.10)", + "--color-primary-light-800": "rgb(225,234,241)", + "--color-primary-light-800-alpha-100": "rgba(225, 234, 241, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(225, 234, 241, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(225, 234, 241, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(225, 234, 241, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(225, 234, 241, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(225, 234, 241, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(225, 234, 241, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(225, 234, 241, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(225, 234, 241, 0.10)", + "--color-primary-light-900": "rgb(231,238,244)", + "--color-primary-light-900-alpha-100": "rgba(231, 238, 244, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(231, 238, 244, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(231, 238, 244, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(231, 238, 244, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(231, 238, 244, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(231, 238, 244, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(231, 238, 244, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(231, 238, 244, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(231, 238, 244, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(77, 131, 175)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(245, 171, 53)", + "--color-primary-dark-100": "rgb(221,154,48)", + "--color-primary-dark-100-alpha-100": "rgba(221, 154, 48, 0.90)", + "--color-primary-alpha-100": "rgba(245, 171, 53, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(221, 154, 48, 0.80)", + "--color-primary-alpha-200": "rgba(245, 171, 53, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(221, 154, 48, 0.70)", + "--color-primary-alpha-300": "rgba(245, 171, 53, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(221, 154, 48, 0.60)", + "--color-primary-alpha-400": "rgba(245, 171, 53, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(221, 154, 48, 0.50)", + "--color-primary-alpha-500": "rgba(245, 171, 53, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(221, 154, 48, 0.40)", + "--color-primary-alpha-600": "rgba(245, 171, 53, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(221, 154, 48, 0.30)", + "--color-primary-alpha-700": "rgba(245, 171, 53, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(221, 154, 48, 0.20)", + "--color-primary-alpha-800": "rgba(245, 171, 53, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(221, 154, 48, 0.10)", + "--color-primary-alpha-900": "rgba(245, 171, 53, 0.10)", + "--color-primary-dark-200": "rgb(199,139,43)", + "--color-primary-dark-200-alpha-100": "rgba(199, 139, 43, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(199, 139, 43, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(199, 139, 43, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(199, 139, 43, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(199, 139, 43, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(199, 139, 43, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(199, 139, 43, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(199, 139, 43, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(199, 139, 43, 0.10)", + "--color-primary-dark-300": "rgb(179,125,39)", + "--color-primary-dark-300-alpha-100": "rgba(179, 125, 39, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(179, 125, 39, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(179, 125, 39, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(179, 125, 39, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(179, 125, 39, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(179, 125, 39, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(179, 125, 39, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(179, 125, 39, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(179, 125, 39, 0.10)", + "--color-primary-dark-400": "rgb(161,113,35)", + "--color-primary-dark-400-alpha-100": "rgba(161, 113, 35, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(161, 113, 35, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(161, 113, 35, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(161, 113, 35, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(161, 113, 35, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(161, 113, 35, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(161, 113, 35, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(161, 113, 35, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(161, 113, 35, 0.10)", + "--color-primary-dark-500": "rgb(145,102,32)", + "--color-primary-dark-500-alpha-100": "rgba(145, 102, 32, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(145, 102, 32, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(145, 102, 32, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(145, 102, 32, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(145, 102, 32, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(145, 102, 32, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(145, 102, 32, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(145, 102, 32, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(145, 102, 32, 0.10)", + "--color-primary-dark-600": "rgb(131,92,29)", + "--color-primary-dark-600-alpha-100": "rgba(131, 92, 29, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(131, 92, 29, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(131, 92, 29, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(131, 92, 29, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(131, 92, 29, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(131, 92, 29, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(131, 92, 29, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(131, 92, 29, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(131, 92, 29, 0.10)", + "--color-primary-dark-700": "rgb(118,83,26)", + "--color-primary-dark-700-alpha-100": "rgba(118, 83, 26, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(118, 83, 26, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(118, 83, 26, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(118, 83, 26, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(118, 83, 26, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(118, 83, 26, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(118, 83, 26, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(118, 83, 26, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(118, 83, 26, 0.10)", + "--color-primary-dark-800": "rgb(106,75,23)", + "--color-primary-dark-800-alpha-100": "rgba(106, 75, 23, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(106, 75, 23, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(106, 75, 23, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(106, 75, 23, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(106, 75, 23, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(106, 75, 23, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(106, 75, 23, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(106, 75, 23, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(106, 75, 23, 0.10)", + "--color-primary-dark-900": "rgb(95,68,21)", + "--color-primary-dark-900-alpha-100": "rgba(95, 68, 21, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(95, 68, 21, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(95, 68, 21, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(95, 68, 21, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(95, 68, 21, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(95, 68, 21, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(95, 68, 21, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(95, 68, 21, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(95, 68, 21, 0.10)", + "--color-primary-dark-1000": "rgb(86,61,19)", + "--color-primary-dark-1000-alpha-100": "rgba(86, 61, 19, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(86, 61, 19, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(86, 61, 19, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(86, 61, 19, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(86, 61, 19, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(86, 61, 19, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(86, 61, 19, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(86, 61, 19, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(86, 61, 19, 0.10)", + "--color-primary-light-100": "rgb(247,188,93)", + "--color-primary-light-100-alpha-100": "rgba(247, 188, 93, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(247, 188, 93, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(247, 188, 93, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(247, 188, 93, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(247, 188, 93, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(247, 188, 93, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(247, 188, 93, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(247, 188, 93, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(247, 188, 93, 0.10)", + "--color-primary-light-200": "rgb(249,201,125)", + "--color-primary-light-200-alpha-100": "rgba(249, 201, 125, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(249, 201, 125, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(249, 201, 125, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(249, 201, 125, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(249, 201, 125, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(249, 201, 125, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(249, 201, 125, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(249, 201, 125, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(249, 201, 125, 0.10)", + "--color-primary-light-300": "rgb(250,212,151)", + "--color-primary-light-300-alpha-100": "rgba(250, 212, 151, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(250, 212, 151, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(250, 212, 151, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(250, 212, 151, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(250, 212, 151, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(250, 212, 151, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(250, 212, 151, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(250, 212, 151, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(250, 212, 151, 0.10)", + "--color-primary-light-400": "rgb(251,221,172)", + "--color-primary-light-400-alpha-100": "rgba(251, 221, 172, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(251, 221, 172, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(251, 221, 172, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(251, 221, 172, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(251, 221, 172, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(251, 221, 172, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(251, 221, 172, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(251, 221, 172, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(251, 221, 172, 0.10)", + "--color-primary-light-500": "rgb(252,228,189)", + "--color-primary-light-500-alpha-100": "rgba(252, 228, 189, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(252, 228, 189, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(252, 228, 189, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(252, 228, 189, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(252, 228, 189, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(252, 228, 189, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(252, 228, 189, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(252, 228, 189, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(252, 228, 189, 0.10)", + "--color-primary-light-600": "rgb(253,233,202)", + "--color-primary-light-600-alpha-100": "rgba(253, 233, 202, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(253, 233, 202, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(253, 233, 202, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(253, 233, 202, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(253, 233, 202, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(253, 233, 202, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(253, 233, 202, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(253, 233, 202, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(253, 233, 202, 0.10)", + "--color-primary-light-700": "rgb(253,237,213)", + "--color-primary-light-700-alpha-100": "rgba(253, 237, 213, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(253, 237, 213, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(253, 237, 213, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(253, 237, 213, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(253, 237, 213, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(253, 237, 213, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(253, 237, 213, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(253, 237, 213, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(253, 237, 213, 0.10)", + "--color-primary-light-800": "rgb(253,241,221)", + "--color-primary-light-800-alpha-100": "rgba(253, 241, 221, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(253, 241, 221, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(253, 241, 221, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(253, 241, 221, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(253, 241, 221, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(253, 241, 221, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(253, 241, 221, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(253, 241, 221, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(253, 241, 221, 0.10)", + "--color-primary-light-900": "rgb(253,244,228)", + "--color-primary-light-900-alpha-100": "rgba(253, 244, 228, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(253, 244, 228, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(253, 244, 228, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(253, 244, 228, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(253, 244, 228, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(253, 244, 228, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(253, 244, 228, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(253, 244, 228, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(253, 244, 228, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(245, 171, 53)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(214, 69, 65)", + "--color-primary-dark-100": "rgb(193,62,59)", + "--color-primary-dark-100-alpha-100": "rgba(193, 62, 59, 0.90)", + "--color-primary-alpha-100": "rgba(214, 69, 65, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(193, 62, 59, 0.80)", + "--color-primary-alpha-200": "rgba(214, 69, 65, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(193, 62, 59, 0.70)", + "--color-primary-alpha-300": "rgba(214, 69, 65, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(193, 62, 59, 0.60)", + "--color-primary-alpha-400": "rgba(214, 69, 65, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(193, 62, 59, 0.50)", + "--color-primary-alpha-500": "rgba(214, 69, 65, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(193, 62, 59, 0.40)", + "--color-primary-alpha-600": "rgba(214, 69, 65, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(193, 62, 59, 0.30)", + "--color-primary-alpha-700": "rgba(214, 69, 65, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(193, 62, 59, 0.20)", + "--color-primary-alpha-800": "rgba(214, 69, 65, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(193, 62, 59, 0.10)", + "--color-primary-alpha-900": "rgba(214, 69, 65, 0.10)", + "--color-primary-dark-200": "rgb(174,56,53)", + "--color-primary-dark-200-alpha-100": "rgba(174, 56, 53, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(174, 56, 53, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(174, 56, 53, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(174, 56, 53, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(174, 56, 53, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(174, 56, 53, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(174, 56, 53, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(174, 56, 53, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(174, 56, 53, 0.10)", + "--color-primary-dark-300": "rgb(157,50,48)", + "--color-primary-dark-300-alpha-100": "rgba(157, 50, 48, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(157, 50, 48, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(157, 50, 48, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(157, 50, 48, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(157, 50, 48, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(157, 50, 48, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(157, 50, 48, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(157, 50, 48, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(157, 50, 48, 0.10)", + "--color-primary-dark-400": "rgb(141,45,43)", + "--color-primary-dark-400-alpha-100": "rgba(141, 45, 43, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(141, 45, 43, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(141, 45, 43, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(141, 45, 43, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(141, 45, 43, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(141, 45, 43, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(141, 45, 43, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(141, 45, 43, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(141, 45, 43, 0.10)", + "--color-primary-dark-500": "rgb(127,41,39)", + "--color-primary-dark-500-alpha-100": "rgba(127, 41, 39, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(127, 41, 39, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(127, 41, 39, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(127, 41, 39, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(127, 41, 39, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(127, 41, 39, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(127, 41, 39, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(127, 41, 39, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(127, 41, 39, 0.10)", + "--color-primary-dark-600": "rgb(114,37,35)", + "--color-primary-dark-600-alpha-100": "rgba(114, 37, 35, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(114, 37, 35, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(114, 37, 35, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(114, 37, 35, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(114, 37, 35, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(114, 37, 35, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(114, 37, 35, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(114, 37, 35, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(114, 37, 35, 0.10)", + "--color-primary-dark-700": "rgb(103,33,32)", + "--color-primary-dark-700-alpha-100": "rgba(103, 33, 32, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(103, 33, 32, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(103, 33, 32, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(103, 33, 32, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(103, 33, 32, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(103, 33, 32, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(103, 33, 32, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(103, 33, 32, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(103, 33, 32, 0.10)", + "--color-primary-dark-800": "rgb(93,30,29)", + "--color-primary-dark-800-alpha-100": "rgba(93, 30, 29, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(93, 30, 29, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(93, 30, 29, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(93, 30, 29, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(93, 30, 29, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(93, 30, 29, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(93, 30, 29, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(93, 30, 29, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(93, 30, 29, 0.10)", + "--color-primary-dark-900": "rgb(84,27,26)", + "--color-primary-dark-900-alpha-100": "rgba(84, 27, 26, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(84, 27, 26, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(84, 27, 26, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(84, 27, 26, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(84, 27, 26, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(84, 27, 26, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(84, 27, 26, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(84, 27, 26, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(84, 27, 26, 0.10)", + "--color-primary-dark-1000": "rgb(76,24,23)", + "--color-primary-dark-1000-alpha-100": "rgba(76, 24, 23, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(76, 24, 23, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(76, 24, 23, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(76, 24, 23, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(76, 24, 23, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(76, 24, 23, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(76, 24, 23, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(76, 24, 23, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(76, 24, 23, 0.10)", + "--color-primary-light-100": "rgb(222,106,103)", + "--color-primary-light-100-alpha-100": "rgba(222, 106, 103, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(222, 106, 103, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(222, 106, 103, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(222, 106, 103, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(222, 106, 103, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(222, 106, 103, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(222, 106, 103, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(222, 106, 103, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(222, 106, 103, 0.10)", + "--color-primary-light-200": "rgb(229,136,133)", + "--color-primary-light-200-alpha-100": "rgba(229, 136, 133, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(229, 136, 133, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(229, 136, 133, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(229, 136, 133, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(229, 136, 133, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(229, 136, 133, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(229, 136, 133, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(229, 136, 133, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(229, 136, 133, 0.10)", + "--color-primary-light-300": "rgb(234,160,157)", + "--color-primary-light-300-alpha-100": "rgba(234, 160, 157, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(234, 160, 157, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(234, 160, 157, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(234, 160, 157, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(234, 160, 157, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(234, 160, 157, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(234, 160, 157, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(234, 160, 157, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(234, 160, 157, 0.10)", + "--color-primary-light-400": "rgb(238,179,177)", + "--color-primary-light-400-alpha-100": "rgba(238, 179, 177, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(238, 179, 177, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(238, 179, 177, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(238, 179, 177, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(238, 179, 177, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(238, 179, 177, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(238, 179, 177, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(238, 179, 177, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(238, 179, 177, 0.10)", + "--color-primary-light-500": "rgb(241,194,193)", + "--color-primary-light-500-alpha-100": "rgba(241, 194, 193, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(241, 194, 193, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(241, 194, 193, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(241, 194, 193, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(241, 194, 193, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(241, 194, 193, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(241, 194, 193, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(241, 194, 193, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(241, 194, 193, 0.10)", + "--color-primary-light-600": "rgb(244,206,205)", + "--color-primary-light-600-alpha-100": "rgba(244, 206, 205, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(244, 206, 205, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(244, 206, 205, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(244, 206, 205, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(244, 206, 205, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(244, 206, 205, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(244, 206, 205, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(244, 206, 205, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(244, 206, 205, 0.10)", + "--color-primary-light-700": "rgb(246,216,215)", + "--color-primary-light-700-alpha-100": "rgba(246, 216, 215, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(246, 216, 215, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(246, 216, 215, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(246, 216, 215, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(246, 216, 215, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(246, 216, 215, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(246, 216, 215, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(246, 216, 215, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(246, 216, 215, 0.10)", + "--color-primary-light-800": "rgb(248,224,223)", + "--color-primary-light-800-alpha-100": "rgba(248, 224, 223, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(248, 224, 223, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(248, 224, 223, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(248, 224, 223, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(248, 224, 223, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(248, 224, 223, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(248, 224, 223, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(248, 224, 223, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(248, 224, 223, 0.10)", + "--color-primary-light-900": "rgb(249,230,229)", + "--color-primary-light-900-alpha-100": "rgba(249, 230, 229, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(249, 230, 229, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(249, 230, 229, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(249, 230, 229, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(249, 230, 229, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(249, 230, 229, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(249, 230, 229, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(249, 230, 229, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(249, 230, 229, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(214, 69, 65)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(241, 130, 141)", + "--color-primary-dark-100": "rgb(217,117,127)", + "--color-primary-dark-100-alpha-100": "rgba(217, 117, 127, 0.90)", + "--color-primary-alpha-100": "rgba(241, 130, 141, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(217, 117, 127, 0.80)", + "--color-primary-alpha-200": "rgba(241, 130, 141, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(217, 117, 127, 0.70)", + "--color-primary-alpha-300": "rgba(241, 130, 141, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(217, 117, 127, 0.60)", + "--color-primary-alpha-400": "rgba(241, 130, 141, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(217, 117, 127, 0.50)", + "--color-primary-alpha-500": "rgba(241, 130, 141, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(217, 117, 127, 0.40)", + "--color-primary-alpha-600": "rgba(241, 130, 141, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(217, 117, 127, 0.30)", + "--color-primary-alpha-700": "rgba(241, 130, 141, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(217, 117, 127, 0.20)", + "--color-primary-alpha-800": "rgba(241, 130, 141, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(217, 117, 127, 0.10)", + "--color-primary-alpha-900": "rgba(241, 130, 141, 0.10)", + "--color-primary-dark-200": "rgb(195,105,114)", + "--color-primary-dark-200-alpha-100": "rgba(195, 105, 114, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(195, 105, 114, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(195, 105, 114, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(195, 105, 114, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(195, 105, 114, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(195, 105, 114, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(195, 105, 114, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(195, 105, 114, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(195, 105, 114, 0.10)", + "--color-primary-dark-300": "rgb(176,95,103)", + "--color-primary-dark-300-alpha-100": "rgba(176, 95, 103, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(176, 95, 103, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(176, 95, 103, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(176, 95, 103, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(176, 95, 103, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(176, 95, 103, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(176, 95, 103, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(176, 95, 103, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(176, 95, 103, 0.10)", + "--color-primary-dark-400": "rgb(158,86,93)", + "--color-primary-dark-400-alpha-100": "rgba(158, 86, 93, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(158, 86, 93, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(158, 86, 93, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(158, 86, 93, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(158, 86, 93, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(158, 86, 93, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(158, 86, 93, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(158, 86, 93, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(158, 86, 93, 0.10)", + "--color-primary-dark-500": "rgb(142,77,84)", + "--color-primary-dark-500-alpha-100": "rgba(142, 77, 84, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(142, 77, 84, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(142, 77, 84, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(142, 77, 84, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(142, 77, 84, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(142, 77, 84, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(142, 77, 84, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(142, 77, 84, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(142, 77, 84, 0.10)", + "--color-primary-dark-600": "rgb(128,69,76)", + "--color-primary-dark-600-alpha-100": "rgba(128, 69, 76, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(128, 69, 76, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(128, 69, 76, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(128, 69, 76, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(128, 69, 76, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(128, 69, 76, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(128, 69, 76, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(128, 69, 76, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(128, 69, 76, 0.10)", + "--color-primary-dark-700": "rgb(115,62,68)", + "--color-primary-dark-700-alpha-100": "rgba(115, 62, 68, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(115, 62, 68, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(115, 62, 68, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(115, 62, 68, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(115, 62, 68, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(115, 62, 68, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(115, 62, 68, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(115, 62, 68, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(115, 62, 68, 0.10)", + "--color-primary-dark-800": "rgb(104,56,61)", + "--color-primary-dark-800-alpha-100": "rgba(104, 56, 61, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(104, 56, 61, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(104, 56, 61, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(104, 56, 61, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(104, 56, 61, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(104, 56, 61, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(104, 56, 61, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(104, 56, 61, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(104, 56, 61, 0.10)", + "--color-primary-dark-900": "rgb(94,50,55)", + "--color-primary-dark-900-alpha-100": "rgba(94, 50, 55, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(94, 50, 55, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(94, 50, 55, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(94, 50, 55, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(94, 50, 55, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(94, 50, 55, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(94, 50, 55, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(94, 50, 55, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(94, 50, 55, 0.10)", + "--color-primary-dark-1000": "rgb(85,45,50)", + "--color-primary-dark-1000-alpha-100": "rgba(85, 45, 50, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(85, 45, 50, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(85, 45, 50, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(85, 45, 50, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(85, 45, 50, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(85, 45, 50, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(85, 45, 50, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(85, 45, 50, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(85, 45, 50, 0.10)", + "--color-primary-light-100": "rgb(244,155,164)", + "--color-primary-light-100-alpha-100": "rgba(244, 155, 164, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(244, 155, 164, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(244, 155, 164, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(244, 155, 164, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(244, 155, 164, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(244, 155, 164, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(244, 155, 164, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(244, 155, 164, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(244, 155, 164, 0.10)", + "--color-primary-light-200": "rgb(246,175,182)", + "--color-primary-light-200-alpha-100": "rgba(246, 175, 182, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(246, 175, 182, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(246, 175, 182, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(246, 175, 182, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(246, 175, 182, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(246, 175, 182, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(246, 175, 182, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(246, 175, 182, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(246, 175, 182, 0.10)", + "--color-primary-light-300": "rgb(248,191,197)", + "--color-primary-light-300-alpha-100": "rgba(248, 191, 197, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(248, 191, 197, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(248, 191, 197, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(248, 191, 197, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(248, 191, 197, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(248, 191, 197, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(248, 191, 197, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(248, 191, 197, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(248, 191, 197, 0.10)", + "--color-primary-light-400": "rgb(249,204,209)", + "--color-primary-light-400-alpha-100": "rgba(249, 204, 209, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(249, 204, 209, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(249, 204, 209, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(249, 204, 209, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(249, 204, 209, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(249, 204, 209, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(249, 204, 209, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(249, 204, 209, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(249, 204, 209, 0.10)", + "--color-primary-light-500": "rgb(250,214,218)", + "--color-primary-light-500-alpha-100": "rgba(250, 214, 218, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(250, 214, 218, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(250, 214, 218, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(250, 214, 218, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(250, 214, 218, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(250, 214, 218, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(250, 214, 218, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(250, 214, 218, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(250, 214, 218, 0.10)", + "--color-primary-light-600": "rgb(251,222,225)", + "--color-primary-light-600-alpha-100": "rgba(251, 222, 225, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(251, 222, 225, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(251, 222, 225, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(251, 222, 225, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(251, 222, 225, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(251, 222, 225, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(251, 222, 225, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(251, 222, 225, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(251, 222, 225, 0.10)", + "--color-primary-light-700": "rgb(252,229,231)", + "--color-primary-light-700-alpha-100": "rgba(252, 229, 231, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(252, 229, 231, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(252, 229, 231, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(252, 229, 231, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(252, 229, 231, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(252, 229, 231, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(252, 229, 231, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(252, 229, 231, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(252, 229, 231, 0.10)", + "--color-primary-light-800": "rgb(253,234,236)", + "--color-primary-light-800-alpha-100": "rgba(253, 234, 236, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(253, 234, 236, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(253, 234, 236, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(253, 234, 236, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(253, 234, 236, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(253, 234, 236, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(253, 234, 236, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(253, 234, 236, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(253, 234, 236, 0.10)", + "--color-primary-light-900": "rgb(253,238,240)", + "--color-primary-light-900-alpha-100": "rgba(253, 238, 240, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(253, 238, 240, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(253, 238, 240, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(253, 238, 240, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(253, 238, 240, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(253, 238, 240, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(253, 238, 240, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(253, 238, 240, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(253, 238, 240, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(241, 130, 141)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(155, 89, 182)", + "--color-primary-dark-100": "rgb(140,80,164)", + "--color-primary-dark-100-alpha-100": "rgba(140, 80, 164, 0.90)", + "--color-primary-alpha-100": "rgba(155, 89, 182, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(140, 80, 164, 0.80)", + "--color-primary-alpha-200": "rgba(155, 89, 182, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(140, 80, 164, 0.70)", + "--color-primary-alpha-300": "rgba(155, 89, 182, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(140, 80, 164, 0.60)", + "--color-primary-alpha-400": "rgba(155, 89, 182, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(140, 80, 164, 0.50)", + "--color-primary-alpha-500": "rgba(155, 89, 182, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(140, 80, 164, 0.40)", + "--color-primary-alpha-600": "rgba(155, 89, 182, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(140, 80, 164, 0.30)", + "--color-primary-alpha-700": "rgba(155, 89, 182, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(140, 80, 164, 0.20)", + "--color-primary-alpha-800": "rgba(155, 89, 182, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(140, 80, 164, 0.10)", + "--color-primary-alpha-900": "rgba(155, 89, 182, 0.10)", + "--color-primary-dark-200": "rgb(126,72,148)", + "--color-primary-dark-200-alpha-100": "rgba(126, 72, 148, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(126, 72, 148, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(126, 72, 148, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(126, 72, 148, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(126, 72, 148, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(126, 72, 148, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(126, 72, 148, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(126, 72, 148, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(126, 72, 148, 0.10)", + "--color-primary-dark-300": "rgb(113,65,133)", + "--color-primary-dark-300-alpha-100": "rgba(113, 65, 133, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(113, 65, 133, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(113, 65, 133, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(113, 65, 133, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(113, 65, 133, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(113, 65, 133, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(113, 65, 133, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(113, 65, 133, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(113, 65, 133, 0.10)", + "--color-primary-dark-400": "rgb(102,59,120)", + "--color-primary-dark-400-alpha-100": "rgba(102, 59, 120, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(102, 59, 120, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(102, 59, 120, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(102, 59, 120, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(102, 59, 120, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(102, 59, 120, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(102, 59, 120, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(102, 59, 120, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(102, 59, 120, 0.10)", + "--color-primary-dark-500": "rgb(92,53,108)", + "--color-primary-dark-500-alpha-100": "rgba(92, 53, 108, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(92, 53, 108, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(92, 53, 108, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(92, 53, 108, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(92, 53, 108, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(92, 53, 108, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(92, 53, 108, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(92, 53, 108, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(92, 53, 108, 0.10)", + "--color-primary-dark-600": "rgb(83,48,97)", + "--color-primary-dark-600-alpha-100": "rgba(83, 48, 97, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(83, 48, 97, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(83, 48, 97, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(83, 48, 97, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(83, 48, 97, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(83, 48, 97, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(83, 48, 97, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(83, 48, 97, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(83, 48, 97, 0.10)", + "--color-primary-dark-700": "rgb(75,43,87)", + "--color-primary-dark-700-alpha-100": "rgba(75, 43, 87, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(75, 43, 87, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(75, 43, 87, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(75, 43, 87, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(75, 43, 87, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(75, 43, 87, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(75, 43, 87, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(75, 43, 87, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(75, 43, 87, 0.10)", + "--color-primary-dark-800": "rgb(68,39,78)", + "--color-primary-dark-800-alpha-100": "rgba(68, 39, 78, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(68, 39, 78, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(68, 39, 78, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(68, 39, 78, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(68, 39, 78, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(68, 39, 78, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(68, 39, 78, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(68, 39, 78, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(68, 39, 78, 0.10)", + "--color-primary-dark-900": "rgb(61,35,70)", + "--color-primary-dark-900-alpha-100": "rgba(61, 35, 70, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(61, 35, 70, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(61, 35, 70, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(61, 35, 70, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(61, 35, 70, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(61, 35, 70, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(61, 35, 70, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(61, 35, 70, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(61, 35, 70, 0.10)", + "--color-primary-dark-1000": "rgb(55,32,63)", + "--color-primary-dark-1000-alpha-100": "rgba(55, 32, 63, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(55, 32, 63, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(55, 32, 63, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(55, 32, 63, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(55, 32, 63, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(55, 32, 63, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(55, 32, 63, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(55, 32, 63, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(55, 32, 63, 0.10)", + "--color-primary-light-100": "rgb(175,122,197)", + "--color-primary-light-100-alpha-100": "rgba(175, 122, 197, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(175, 122, 197, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(175, 122, 197, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(175, 122, 197, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(175, 122, 197, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(175, 122, 197, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(175, 122, 197, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(175, 122, 197, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(175, 122, 197, 0.10)", + "--color-primary-light-200": "rgb(191,149,209)", + "--color-primary-light-200-alpha-100": "rgba(191, 149, 209, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(191, 149, 209, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(191, 149, 209, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(191, 149, 209, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(191, 149, 209, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(191, 149, 209, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(191, 149, 209, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(191, 149, 209, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(191, 149, 209, 0.10)", + "--color-primary-light-300": "rgb(204,170,218)", + "--color-primary-light-300-alpha-100": "rgba(204, 170, 218, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(204, 170, 218, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(204, 170, 218, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(204, 170, 218, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(204, 170, 218, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(204, 170, 218, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(204, 170, 218, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(204, 170, 218, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(204, 170, 218, 0.10)", + "--color-primary-light-400": "rgb(214,187,225)", + "--color-primary-light-400-alpha-100": "rgba(214, 187, 225, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(214, 187, 225, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(214, 187, 225, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(214, 187, 225, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(214, 187, 225, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(214, 187, 225, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(214, 187, 225, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(214, 187, 225, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(214, 187, 225, 0.10)", + "--color-primary-light-500": "rgb(222,201,231)", + "--color-primary-light-500-alpha-100": "rgba(222, 201, 231, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(222, 201, 231, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(222, 201, 231, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(222, 201, 231, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(222, 201, 231, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(222, 201, 231, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(222, 201, 231, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(222, 201, 231, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(222, 201, 231, 0.10)", + "--color-primary-light-600": "rgb(229,212,236)", + "--color-primary-light-600-alpha-100": "rgba(229, 212, 236, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(229, 212, 236, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(229, 212, 236, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(229, 212, 236, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(229, 212, 236, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(229, 212, 236, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(229, 212, 236, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(229, 212, 236, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(229, 212, 236, 0.10)", + "--color-primary-light-700": "rgb(234,221,240)", + "--color-primary-light-700-alpha-100": "rgba(234, 221, 240, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(234, 221, 240, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(234, 221, 240, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(234, 221, 240, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(234, 221, 240, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(234, 221, 240, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(234, 221, 240, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(234, 221, 240, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(234, 221, 240, 0.10)", + "--color-primary-light-800": "rgb(238,228,243)", + "--color-primary-light-800-alpha-100": "rgba(238, 228, 243, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(238, 228, 243, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(238, 228, 243, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(238, 228, 243, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(238, 228, 243, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(238, 228, 243, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(238, 228, 243, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(238, 228, 243, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(238, 228, 243, 0.10)", + "--color-primary-light-900": "rgb(241,233,245)", + "--color-primary-light-900-alpha-100": "rgba(241, 233, 245, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(241, 233, 245, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(241, 233, 245, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(241, 233, 245, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(241, 233, 245, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(241, 233, 245, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(241, 233, 245, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(241, 233, 245, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(241, 233, 245, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(155, 89, 182)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(108, 122, 137)", + "--color-primary-dark-100": "rgb(97,110,123)", + "--color-primary-dark-100-alpha-100": "rgba(97, 110, 123, 0.90)", + "--color-primary-alpha-100": "rgba(108, 122, 137, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(97, 110, 123, 0.80)", + "--color-primary-alpha-200": "rgba(108, 122, 137, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(97, 110, 123, 0.70)", + "--color-primary-alpha-300": "rgba(108, 122, 137, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(97, 110, 123, 0.60)", + "--color-primary-alpha-400": "rgba(108, 122, 137, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(97, 110, 123, 0.50)", + "--color-primary-alpha-500": "rgba(108, 122, 137, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(97, 110, 123, 0.40)", + "--color-primary-alpha-600": "rgba(108, 122, 137, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(97, 110, 123, 0.30)", + "--color-primary-alpha-700": "rgba(108, 122, 137, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(97, 110, 123, 0.20)", + "--color-primary-alpha-800": "rgba(108, 122, 137, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(97, 110, 123, 0.10)", + "--color-primary-alpha-900": "rgba(108, 122, 137, 0.10)", + "--color-primary-dark-200": "rgb(87,99,111)", + "--color-primary-dark-200-alpha-100": "rgba(87, 99, 111, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(87, 99, 111, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(87, 99, 111, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(87, 99, 111, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(87, 99, 111, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(87, 99, 111, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(87, 99, 111, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(87, 99, 111, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(87, 99, 111, 0.10)", + "--color-primary-dark-300": "rgb(78,89,100)", + "--color-primary-dark-300-alpha-100": "rgba(78, 89, 100, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(78, 89, 100, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(78, 89, 100, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(78, 89, 100, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(78, 89, 100, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(78, 89, 100, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(78, 89, 100, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(78, 89, 100, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(78, 89, 100, 0.10)", + "--color-primary-dark-400": "rgb(70,80,90)", + "--color-primary-dark-400-alpha-100": "rgba(70, 80, 90, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(70, 80, 90, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(70, 80, 90, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(70, 80, 90, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(70, 80, 90, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(70, 80, 90, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(70, 80, 90, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(70, 80, 90, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(70, 80, 90, 0.10)", + "--color-primary-dark-500": "rgb(63,72,81)", + "--color-primary-dark-500-alpha-100": "rgba(63, 72, 81, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(63, 72, 81, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(63, 72, 81, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(63, 72, 81, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(63, 72, 81, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(63, 72, 81, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(63, 72, 81, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(63, 72, 81, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(63, 72, 81, 0.10)", + "--color-primary-dark-600": "rgb(57,65,73)", + "--color-primary-dark-600-alpha-100": "rgba(57, 65, 73, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(57, 65, 73, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(57, 65, 73, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(57, 65, 73, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(57, 65, 73, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(57, 65, 73, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(57, 65, 73, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(57, 65, 73, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(57, 65, 73, 0.10)", + "--color-primary-dark-700": "rgb(51,59,66)", + "--color-primary-dark-700-alpha-100": "rgba(51, 59, 66, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(51, 59, 66, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(51, 59, 66, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(51, 59, 66, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(51, 59, 66, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(51, 59, 66, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(51, 59, 66, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(51, 59, 66, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(51, 59, 66, 0.10)", + "--color-primary-dark-800": "rgb(46,53,59)", + "--color-primary-dark-800-alpha-100": "rgba(46, 53, 59, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(46, 53, 59, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(46, 53, 59, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(46, 53, 59, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(46, 53, 59, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(46, 53, 59, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(46, 53, 59, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(46, 53, 59, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(46, 53, 59, 0.10)", + "--color-primary-dark-900": "rgb(41,48,53)", + "--color-primary-dark-900-alpha-100": "rgba(41, 48, 53, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(41, 48, 53, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(41, 48, 53, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(41, 48, 53, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(41, 48, 53, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(41, 48, 53, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(41, 48, 53, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(41, 48, 53, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(41, 48, 53, 0.10)", + "--color-primary-dark-1000": "rgb(37,43,48)", + "--color-primary-dark-1000-alpha-100": "rgba(37, 43, 48, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(37, 43, 48, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(37, 43, 48, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(37, 43, 48, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(37, 43, 48, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(37, 43, 48, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(37, 43, 48, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(37, 43, 48, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(37, 43, 48, 0.10)", + "--color-primary-light-100": "rgb(137,149,161)", + "--color-primary-light-100-alpha-100": "rgba(137, 149, 161, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(137, 149, 161, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(137, 149, 161, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(137, 149, 161, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(137, 149, 161, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(137, 149, 161, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(137, 149, 161, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(137, 149, 161, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(137, 149, 161, 0.10)", + "--color-primary-light-200": "rgb(161,170,180)", + "--color-primary-light-200-alpha-100": "rgba(161, 170, 180, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(161, 170, 180, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(161, 170, 180, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(161, 170, 180, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(161, 170, 180, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(161, 170, 180, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(161, 170, 180, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(161, 170, 180, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(161, 170, 180, 0.10)", + "--color-primary-light-300": "rgb(180,187,195)", + "--color-primary-light-300-alpha-100": "rgba(180, 187, 195, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(180, 187, 195, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(180, 187, 195, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(180, 187, 195, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(180, 187, 195, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(180, 187, 195, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(180, 187, 195, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(180, 187, 195, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(180, 187, 195, 0.10)", + "--color-primary-light-400": "rgb(195,201,207)", + "--color-primary-light-400-alpha-100": "rgba(195, 201, 207, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(195, 201, 207, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(195, 201, 207, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(195, 201, 207, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(195, 201, 207, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(195, 201, 207, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(195, 201, 207, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(195, 201, 207, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(195, 201, 207, 0.10)", + "--color-primary-light-500": "rgb(207,212,217)", + "--color-primary-light-500-alpha-100": "rgba(207, 212, 217, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(207, 212, 217, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(207, 212, 217, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(207, 212, 217, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(207, 212, 217, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(207, 212, 217, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(207, 212, 217, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(207, 212, 217, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(207, 212, 217, 0.10)", + "--color-primary-light-600": "rgb(217,221,225)", + "--color-primary-light-600-alpha-100": "rgba(217, 221, 225, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(217, 221, 225, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(217, 221, 225, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(217, 221, 225, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(217, 221, 225, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(217, 221, 225, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(217, 221, 225, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(217, 221, 225, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(217, 221, 225, 0.10)", + "--color-primary-light-700": "rgb(225,228,231)", + "--color-primary-light-700-alpha-100": "rgba(225, 228, 231, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(225, 228, 231, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(225, 228, 231, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(225, 228, 231, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(225, 228, 231, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(225, 228, 231, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(225, 228, 231, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(225, 228, 231, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(225, 228, 231, 0.10)", + "--color-primary-light-800": "rgb(231,233,236)", + "--color-primary-light-800-alpha-100": "rgba(231, 233, 236, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(231, 233, 236, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(231, 233, 236, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(231, 233, 236, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(231, 233, 236, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(231, 233, 236, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(231, 233, 236, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(231, 233, 236, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(231, 233, 236, 0.10)", + "--color-primary-light-900": "rgb(236,237,240)", + "--color-primary-light-900-alpha-100": "rgba(236, 237, 240, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(236, 237, 240, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(236, 237, 240, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(236, 237, 240, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(236, 237, 240, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(236, 237, 240, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(236, 237, 240, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(236, 237, 240, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(236, 237, 240, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(108, 122, 137)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(51, 110, 123)", + "--color-primary-dark-100": "rgb(46,99,111)", + "--color-primary-dark-100-alpha-100": "rgba(46, 99, 111, 0.90)", + "--color-primary-alpha-100": "rgba(51, 110, 123, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(46, 99, 111, 0.80)", + "--color-primary-alpha-200": "rgba(51, 110, 123, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(46, 99, 111, 0.70)", + "--color-primary-alpha-300": "rgba(51, 110, 123, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(46, 99, 111, 0.60)", + "--color-primary-alpha-400": "rgba(51, 110, 123, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(46, 99, 111, 0.50)", + "--color-primary-alpha-500": "rgba(51, 110, 123, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(46, 99, 111, 0.40)", + "--color-primary-alpha-600": "rgba(51, 110, 123, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(46, 99, 111, 0.30)", + "--color-primary-alpha-700": "rgba(51, 110, 123, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(46, 99, 111, 0.20)", + "--color-primary-alpha-800": "rgba(51, 110, 123, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(46, 99, 111, 0.10)", + "--color-primary-alpha-900": "rgba(51, 110, 123, 0.10)", + "--color-primary-dark-200": "rgb(41,89,100)", + "--color-primary-dark-200-alpha-100": "rgba(41, 89, 100, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(41, 89, 100, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(41, 89, 100, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(41, 89, 100, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(41, 89, 100, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(41, 89, 100, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(41, 89, 100, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(41, 89, 100, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(41, 89, 100, 0.10)", + "--color-primary-dark-300": "rgb(37,80,90)", + "--color-primary-dark-300-alpha-100": "rgba(37, 80, 90, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(37, 80, 90, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(37, 80, 90, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(37, 80, 90, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(37, 80, 90, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(37, 80, 90, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(37, 80, 90, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(37, 80, 90, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(37, 80, 90, 0.10)", + "--color-primary-dark-400": "rgb(33,72,81)", + "--color-primary-dark-400-alpha-100": "rgba(33, 72, 81, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(33, 72, 81, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(33, 72, 81, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(33, 72, 81, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(33, 72, 81, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(33, 72, 81, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(33, 72, 81, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(33, 72, 81, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(33, 72, 81, 0.10)", + "--color-primary-dark-500": "rgb(30,65,73)", + "--color-primary-dark-500-alpha-100": "rgba(30, 65, 73, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(30, 65, 73, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(30, 65, 73, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(30, 65, 73, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(30, 65, 73, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(30, 65, 73, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(30, 65, 73, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(30, 65, 73, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(30, 65, 73, 0.10)", + "--color-primary-dark-600": "rgb(27,59,66)", + "--color-primary-dark-600-alpha-100": "rgba(27, 59, 66, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(27, 59, 66, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(27, 59, 66, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(27, 59, 66, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(27, 59, 66, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(27, 59, 66, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(27, 59, 66, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(27, 59, 66, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(27, 59, 66, 0.10)", + "--color-primary-dark-700": "rgb(24,53,59)", + "--color-primary-dark-700-alpha-100": "rgba(24, 53, 59, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(24, 53, 59, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(24, 53, 59, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(24, 53, 59, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(24, 53, 59, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(24, 53, 59, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(24, 53, 59, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(24, 53, 59, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(24, 53, 59, 0.10)", + "--color-primary-dark-800": "rgb(22,48,53)", + "--color-primary-dark-800-alpha-100": "rgba(22, 48, 53, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(22, 48, 53, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(22, 48, 53, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(22, 48, 53, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(22, 48, 53, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(22, 48, 53, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(22, 48, 53, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(22, 48, 53, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(22, 48, 53, 0.10)", + "--color-primary-dark-900": "rgb(20,43,48)", + "--color-primary-dark-900-alpha-100": "rgba(20, 43, 48, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(20, 43, 48, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(20, 43, 48, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(20, 43, 48, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(20, 43, 48, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(20, 43, 48, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(20, 43, 48, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(20, 43, 48, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(20, 43, 48, 0.10)", + "--color-primary-dark-1000": "rgb(18,39,43)", + "--color-primary-dark-1000-alpha-100": "rgba(18, 39, 43, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(18, 39, 43, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(18, 39, 43, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(18, 39, 43, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(18, 39, 43, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(18, 39, 43, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(18, 39, 43, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(18, 39, 43, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(18, 39, 43, 0.10)", + "--color-primary-light-100": "rgb(92,139,149)", + "--color-primary-light-100-alpha-100": "rgba(92, 139, 149, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(92, 139, 149, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(92, 139, 149, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(92, 139, 149, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(92, 139, 149, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(92, 139, 149, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(92, 139, 149, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(92, 139, 149, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(92, 139, 149, 0.10)", + "--color-primary-light-200": "rgb(125,162,170)", + "--color-primary-light-200-alpha-100": "rgba(125, 162, 170, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(125, 162, 170, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(125, 162, 170, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(125, 162, 170, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(125, 162, 170, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(125, 162, 170, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(125, 162, 170, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(125, 162, 170, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(125, 162, 170, 0.10)", + "--color-primary-light-300": "rgb(151,181,187)", + "--color-primary-light-300-alpha-100": "rgba(151, 181, 187, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(151, 181, 187, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(151, 181, 187, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(151, 181, 187, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(151, 181, 187, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(151, 181, 187, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(151, 181, 187, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(151, 181, 187, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(151, 181, 187, 0.10)", + "--color-primary-light-400": "rgb(172,196,201)", + "--color-primary-light-400-alpha-100": "rgba(172, 196, 201, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(172, 196, 201, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(172, 196, 201, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(172, 196, 201, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(172, 196, 201, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(172, 196, 201, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(172, 196, 201, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(172, 196, 201, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(172, 196, 201, 0.10)", + "--color-primary-light-500": "rgb(189,208,212)", + "--color-primary-light-500-alpha-100": "rgba(189, 208, 212, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(189, 208, 212, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(189, 208, 212, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(189, 208, 212, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(189, 208, 212, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(189, 208, 212, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(189, 208, 212, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(189, 208, 212, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(189, 208, 212, 0.10)", + "--color-primary-light-600": "rgb(202,217,221)", + "--color-primary-light-600-alpha-100": "rgba(202, 217, 221, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(202, 217, 221, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(202, 217, 221, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(202, 217, 221, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(202, 217, 221, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(202, 217, 221, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(202, 217, 221, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(202, 217, 221, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(202, 217, 221, 0.10)", + "--color-primary-light-700": "rgb(213,225,228)", + "--color-primary-light-700-alpha-100": "rgba(213, 225, 228, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(213, 225, 228, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(213, 225, 228, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(213, 225, 228, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(213, 225, 228, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(213, 225, 228, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(213, 225, 228, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(213, 225, 228, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(213, 225, 228, 0.10)", + "--color-primary-light-800": "rgb(221,231,233)", + "--color-primary-light-800-alpha-100": "rgba(221, 231, 233, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(221, 231, 233, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(221, 231, 233, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(221, 231, 233, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(221, 231, 233, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(221, 231, 233, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(221, 231, 233, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(221, 231, 233, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(221, 231, 233, 0.10)", + "--color-primary-light-900": "rgb(228,236,237)", + "--color-primary-light-900-alpha-100": "rgba(228, 236, 237, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(228, 236, 237, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(228, 236, 237, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(228, 236, 237, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(228, 236, 237, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(228, 236, 237, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(228, 236, 237, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(228, 236, 237, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(228, 236, 237, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(51, 110, 123)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(79, 98, 208)", + "--color-primary-dark-100": "rgb(71,88,187)", + "--color-primary-dark-100-alpha-100": "rgba(71, 88, 187, 0.90)", + "--color-primary-alpha-100": "rgba(79, 98, 208, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(71, 88, 187, 0.80)", + "--color-primary-alpha-200": "rgba(79, 98, 208, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(71, 88, 187, 0.70)", + "--color-primary-alpha-300": "rgba(79, 98, 208, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(71, 88, 187, 0.60)", + "--color-primary-alpha-400": "rgba(79, 98, 208, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(71, 88, 187, 0.50)", + "--color-primary-alpha-500": "rgba(79, 98, 208, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(71, 88, 187, 0.40)", + "--color-primary-alpha-600": "rgba(79, 98, 208, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(71, 88, 187, 0.30)", + "--color-primary-alpha-700": "rgba(79, 98, 208, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(71, 88, 187, 0.20)", + "--color-primary-alpha-800": "rgba(79, 98, 208, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(71, 88, 187, 0.10)", + "--color-primary-alpha-900": "rgba(79, 98, 208, 0.10)", + "--color-primary-dark-200": "rgb(64,79,168)", + "--color-primary-dark-200-alpha-100": "rgba(64, 79, 168, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(64, 79, 168, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(64, 79, 168, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(64, 79, 168, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(64, 79, 168, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(64, 79, 168, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(64, 79, 168, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(64, 79, 168, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(64, 79, 168, 0.10)", + "--color-primary-dark-300": "rgb(58,71,151)", + "--color-primary-dark-300-alpha-100": "rgba(58, 71, 151, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(58, 71, 151, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(58, 71, 151, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(58, 71, 151, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(58, 71, 151, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(58, 71, 151, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(58, 71, 151, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(58, 71, 151, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(58, 71, 151, 0.10)", + "--color-primary-dark-400": "rgb(52,64,136)", + "--color-primary-dark-400-alpha-100": "rgba(52, 64, 136, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(52, 64, 136, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(52, 64, 136, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(52, 64, 136, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(52, 64, 136, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(52, 64, 136, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(52, 64, 136, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(52, 64, 136, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(52, 64, 136, 0.10)", + "--color-primary-dark-500": "rgb(47,58,122)", + "--color-primary-dark-500-alpha-100": "rgba(47, 58, 122, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(47, 58, 122, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(47, 58, 122, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(47, 58, 122, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(47, 58, 122, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(47, 58, 122, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(47, 58, 122, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(47, 58, 122, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(47, 58, 122, 0.10)", + "--color-primary-dark-600": "rgb(42,52,110)", + "--color-primary-dark-600-alpha-100": "rgba(42, 52, 110, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(42, 52, 110, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(42, 52, 110, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(42, 52, 110, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(42, 52, 110, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(42, 52, 110, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(42, 52, 110, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(42, 52, 110, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(42, 52, 110, 0.10)", + "--color-primary-dark-700": "rgb(38,47,99)", + "--color-primary-dark-700-alpha-100": "rgba(38, 47, 99, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(38, 47, 99, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(38, 47, 99, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(38, 47, 99, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(38, 47, 99, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(38, 47, 99, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(38, 47, 99, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(38, 47, 99, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(38, 47, 99, 0.10)", + "--color-primary-dark-800": "rgb(34,42,89)", + "--color-primary-dark-800-alpha-100": "rgba(34, 42, 89, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(34, 42, 89, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(34, 42, 89, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(34, 42, 89, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(34, 42, 89, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(34, 42, 89, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(34, 42, 89, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(34, 42, 89, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(34, 42, 89, 0.10)", + "--color-primary-dark-900": "rgb(31,38,80)", + "--color-primary-dark-900-alpha-100": "rgba(31, 38, 80, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(31, 38, 80, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(31, 38, 80, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(31, 38, 80, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(31, 38, 80, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(31, 38, 80, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(31, 38, 80, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(31, 38, 80, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(31, 38, 80, 0.10)", + "--color-primary-dark-1000": "rgb(28,34,72)", + "--color-primary-dark-1000-alpha-100": "rgba(28, 34, 72, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(28, 34, 72, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(28, 34, 72, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(28, 34, 72, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(28, 34, 72, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(28, 34, 72, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(28, 34, 72, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(28, 34, 72, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(28, 34, 72, 0.10)", + "--color-primary-light-100": "rgb(114,129,217)", + "--color-primary-light-100-alpha-100": "rgba(114, 129, 217, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(114, 129, 217, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(114, 129, 217, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(114, 129, 217, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(114, 129, 217, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(114, 129, 217, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(114, 129, 217, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(114, 129, 217, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(114, 129, 217, 0.10)", + "--color-primary-light-200": "rgb(142,154,225)", + "--color-primary-light-200-alpha-100": "rgba(142, 154, 225, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(142, 154, 225, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(142, 154, 225, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(142, 154, 225, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(142, 154, 225, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(142, 154, 225, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(142, 154, 225, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(142, 154, 225, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(142, 154, 225, 0.10)", + "--color-primary-light-300": "rgb(165,174,231)", + "--color-primary-light-300-alpha-100": "rgba(165, 174, 231, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(165, 174, 231, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(165, 174, 231, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(165, 174, 231, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(165, 174, 231, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(165, 174, 231, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(165, 174, 231, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(165, 174, 231, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(165, 174, 231, 0.10)", + "--color-primary-light-400": "rgb(183,190,236)", + "--color-primary-light-400-alpha-100": "rgba(183, 190, 236, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(183, 190, 236, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(183, 190, 236, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(183, 190, 236, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(183, 190, 236, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(183, 190, 236, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(183, 190, 236, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(183, 190, 236, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(183, 190, 236, 0.10)", + "--color-primary-light-500": "rgb(197,203,240)", + "--color-primary-light-500-alpha-100": "rgba(197, 203, 240, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(197, 203, 240, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(197, 203, 240, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(197, 203, 240, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(197, 203, 240, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(197, 203, 240, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(197, 203, 240, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(197, 203, 240, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(197, 203, 240, 0.10)", + "--color-primary-light-600": "rgb(209,213,243)", + "--color-primary-light-600-alpha-100": "rgba(209, 213, 243, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(209, 213, 243, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(209, 213, 243, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(209, 213, 243, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(209, 213, 243, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(209, 213, 243, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(209, 213, 243, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(209, 213, 243, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(209, 213, 243, 0.10)", + "--color-primary-light-700": "rgb(218,221,245)", + "--color-primary-light-700-alpha-100": "rgba(218, 221, 245, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(218, 221, 245, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(218, 221, 245, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(218, 221, 245, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(218, 221, 245, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(218, 221, 245, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(218, 221, 245, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(218, 221, 245, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(218, 221, 245, 0.10)", + "--color-primary-light-800": "rgb(225,228,247)", + "--color-primary-light-800-alpha-100": "rgba(225, 228, 247, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(225, 228, 247, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(225, 228, 247, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(225, 228, 247, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(225, 228, 247, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(225, 228, 247, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(225, 228, 247, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(225, 228, 247, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(225, 228, 247, 0.10)", + "--color-primary-light-900": "rgb(231,233,249)", + "--color-primary-light-900-alpha-100": "rgba(231, 233, 249, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(231, 233, 249, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(231, 233, 249, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(231, 233, 249, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(231, 233, 249, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(231, 233, 249, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(231, 233, 249, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(231, 233, 249, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(231, 233, 249, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(79, 98, 208)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(150, 150, 150)", + "--color-primary-dark-100": "rgb(171,171,171)", + "--color-primary-dark-100-alpha-100": "rgba(171, 171, 171, 0.90)", + "--color-primary-alpha-100": "rgba(150, 150, 150, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(171, 171, 171, 0.80)", + "--color-primary-alpha-200": "rgba(150, 150, 150, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(171, 171, 171, 0.70)", + "--color-primary-alpha-300": "rgba(150, 150, 150, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(171, 171, 171, 0.60)", + "--color-primary-alpha-400": "rgba(150, 150, 150, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(171, 171, 171, 0.50)", + "--color-primary-alpha-500": "rgba(150, 150, 150, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(171, 171, 171, 0.40)", + "--color-primary-alpha-600": "rgba(150, 150, 150, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(171, 171, 171, 0.30)", + "--color-primary-alpha-700": "rgba(150, 150, 150, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(171, 171, 171, 0.20)", + "--color-primary-alpha-800": "rgba(150, 150, 150, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(171, 171, 171, 0.10)", + "--color-primary-alpha-900": "rgba(150, 150, 150, 0.10)", + "--color-primary-dark-200": "rgb(188,188,188)", + "--color-primary-dark-200-alpha-100": "rgba(188, 188, 188, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(188, 188, 188, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(188, 188, 188, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(188, 188, 188, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(188, 188, 188, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(188, 188, 188, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(188, 188, 188, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(188, 188, 188, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(188, 188, 188, 0.10)", + "--color-primary-dark-300": "rgb(201,201,201)", + "--color-primary-dark-300-alpha-100": "rgba(201, 201, 201, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(201, 201, 201, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(201, 201, 201, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(201, 201, 201, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(201, 201, 201, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(201, 201, 201, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(201, 201, 201, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(201, 201, 201, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(201, 201, 201, 0.10)", + "--color-primary-dark-400": "rgb(212,212,212)", + "--color-primary-dark-400-alpha-100": "rgba(212, 212, 212, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(212, 212, 212, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(212, 212, 212, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(212, 212, 212, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(212, 212, 212, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(212, 212, 212, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(212, 212, 212, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(212, 212, 212, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(212, 212, 212, 0.10)", + "--color-primary-dark-500": "rgb(221,221,221)", + "--color-primary-dark-500-alpha-100": "rgba(221, 221, 221, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(221, 221, 221, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(221, 221, 221, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(221, 221, 221, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(221, 221, 221, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(221, 221, 221, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(221, 221, 221, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(221, 221, 221, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(221, 221, 221, 0.10)", + "--color-primary-dark-600": "rgb(228,228,228)", + "--color-primary-dark-600-alpha-100": "rgba(228, 228, 228, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(228, 228, 228, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(228, 228, 228, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(228, 228, 228, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(228, 228, 228, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(228, 228, 228, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(228, 228, 228, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(228, 228, 228, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(228, 228, 228, 0.10)", + "--color-primary-dark-700": "rgb(233,233,233)", + "--color-primary-dark-700-alpha-100": "rgba(233, 233, 233, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(233, 233, 233, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(233, 233, 233, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(233, 233, 233, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(233, 233, 233, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(233, 233, 233, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(233, 233, 233, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(233, 233, 233, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(233, 233, 233, 0.10)", + "--color-primary-dark-800": "rgb(237,237,237)", + "--color-primary-dark-800-alpha-100": "rgba(237, 237, 237, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(237, 237, 237, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(237, 237, 237, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(237, 237, 237, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(237, 237, 237, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(237, 237, 237, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(237, 237, 237, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(237, 237, 237, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(237, 237, 237, 0.10)", + "--color-primary-dark-900": "rgb(241,241,241)", + "--color-primary-dark-900-alpha-100": "rgba(241, 241, 241, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(241, 241, 241, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(241, 241, 241, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(241, 241, 241, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(241, 241, 241, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(241, 241, 241, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(241, 241, 241, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(241, 241, 241, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(241, 241, 241, 0.10)", + "--color-primary-dark-1000": "rgb(244,244,244)", + "--color-primary-dark-1000-alpha-100": "rgba(244, 244, 244, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(244, 244, 244, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(244, 244, 244, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(244, 244, 244, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(244, 244, 244, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(244, 244, 244, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(244, 244, 244, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(244, 244, 244, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(244, 244, 244, 0.10)", + "--color-primary-light-100": "rgb(135,135,135)", + "--color-primary-light-100-alpha-100": "rgba(135, 135, 135, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(135, 135, 135, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(135, 135, 135, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(135, 135, 135, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(135, 135, 135, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(135, 135, 135, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(135, 135, 135, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(135, 135, 135, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(135, 135, 135, 0.10)", + "--color-primary-light-200": "rgb(122,122,122)", + "--color-primary-light-200-alpha-100": "rgba(122, 122, 122, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(122, 122, 122, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(122, 122, 122, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(122, 122, 122, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(122, 122, 122, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(122, 122, 122, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(122, 122, 122, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(122, 122, 122, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(122, 122, 122, 0.10)", + "--color-primary-light-300": "rgb(110,110,110)", + "--color-primary-light-300-alpha-100": "rgba(110, 110, 110, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(110, 110, 110, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(110, 110, 110, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(110, 110, 110, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(110, 110, 110, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(110, 110, 110, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(110, 110, 110, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(110, 110, 110, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(110, 110, 110, 0.10)", + "--color-primary-light-400": "rgb(99,99,99)", + "--color-primary-light-400-alpha-100": "rgba(99, 99, 99, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(99, 99, 99, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(99, 99, 99, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(99, 99, 99, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(99, 99, 99, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(99, 99, 99, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(99, 99, 99, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(99, 99, 99, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(99, 99, 99, 0.10)", + "--color-primary-light-500": "rgb(89,89,89)", + "--color-primary-light-500-alpha-100": "rgba(89, 89, 89, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(89, 89, 89, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(89, 89, 89, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(89, 89, 89, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(89, 89, 89, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(89, 89, 89, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(89, 89, 89, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(89, 89, 89, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(89, 89, 89, 0.10)", + "--color-primary-light-600": "rgb(80,80,80)", + "--color-primary-light-600-alpha-100": "rgba(80, 80, 80, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(80, 80, 80, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(80, 80, 80, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(80, 80, 80, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(80, 80, 80, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(80, 80, 80, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(80, 80, 80, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(80, 80, 80, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(80, 80, 80, 0.10)", + "--color-primary-light-700": "rgb(72,72,72)", + "--color-primary-light-700-alpha-100": "rgba(72, 72, 72, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(72, 72, 72, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(72, 72, 72, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(72, 72, 72, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(72, 72, 72, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(72, 72, 72, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(72, 72, 72, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(72, 72, 72, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(72, 72, 72, 0.10)", + "--color-primary-light-800": "rgb(65,65,65)", + "--color-primary-light-800-alpha-100": "rgba(65, 65, 65, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(65, 65, 65, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(65, 65, 65, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(65, 65, 65, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(65, 65, 65, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(65, 65, 65, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(65, 65, 65, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(65, 65, 65, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(65, 65, 65, 0.10)", + "--color-primary-light-900": "rgb(59,59,59)", + "--color-primary-light-900-alpha-100": "rgba(59, 59, 59, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(59, 59, 59, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(59, 59, 59, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(59, 59, 59, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(59, 59, 59, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(59, 59, 59, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(59, 59, 59, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(59, 59, 59, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(59, 59, 59, 0.10)", + "--color-primary-light-1000": "rgb(38,38,38)", + "--color-primary-light-1000-alpha-100": "rgba(38, 38, 38, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(38, 38, 38, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(38, 38, 38, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(38, 38, 38, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(38, 38, 38, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(38, 38, 38, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(38, 38, 38, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(38, 38, 38, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(38, 38, 38, 0.10)", + "--color-theme": "rgb(59,59,59)", + "--color-1000": "rgb(229, 229, 229)", + "--color-950": "rgb(218,218,218)", + "--color-900": "rgb(207,207,207)", + "--color-850": "rgb(197,197,197)", + "--color-800": "rgb(187,187,187)", + "--color-750": "rgb(178,178,178)", + "--color-700": "rgb(169,169,169)", + "--color-650": "rgb(161,161,161)", + "--color-600": "rgb(153,153,153)", + "--color-550": "rgb(145,145,145)", + "--color-500": "rgb(138,138,138)", + "--color-450": "rgb(131,131,131)", + "--color-400": "rgb(124,124,124)", + "--color-350": "rgb(118,118,118)", + "--color-300": "rgb(112,112,112)", + "--color-250": "rgb(106,106,106)", + "--color-200": "rgb(101,101,101)", + "--color-150": "rgb(96,96,96)", + "--color-100": "rgb(91,91,91)", + "--color-050": "rgb(86,86,86)", + "--color-000": "rgb(82,82,82)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(74, 55, 82)", + "--color-primary-dark-100": "rgb(67,50,74)", + "--color-primary-dark-100-alpha-100": "rgba(67, 50, 74, 0.90)", + "--color-primary-alpha-100": "rgba(74, 55, 82, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(67, 50, 74, 0.80)", + "--color-primary-alpha-200": "rgba(74, 55, 82, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(67, 50, 74, 0.70)", + "--color-primary-alpha-300": "rgba(74, 55, 82, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(67, 50, 74, 0.60)", + "--color-primary-alpha-400": "rgba(74, 55, 82, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(67, 50, 74, 0.50)", + "--color-primary-alpha-500": "rgba(74, 55, 82, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(67, 50, 74, 0.40)", + "--color-primary-alpha-600": "rgba(74, 55, 82, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(67, 50, 74, 0.30)", + "--color-primary-alpha-700": "rgba(74, 55, 82, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(67, 50, 74, 0.20)", + "--color-primary-alpha-800": "rgba(74, 55, 82, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(67, 50, 74, 0.10)", + "--color-primary-alpha-900": "rgba(74, 55, 82, 0.10)", + "--color-primary-dark-200": "rgb(60,45,67)", + "--color-primary-dark-200-alpha-100": "rgba(60, 45, 67, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(60, 45, 67, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(60, 45, 67, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(60, 45, 67, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(60, 45, 67, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(60, 45, 67, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(60, 45, 67, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(60, 45, 67, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(60, 45, 67, 0.10)", + "--color-primary-dark-300": "rgb(54,41,60)", + "--color-primary-dark-300-alpha-100": "rgba(54, 41, 60, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(54, 41, 60, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(54, 41, 60, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(54, 41, 60, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(54, 41, 60, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(54, 41, 60, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(54, 41, 60, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(54, 41, 60, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(54, 41, 60, 0.10)", + "--color-primary-dark-400": "rgb(49,37,54)", + "--color-primary-dark-400-alpha-100": "rgba(49, 37, 54, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(49, 37, 54, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(49, 37, 54, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(49, 37, 54, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(49, 37, 54, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(49, 37, 54, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(49, 37, 54, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(49, 37, 54, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(49, 37, 54, 0.10)", + "--color-primary-dark-500": "rgb(44,33,49)", + "--color-primary-dark-500-alpha-100": "rgba(44, 33, 49, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(44, 33, 49, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(44, 33, 49, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(44, 33, 49, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(44, 33, 49, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(44, 33, 49, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(44, 33, 49, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(44, 33, 49, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(44, 33, 49, 0.10)", + "--color-primary-dark-600": "rgb(40,30,44)", + "--color-primary-dark-600-alpha-100": "rgba(40, 30, 44, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(40, 30, 44, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(40, 30, 44, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(40, 30, 44, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(40, 30, 44, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(40, 30, 44, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(40, 30, 44, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(40, 30, 44, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(40, 30, 44, 0.10)", + "--color-primary-dark-700": "rgb(36,27,40)", + "--color-primary-dark-700-alpha-100": "rgba(36, 27, 40, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(36, 27, 40, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(36, 27, 40, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(36, 27, 40, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(36, 27, 40, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(36, 27, 40, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(36, 27, 40, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(36, 27, 40, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(36, 27, 40, 0.10)", + "--color-primary-dark-800": "rgb(32,24,36)", + "--color-primary-dark-800-alpha-100": "rgba(32, 24, 36, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(32, 24, 36, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(32, 24, 36, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(32, 24, 36, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(32, 24, 36, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(32, 24, 36, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(32, 24, 36, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(32, 24, 36, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(32, 24, 36, 0.10)", + "--color-primary-dark-900": "rgb(29,22,32)", + "--color-primary-dark-900-alpha-100": "rgba(29, 22, 32, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(29, 22, 32, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(29, 22, 32, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(29, 22, 32, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(29, 22, 32, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(29, 22, 32, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(29, 22, 32, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(29, 22, 32, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(29, 22, 32, 0.10)", + "--color-primary-dark-1000": "rgb(26,20,29)", + "--color-primary-dark-1000-alpha-100": "rgba(26, 20, 29, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(26, 20, 29, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(26, 20, 29, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(26, 20, 29, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(26, 20, 29, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(26, 20, 29, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(26, 20, 29, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(26, 20, 29, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(26, 20, 29, 0.10)", + "--color-primary-light-100": "rgb(110,95,117)", + "--color-primary-light-100-alpha-100": "rgba(110, 95, 117, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(110, 95, 117, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(110, 95, 117, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(110, 95, 117, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(110, 95, 117, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(110, 95, 117, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(110, 95, 117, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(110, 95, 117, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(110, 95, 117, 0.10)", + "--color-primary-light-200": "rgb(139,127,145)", + "--color-primary-light-200-alpha-100": "rgba(139, 127, 145, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(139, 127, 145, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(139, 127, 145, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(139, 127, 145, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(139, 127, 145, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(139, 127, 145, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(139, 127, 145, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(139, 127, 145, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(139, 127, 145, 0.10)", + "--color-primary-light-300": "rgb(162,153,167)", + "--color-primary-light-300-alpha-100": "rgba(162, 153, 167, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(162, 153, 167, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(162, 153, 167, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(162, 153, 167, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(162, 153, 167, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(162, 153, 167, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(162, 153, 167, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(162, 153, 167, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(162, 153, 167, 0.10)", + "--color-primary-light-400": "rgb(181,173,185)", + "--color-primary-light-400-alpha-100": "rgba(181, 173, 185, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(181, 173, 185, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(181, 173, 185, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(181, 173, 185, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(181, 173, 185, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(181, 173, 185, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(181, 173, 185, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(181, 173, 185, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(181, 173, 185, 0.10)", + "--color-primary-light-500": "rgb(196,189,199)", + "--color-primary-light-500-alpha-100": "rgba(196, 189, 199, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(196, 189, 199, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(196, 189, 199, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(196, 189, 199, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(196, 189, 199, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(196, 189, 199, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(196, 189, 199, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(196, 189, 199, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(196, 189, 199, 0.10)", + "--color-primary-light-600": "rgb(208,202,210)", + "--color-primary-light-600-alpha-100": "rgba(208, 202, 210, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(208, 202, 210, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(208, 202, 210, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(208, 202, 210, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(208, 202, 210, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(208, 202, 210, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(208, 202, 210, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(208, 202, 210, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(208, 202, 210, 0.10)", + "--color-primary-light-700": "rgb(217,213,219)", + "--color-primary-light-700-alpha-100": "rgba(217, 213, 219, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(217, 213, 219, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(217, 213, 219, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(217, 213, 219, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(217, 213, 219, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(217, 213, 219, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(217, 213, 219, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(217, 213, 219, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(217, 213, 219, 0.10)", + "--color-primary-light-800": "rgb(225,221,226)", + "--color-primary-light-800-alpha-100": "rgba(225, 221, 226, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(225, 221, 226, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(225, 221, 226, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(225, 221, 226, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(225, 221, 226, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(225, 221, 226, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(225, 221, 226, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(225, 221, 226, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(225, 221, 226, 0.10)", + "--color-primary-light-900": "rgb(231,228,232)", + "--color-primary-light-900-alpha-100": "rgba(231, 228, 232, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(231, 228, 232, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(231, 228, 232, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(231, 228, 232, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(231, 228, 232, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(231, 228, 232, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(231, 228, 232, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(231, 228, 232, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(231, 228, 232, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(74, 55, 82)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(87, 144, 167)", + "--color-primary-dark-100": "rgb(78,130,150)", + "--color-primary-dark-100-alpha-100": "rgba(78, 130, 150, 0.90)", + "--color-primary-alpha-100": "rgba(87, 144, 167, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(78, 130, 150, 0.80)", + "--color-primary-alpha-200": "rgba(87, 144, 167, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(78, 130, 150, 0.70)", + "--color-primary-alpha-300": "rgba(87, 144, 167, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(78, 130, 150, 0.60)", + "--color-primary-alpha-400": "rgba(87, 144, 167, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(78, 130, 150, 0.50)", + "--color-primary-alpha-500": "rgba(87, 144, 167, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(78, 130, 150, 0.40)", + "--color-primary-alpha-600": "rgba(87, 144, 167, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(78, 130, 150, 0.30)", + "--color-primary-alpha-700": "rgba(87, 144, 167, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(78, 130, 150, 0.20)", + "--color-primary-alpha-800": "rgba(87, 144, 167, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(78, 130, 150, 0.10)", + "--color-primary-alpha-900": "rgba(87, 144, 167, 0.10)", + "--color-primary-dark-200": "rgb(70,117,135)", + "--color-primary-dark-200-alpha-100": "rgba(70, 117, 135, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(70, 117, 135, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(70, 117, 135, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(70, 117, 135, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(70, 117, 135, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(70, 117, 135, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(70, 117, 135, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(70, 117, 135, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(70, 117, 135, 0.10)", + "--color-primary-dark-300": "rgb(63,105,122)", + "--color-primary-dark-300-alpha-100": "rgba(63, 105, 122, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(63, 105, 122, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(63, 105, 122, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(63, 105, 122, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(63, 105, 122, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(63, 105, 122, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(63, 105, 122, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(63, 105, 122, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(63, 105, 122, 0.10)", + "--color-primary-dark-400": "rgb(57,95,110)", + "--color-primary-dark-400-alpha-100": "rgba(57, 95, 110, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(57, 95, 110, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(57, 95, 110, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(57, 95, 110, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(57, 95, 110, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(57, 95, 110, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(57, 95, 110, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(57, 95, 110, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(57, 95, 110, 0.10)", + "--color-primary-dark-500": "rgb(51,86,99)", + "--color-primary-dark-500-alpha-100": "rgba(51, 86, 99, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(51, 86, 99, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(51, 86, 99, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(51, 86, 99, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(51, 86, 99, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(51, 86, 99, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(51, 86, 99, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(51, 86, 99, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(51, 86, 99, 0.10)", + "--color-primary-dark-600": "rgb(46,77,89)", + "--color-primary-dark-600-alpha-100": "rgba(46, 77, 89, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(46, 77, 89, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(46, 77, 89, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(46, 77, 89, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(46, 77, 89, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(46, 77, 89, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(46, 77, 89, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(46, 77, 89, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(46, 77, 89, 0.10)", + "--color-primary-dark-700": "rgb(41,69,80)", + "--color-primary-dark-700-alpha-100": "rgba(41, 69, 80, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(41, 69, 80, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(41, 69, 80, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(41, 69, 80, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(41, 69, 80, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(41, 69, 80, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(41, 69, 80, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(41, 69, 80, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(41, 69, 80, 0.10)", + "--color-primary-dark-800": "rgb(37,62,72)", + "--color-primary-dark-800-alpha-100": "rgba(37, 62, 72, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(37, 62, 72, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(37, 62, 72, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(37, 62, 72, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(37, 62, 72, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(37, 62, 72, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(37, 62, 72, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(37, 62, 72, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(37, 62, 72, 0.10)", + "--color-primary-dark-900": "rgb(33,56,65)", + "--color-primary-dark-900-alpha-100": "rgba(33, 56, 65, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(33, 56, 65, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(33, 56, 65, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(33, 56, 65, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(33, 56, 65, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(33, 56, 65, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(33, 56, 65, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(33, 56, 65, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(33, 56, 65, 0.10)", + "--color-primary-dark-1000": "rgb(30,50,59)", + "--color-primary-dark-1000-alpha-100": "rgba(30, 50, 59, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(30, 50, 59, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(30, 50, 59, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(30, 50, 59, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(30, 50, 59, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(30, 50, 59, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(30, 50, 59, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(30, 50, 59, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(30, 50, 59, 0.10)", + "--color-primary-light-100": "rgb(121,166,185)", + "--color-primary-light-100-alpha-100": "rgba(121, 166, 185, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(121, 166, 185, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(121, 166, 185, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(121, 166, 185, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(121, 166, 185, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(121, 166, 185, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(121, 166, 185, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(121, 166, 185, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(121, 166, 185, 0.10)", + "--color-primary-light-200": "rgb(148,184,199)", + "--color-primary-light-200-alpha-100": "rgba(148, 184, 199, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(148, 184, 199, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(148, 184, 199, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(148, 184, 199, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(148, 184, 199, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(148, 184, 199, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(148, 184, 199, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(148, 184, 199, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(148, 184, 199, 0.10)", + "--color-primary-light-300": "rgb(169,198,210)", + "--color-primary-light-300-alpha-100": "rgba(169, 198, 210, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(169, 198, 210, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(169, 198, 210, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(169, 198, 210, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(169, 198, 210, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(169, 198, 210, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(169, 198, 210, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(169, 198, 210, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(169, 198, 210, 0.10)", + "--color-primary-light-400": "rgb(186,209,219)", + "--color-primary-light-400-alpha-100": "rgba(186, 209, 219, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(186, 209, 219, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(186, 209, 219, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(186, 209, 219, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(186, 209, 219, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(186, 209, 219, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(186, 209, 219, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(186, 209, 219, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(186, 209, 219, 0.10)", + "--color-primary-light-500": "rgb(200,218,226)", + "--color-primary-light-500-alpha-100": "rgba(200, 218, 226, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(200, 218, 226, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(200, 218, 226, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(200, 218, 226, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(200, 218, 226, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(200, 218, 226, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(200, 218, 226, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(200, 218, 226, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(200, 218, 226, 0.10)", + "--color-primary-light-600": "rgb(211,225,232)", + "--color-primary-light-600-alpha-100": "rgba(211, 225, 232, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(211, 225, 232, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(211, 225, 232, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(211, 225, 232, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(211, 225, 232, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(211, 225, 232, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(211, 225, 232, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(211, 225, 232, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(211, 225, 232, 0.10)", + "--color-primary-light-700": "rgb(220,231,237)", + "--color-primary-light-700-alpha-100": "rgba(220, 231, 237, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(220, 231, 237, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(220, 231, 237, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(220, 231, 237, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(220, 231, 237, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(220, 231, 237, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(220, 231, 237, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(220, 231, 237, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(220, 231, 237, 0.10)", + "--color-primary-light-800": "rgb(227,236,241)", + "--color-primary-light-800-alpha-100": "rgba(227, 236, 241, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(227, 236, 241, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(227, 236, 241, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(227, 236, 241, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(227, 236, 241, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(227, 236, 241, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(227, 236, 241, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(227, 236, 241, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(227, 236, 241, 0.10)", + "--color-primary-light-900": "rgb(233,240,244)", + "--color-primary-light-900-alpha-100": "rgba(233, 240, 244, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(233, 240, 244, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(233, 240, 244, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(233, 240, 244, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(233, 240, 244, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(233, 240, 244, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(233, 240, 244, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(233, 240, 244, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(233, 240, 244, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(87, 144, 167)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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": "china_ink", + "name": "近墨者黑", + "isDark": false, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgba(47, 47, 47, 1)", + "--color-primary-dark-100": "rgba(42,42,42, 1)", + "--color-primary-dark-100-alpha-100": "rgba(42, 42, 42, 0.90)", + "--color-primary-alpha-100": "rgba(47, 47, 47, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(42, 42, 42, 0.80)", + "--color-primary-alpha-200": "rgba(47, 47, 47, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(42, 42, 42, 0.70)", + "--color-primary-alpha-300": "rgba(47, 47, 47, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(42, 42, 42, 0.60)", + "--color-primary-alpha-400": "rgba(47, 47, 47, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(42, 42, 42, 0.50)", + "--color-primary-alpha-500": "rgba(47, 47, 47, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(42, 42, 42, 0.40)", + "--color-primary-alpha-600": "rgba(47, 47, 47, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(42, 42, 42, 0.30)", + "--color-primary-alpha-700": "rgba(47, 47, 47, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(42, 42, 42, 0.20)", + "--color-primary-alpha-800": "rgba(47, 47, 47, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(42, 42, 42, 0.10)", + "--color-primary-alpha-900": "rgba(47, 47, 47, 0.10)", + "--color-primary-dark-200": "rgba(38,38,38, 1)", + "--color-primary-dark-200-alpha-100": "rgba(38, 38, 38, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(38, 38, 38, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(38, 38, 38, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(38, 38, 38, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(38, 38, 38, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(38, 38, 38, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(38, 38, 38, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(38, 38, 38, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(38, 38, 38, 0.10)", + "--color-primary-dark-300": "rgba(34,34,34, 1)", + "--color-primary-dark-300-alpha-100": "rgba(34, 34, 34, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(34, 34, 34, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(34, 34, 34, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(34, 34, 34, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(34, 34, 34, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(34, 34, 34, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(34, 34, 34, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(34, 34, 34, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(34, 34, 34, 0.10)", + "--color-primary-dark-400": "rgba(31,31,31, 1)", + "--color-primary-dark-400-alpha-100": "rgba(31, 31, 31, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(31, 31, 31, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(31, 31, 31, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(31, 31, 31, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(31, 31, 31, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(31, 31, 31, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(31, 31, 31, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(31, 31, 31, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(31, 31, 31, 0.10)", + "--color-primary-dark-500": "rgba(28,28,28, 1)", + "--color-primary-dark-500-alpha-100": "rgba(28, 28, 28, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(28, 28, 28, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(28, 28, 28, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(28, 28, 28, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(28, 28, 28, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(28, 28, 28, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(28, 28, 28, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(28, 28, 28, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(28, 28, 28, 0.10)", + "--color-primary-dark-600": "rgba(25,25,25, 1)", + "--color-primary-dark-600-alpha-100": "rgba(25, 25, 25, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(25, 25, 25, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(25, 25, 25, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(25, 25, 25, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(25, 25, 25, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(25, 25, 25, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(25, 25, 25, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(25, 25, 25, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(25, 25, 25, 0.10)", + "--color-primary-dark-700": "rgba(23,23,23, 1)", + "--color-primary-dark-700-alpha-100": "rgba(23, 23, 23, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(23, 23, 23, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(23, 23, 23, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(23, 23, 23, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(23, 23, 23, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(23, 23, 23, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(23, 23, 23, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(23, 23, 23, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(23, 23, 23, 0.10)", + "--color-primary-dark-800": "rgba(21,21,21, 1)", + "--color-primary-dark-800-alpha-100": "rgba(21, 21, 21, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(21, 21, 21, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(21, 21, 21, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(21, 21, 21, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(21, 21, 21, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(21, 21, 21, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(21, 21, 21, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(21, 21, 21, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(21, 21, 21, 0.10)", + "--color-primary-dark-900": "rgba(19,19,19, 1)", + "--color-primary-dark-900-alpha-100": "rgba(19, 19, 19, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(19, 19, 19, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(19, 19, 19, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(19, 19, 19, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(19, 19, 19, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(19, 19, 19, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(19, 19, 19, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(19, 19, 19, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(19, 19, 19, 0.10)", + "--color-primary-dark-1000": "rgba(17,17,17, 1)", + "--color-primary-dark-1000-alpha-100": "rgba(17, 17, 17, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(17, 17, 17, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(17, 17, 17, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(17, 17, 17, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(17, 17, 17, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(17, 17, 17, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(17, 17, 17, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(17, 17, 17, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(17, 17, 17, 0.10)", + "--color-primary-light-100": "rgba(89,89,89, 1)", + "--color-primary-light-100-alpha-100": "rgba(89, 89, 89, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(89, 89, 89, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(89, 89, 89, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(89, 89, 89, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(89, 89, 89, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(89, 89, 89, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(89, 89, 89, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(89, 89, 89, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(89, 89, 89, 0.10)", + "--color-primary-light-200": "rgba(122,122,122, 1)", + "--color-primary-light-200-alpha-100": "rgba(122, 122, 122, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(122, 122, 122, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(122, 122, 122, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(122, 122, 122, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(122, 122, 122, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(122, 122, 122, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(122, 122, 122, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(122, 122, 122, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(122, 122, 122, 0.10)", + "--color-primary-light-300": "rgba(149,149,149, 1)", + "--color-primary-light-300-alpha-100": "rgba(149, 149, 149, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(149, 149, 149, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(149, 149, 149, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(149, 149, 149, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(149, 149, 149, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(149, 149, 149, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(149, 149, 149, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(149, 149, 149, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(149, 149, 149, 0.10)", + "--color-primary-light-400": "rgba(170,170,170, 1)", + "--color-primary-light-400-alpha-100": "rgba(170, 170, 170, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(170, 170, 170, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(170, 170, 170, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(170, 170, 170, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(170, 170, 170, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(170, 170, 170, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(170, 170, 170, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(170, 170, 170, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(170, 170, 170, 0.10)", + "--color-primary-light-500": "rgba(187,187,187, 1)", + "--color-primary-light-500-alpha-100": "rgba(187, 187, 187, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(187, 187, 187, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(187, 187, 187, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(187, 187, 187, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(187, 187, 187, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(187, 187, 187, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(187, 187, 187, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(187, 187, 187, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(187, 187, 187, 0.10)", + "--color-primary-light-600": "rgba(201,201,201, 1)", + "--color-primary-light-600-alpha-100": "rgba(201, 201, 201, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(201, 201, 201, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(201, 201, 201, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(201, 201, 201, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(201, 201, 201, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(201, 201, 201, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(201, 201, 201, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(201, 201, 201, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(201, 201, 201, 0.10)", + "--color-primary-light-700": "rgba(212,212,212, 1)", + "--color-primary-light-700-alpha-100": "rgba(212, 212, 212, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(212, 212, 212, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(212, 212, 212, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(212, 212, 212, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(212, 212, 212, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(212, 212, 212, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(212, 212, 212, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(212, 212, 212, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(212, 212, 212, 0.10)", + "--color-primary-light-800": "rgba(221,221,221, 1)", + "--color-primary-light-800-alpha-100": "rgba(221, 221, 221, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(221, 221, 221, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(221, 221, 221, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(221, 221, 221, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(221, 221, 221, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(221, 221, 221, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(221, 221, 221, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(221, 221, 221, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(221, 221, 221, 0.10)", + "--color-primary-light-900": "rgba(228,228,228, 1)", + "--color-primary-light-900-alpha-100": "rgba(228, 228, 228, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(228, 228, 228, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(228, 228, 228, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(228, 228, 228, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(228, 228, 228, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(228, 228, 228, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(228, 228, 228, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(228, 228, 228, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(228, 228, 228, 0.10)", + "--color-primary-light-1000": "rgba(255,255,255, 1)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgba(47, 47, 47, 1)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--color-app-background": "rgba(255, 255, 255, 0)", + "--color-main-background": "rgba(255, 255, 255, 0.8)", + "--color-nav-font": "var(--color-primary)", + "--background-image": "url(./theme_images/china_ink.jpg)", + "--background-image-position": "center", + "--background-image-size": "cover", + "--color-btn-hide": "rgba(183, 212, 208, 1)", + "--color-btn-min": "rgba(200, 214, 183, 1)", + "--color-btn-close": "rgba(218, 195, 188, 1)", + "--color-badge-primary": "rgba(137, 70, 70, 1)", + "--color-badge-secondary": "rgba(67, 139, 65, 1)", + "--color-badge-tertiary": "rgba(132, 135, 65, 1)" + } + } + }, + { + "id": "happy_new_year", + "name": "新年快乐", + "isDark": false, + "isDarkFont": false, + "isCustom": false, + "config": { + "themeColors": { + "--color-primary": "rgb(192, 57, 43)", + "--color-primary-dark-100": "rgb(173,51,39)", + "--color-primary-dark-100-alpha-100": "rgba(173, 51, 39, 0.90)", + "--color-primary-alpha-100": "rgba(192, 57, 43, 0.90)", + "--color-primary-dark-100-alpha-200": "rgba(173, 51, 39, 0.80)", + "--color-primary-alpha-200": "rgba(192, 57, 43, 0.80)", + "--color-primary-dark-100-alpha-300": "rgba(173, 51, 39, 0.70)", + "--color-primary-alpha-300": "rgba(192, 57, 43, 0.70)", + "--color-primary-dark-100-alpha-400": "rgba(173, 51, 39, 0.60)", + "--color-primary-alpha-400": "rgba(192, 57, 43, 0.60)", + "--color-primary-dark-100-alpha-500": "rgba(173, 51, 39, 0.50)", + "--color-primary-alpha-500": "rgba(192, 57, 43, 0.50)", + "--color-primary-dark-100-alpha-600": "rgba(173, 51, 39, 0.40)", + "--color-primary-alpha-600": "rgba(192, 57, 43, 0.40)", + "--color-primary-dark-100-alpha-700": "rgba(173, 51, 39, 0.30)", + "--color-primary-alpha-700": "rgba(192, 57, 43, 0.30)", + "--color-primary-dark-100-alpha-800": "rgba(173, 51, 39, 0.20)", + "--color-primary-alpha-800": "rgba(192, 57, 43, 0.20)", + "--color-primary-dark-100-alpha-900": "rgba(173, 51, 39, 0.10)", + "--color-primary-alpha-900": "rgba(192, 57, 43, 0.10)", + "--color-primary-dark-200": "rgb(156,46,35)", + "--color-primary-dark-200-alpha-100": "rgba(156, 46, 35, 0.90)", + "--color-primary-dark-200-alpha-200": "rgba(156, 46, 35, 0.80)", + "--color-primary-dark-200-alpha-300": "rgba(156, 46, 35, 0.70)", + "--color-primary-dark-200-alpha-400": "rgba(156, 46, 35, 0.60)", + "--color-primary-dark-200-alpha-500": "rgba(156, 46, 35, 0.50)", + "--color-primary-dark-200-alpha-600": "rgba(156, 46, 35, 0.40)", + "--color-primary-dark-200-alpha-700": "rgba(156, 46, 35, 0.30)", + "--color-primary-dark-200-alpha-800": "rgba(156, 46, 35, 0.20)", + "--color-primary-dark-200-alpha-900": "rgba(156, 46, 35, 0.10)", + "--color-primary-dark-300": "rgb(140,41,32)", + "--color-primary-dark-300-alpha-100": "rgba(140, 41, 32, 0.90)", + "--color-primary-dark-300-alpha-200": "rgba(140, 41, 32, 0.80)", + "--color-primary-dark-300-alpha-300": "rgba(140, 41, 32, 0.70)", + "--color-primary-dark-300-alpha-400": "rgba(140, 41, 32, 0.60)", + "--color-primary-dark-300-alpha-500": "rgba(140, 41, 32, 0.50)", + "--color-primary-dark-300-alpha-600": "rgba(140, 41, 32, 0.40)", + "--color-primary-dark-300-alpha-700": "rgba(140, 41, 32, 0.30)", + "--color-primary-dark-300-alpha-800": "rgba(140, 41, 32, 0.20)", + "--color-primary-dark-300-alpha-900": "rgba(140, 41, 32, 0.10)", + "--color-primary-dark-400": "rgb(126,37,29)", + "--color-primary-dark-400-alpha-100": "rgba(126, 37, 29, 0.90)", + "--color-primary-dark-400-alpha-200": "rgba(126, 37, 29, 0.80)", + "--color-primary-dark-400-alpha-300": "rgba(126, 37, 29, 0.70)", + "--color-primary-dark-400-alpha-400": "rgba(126, 37, 29, 0.60)", + "--color-primary-dark-400-alpha-500": "rgba(126, 37, 29, 0.50)", + "--color-primary-dark-400-alpha-600": "rgba(126, 37, 29, 0.40)", + "--color-primary-dark-400-alpha-700": "rgba(126, 37, 29, 0.30)", + "--color-primary-dark-400-alpha-800": "rgba(126, 37, 29, 0.20)", + "--color-primary-dark-400-alpha-900": "rgba(126, 37, 29, 0.10)", + "--color-primary-dark-500": "rgb(113,33,26)", + "--color-primary-dark-500-alpha-100": "rgba(113, 33, 26, 0.90)", + "--color-primary-dark-500-alpha-200": "rgba(113, 33, 26, 0.80)", + "--color-primary-dark-500-alpha-300": "rgba(113, 33, 26, 0.70)", + "--color-primary-dark-500-alpha-400": "rgba(113, 33, 26, 0.60)", + "--color-primary-dark-500-alpha-500": "rgba(113, 33, 26, 0.50)", + "--color-primary-dark-500-alpha-600": "rgba(113, 33, 26, 0.40)", + "--color-primary-dark-500-alpha-700": "rgba(113, 33, 26, 0.30)", + "--color-primary-dark-500-alpha-800": "rgba(113, 33, 26, 0.20)", + "--color-primary-dark-500-alpha-900": "rgba(113, 33, 26, 0.10)", + "--color-primary-dark-600": "rgb(102,30,23)", + "--color-primary-dark-600-alpha-100": "rgba(102, 30, 23, 0.90)", + "--color-primary-dark-600-alpha-200": "rgba(102, 30, 23, 0.80)", + "--color-primary-dark-600-alpha-300": "rgba(102, 30, 23, 0.70)", + "--color-primary-dark-600-alpha-400": "rgba(102, 30, 23, 0.60)", + "--color-primary-dark-600-alpha-500": "rgba(102, 30, 23, 0.50)", + "--color-primary-dark-600-alpha-600": "rgba(102, 30, 23, 0.40)", + "--color-primary-dark-600-alpha-700": "rgba(102, 30, 23, 0.30)", + "--color-primary-dark-600-alpha-800": "rgba(102, 30, 23, 0.20)", + "--color-primary-dark-600-alpha-900": "rgba(102, 30, 23, 0.10)", + "--color-primary-dark-700": "rgb(92,27,21)", + "--color-primary-dark-700-alpha-100": "rgba(92, 27, 21, 0.90)", + "--color-primary-dark-700-alpha-200": "rgba(92, 27, 21, 0.80)", + "--color-primary-dark-700-alpha-300": "rgba(92, 27, 21, 0.70)", + "--color-primary-dark-700-alpha-400": "rgba(92, 27, 21, 0.60)", + "--color-primary-dark-700-alpha-500": "rgba(92, 27, 21, 0.50)", + "--color-primary-dark-700-alpha-600": "rgba(92, 27, 21, 0.40)", + "--color-primary-dark-700-alpha-700": "rgba(92, 27, 21, 0.30)", + "--color-primary-dark-700-alpha-800": "rgba(92, 27, 21, 0.20)", + "--color-primary-dark-700-alpha-900": "rgba(92, 27, 21, 0.10)", + "--color-primary-dark-800": "rgb(83,24,19)", + "--color-primary-dark-800-alpha-100": "rgba(83, 24, 19, 0.90)", + "--color-primary-dark-800-alpha-200": "rgba(83, 24, 19, 0.80)", + "--color-primary-dark-800-alpha-300": "rgba(83, 24, 19, 0.70)", + "--color-primary-dark-800-alpha-400": "rgba(83, 24, 19, 0.60)", + "--color-primary-dark-800-alpha-500": "rgba(83, 24, 19, 0.50)", + "--color-primary-dark-800-alpha-600": "rgba(83, 24, 19, 0.40)", + "--color-primary-dark-800-alpha-700": "rgba(83, 24, 19, 0.30)", + "--color-primary-dark-800-alpha-800": "rgba(83, 24, 19, 0.20)", + "--color-primary-dark-800-alpha-900": "rgba(83, 24, 19, 0.10)", + "--color-primary-dark-900": "rgb(75,22,17)", + "--color-primary-dark-900-alpha-100": "rgba(75, 22, 17, 0.90)", + "--color-primary-dark-900-alpha-200": "rgba(75, 22, 17, 0.80)", + "--color-primary-dark-900-alpha-300": "rgba(75, 22, 17, 0.70)", + "--color-primary-dark-900-alpha-400": "rgba(75, 22, 17, 0.60)", + "--color-primary-dark-900-alpha-500": "rgba(75, 22, 17, 0.50)", + "--color-primary-dark-900-alpha-600": "rgba(75, 22, 17, 0.40)", + "--color-primary-dark-900-alpha-700": "rgba(75, 22, 17, 0.30)", + "--color-primary-dark-900-alpha-800": "rgba(75, 22, 17, 0.20)", + "--color-primary-dark-900-alpha-900": "rgba(75, 22, 17, 0.10)", + "--color-primary-dark-1000": "rgb(68,20,15)", + "--color-primary-dark-1000-alpha-100": "rgba(68, 20, 15, 0.90)", + "--color-primary-dark-1000-alpha-200": "rgba(68, 20, 15, 0.80)", + "--color-primary-dark-1000-alpha-300": "rgba(68, 20, 15, 0.70)", + "--color-primary-dark-1000-alpha-400": "rgba(68, 20, 15, 0.60)", + "--color-primary-dark-1000-alpha-500": "rgba(68, 20, 15, 0.50)", + "--color-primary-dark-1000-alpha-600": "rgba(68, 20, 15, 0.40)", + "--color-primary-dark-1000-alpha-700": "rgba(68, 20, 15, 0.30)", + "--color-primary-dark-1000-alpha-800": "rgba(68, 20, 15, 0.20)", + "--color-primary-dark-1000-alpha-900": "rgba(68, 20, 15, 0.10)", + "--color-primary-light-100": "rgb(205,97,85)", + "--color-primary-light-100-alpha-100": "rgba(205, 97, 85, 0.90)", + "--color-primary-light-100-alpha-200": "rgba(205, 97, 85, 0.80)", + "--color-primary-light-100-alpha-300": "rgba(205, 97, 85, 0.70)", + "--color-primary-light-100-alpha-400": "rgba(205, 97, 85, 0.60)", + "--color-primary-light-100-alpha-500": "rgba(205, 97, 85, 0.50)", + "--color-primary-light-100-alpha-600": "rgba(205, 97, 85, 0.40)", + "--color-primary-light-100-alpha-700": "rgba(205, 97, 85, 0.30)", + "--color-primary-light-100-alpha-800": "rgba(205, 97, 85, 0.20)", + "--color-primary-light-100-alpha-900": "rgba(205, 97, 85, 0.10)", + "--color-primary-light-200": "rgb(215,129,119)", + "--color-primary-light-200-alpha-100": "rgba(215, 129, 119, 0.90)", + "--color-primary-light-200-alpha-200": "rgba(215, 129, 119, 0.80)", + "--color-primary-light-200-alpha-300": "rgba(215, 129, 119, 0.70)", + "--color-primary-light-200-alpha-400": "rgba(215, 129, 119, 0.60)", + "--color-primary-light-200-alpha-500": "rgba(215, 129, 119, 0.50)", + "--color-primary-light-200-alpha-600": "rgba(215, 129, 119, 0.40)", + "--color-primary-light-200-alpha-700": "rgba(215, 129, 119, 0.30)", + "--color-primary-light-200-alpha-800": "rgba(215, 129, 119, 0.20)", + "--color-primary-light-200-alpha-900": "rgba(215, 129, 119, 0.10)", + "--color-primary-light-300": "rgb(223,154,146)", + "--color-primary-light-300-alpha-100": "rgba(223, 154, 146, 0.90)", + "--color-primary-light-300-alpha-200": "rgba(223, 154, 146, 0.80)", + "--color-primary-light-300-alpha-300": "rgba(223, 154, 146, 0.70)", + "--color-primary-light-300-alpha-400": "rgba(223, 154, 146, 0.60)", + "--color-primary-light-300-alpha-500": "rgba(223, 154, 146, 0.50)", + "--color-primary-light-300-alpha-600": "rgba(223, 154, 146, 0.40)", + "--color-primary-light-300-alpha-700": "rgba(223, 154, 146, 0.30)", + "--color-primary-light-300-alpha-800": "rgba(223, 154, 146, 0.20)", + "--color-primary-light-300-alpha-900": "rgba(223, 154, 146, 0.10)", + "--color-primary-light-400": "rgb(229,174,168)", + "--color-primary-light-400-alpha-100": "rgba(229, 174, 168, 0.90)", + "--color-primary-light-400-alpha-200": "rgba(229, 174, 168, 0.80)", + "--color-primary-light-400-alpha-300": "rgba(229, 174, 168, 0.70)", + "--color-primary-light-400-alpha-400": "rgba(229, 174, 168, 0.60)", + "--color-primary-light-400-alpha-500": "rgba(229, 174, 168, 0.50)", + "--color-primary-light-400-alpha-600": "rgba(229, 174, 168, 0.40)", + "--color-primary-light-400-alpha-700": "rgba(229, 174, 168, 0.30)", + "--color-primary-light-400-alpha-800": "rgba(229, 174, 168, 0.20)", + "--color-primary-light-400-alpha-900": "rgba(229, 174, 168, 0.10)", + "--color-primary-light-500": "rgb(234,190,185)", + "--color-primary-light-500-alpha-100": "rgba(234, 190, 185, 0.90)", + "--color-primary-light-500-alpha-200": "rgba(234, 190, 185, 0.80)", + "--color-primary-light-500-alpha-300": "rgba(234, 190, 185, 0.70)", + "--color-primary-light-500-alpha-400": "rgba(234, 190, 185, 0.60)", + "--color-primary-light-500-alpha-500": "rgba(234, 190, 185, 0.50)", + "--color-primary-light-500-alpha-600": "rgba(234, 190, 185, 0.40)", + "--color-primary-light-500-alpha-700": "rgba(234, 190, 185, 0.30)", + "--color-primary-light-500-alpha-800": "rgba(234, 190, 185, 0.20)", + "--color-primary-light-500-alpha-900": "rgba(234, 190, 185, 0.10)", + "--color-primary-light-600": "rgb(238,203,199)", + "--color-primary-light-600-alpha-100": "rgba(238, 203, 199, 0.90)", + "--color-primary-light-600-alpha-200": "rgba(238, 203, 199, 0.80)", + "--color-primary-light-600-alpha-300": "rgba(238, 203, 199, 0.70)", + "--color-primary-light-600-alpha-400": "rgba(238, 203, 199, 0.60)", + "--color-primary-light-600-alpha-500": "rgba(238, 203, 199, 0.50)", + "--color-primary-light-600-alpha-600": "rgba(238, 203, 199, 0.40)", + "--color-primary-light-600-alpha-700": "rgba(238, 203, 199, 0.30)", + "--color-primary-light-600-alpha-800": "rgba(238, 203, 199, 0.20)", + "--color-primary-light-600-alpha-900": "rgba(238, 203, 199, 0.10)", + "--color-primary-light-700": "rgb(241,213,210)", + "--color-primary-light-700-alpha-100": "rgba(241, 213, 210, 0.90)", + "--color-primary-light-700-alpha-200": "rgba(241, 213, 210, 0.80)", + "--color-primary-light-700-alpha-300": "rgba(241, 213, 210, 0.70)", + "--color-primary-light-700-alpha-400": "rgba(241, 213, 210, 0.60)", + "--color-primary-light-700-alpha-500": "rgba(241, 213, 210, 0.50)", + "--color-primary-light-700-alpha-600": "rgba(241, 213, 210, 0.40)", + "--color-primary-light-700-alpha-700": "rgba(241, 213, 210, 0.30)", + "--color-primary-light-700-alpha-800": "rgba(241, 213, 210, 0.20)", + "--color-primary-light-700-alpha-900": "rgba(241, 213, 210, 0.10)", + "--color-primary-light-800": "rgb(244,221,219)", + "--color-primary-light-800-alpha-100": "rgba(244, 221, 219, 0.90)", + "--color-primary-light-800-alpha-200": "rgba(244, 221, 219, 0.80)", + "--color-primary-light-800-alpha-300": "rgba(244, 221, 219, 0.70)", + "--color-primary-light-800-alpha-400": "rgba(244, 221, 219, 0.60)", + "--color-primary-light-800-alpha-500": "rgba(244, 221, 219, 0.50)", + "--color-primary-light-800-alpha-600": "rgba(244, 221, 219, 0.40)", + "--color-primary-light-800-alpha-700": "rgba(244, 221, 219, 0.30)", + "--color-primary-light-800-alpha-800": "rgba(244, 221, 219, 0.20)", + "--color-primary-light-800-alpha-900": "rgba(244, 221, 219, 0.10)", + "--color-primary-light-900": "rgb(246,228,226)", + "--color-primary-light-900-alpha-100": "rgba(246, 228, 226, 0.90)", + "--color-primary-light-900-alpha-200": "rgba(246, 228, 226, 0.80)", + "--color-primary-light-900-alpha-300": "rgba(246, 228, 226, 0.70)", + "--color-primary-light-900-alpha-400": "rgba(246, 228, 226, 0.60)", + "--color-primary-light-900-alpha-500": "rgba(246, 228, 226, 0.50)", + "--color-primary-light-900-alpha-600": "rgba(246, 228, 226, 0.40)", + "--color-primary-light-900-alpha-700": "rgba(246, 228, 226, 0.30)", + "--color-primary-light-900-alpha-800": "rgba(246, 228, 226, 0.20)", + "--color-primary-light-900-alpha-900": "rgba(246, 228, 226, 0.10)", + "--color-primary-light-1000": "rgb(255,255,255)", + "--color-primary-light-1000-alpha-100": "rgba(255, 255, 255, 0.90)", + "--color-primary-light-1000-alpha-200": "rgba(255, 255, 255, 0.80)", + "--color-primary-light-1000-alpha-300": "rgba(255, 255, 255, 0.70)", + "--color-primary-light-1000-alpha-400": "rgba(255, 255, 255, 0.60)", + "--color-primary-light-1000-alpha-500": "rgba(255, 255, 255, 0.50)", + "--color-primary-light-1000-alpha-600": "rgba(255, 255, 255, 0.40)", + "--color-primary-light-1000-alpha-700": "rgba(255, 255, 255, 0.30)", + "--color-primary-light-1000-alpha-800": "rgba(255, 255, 255, 0.20)", + "--color-primary-light-1000-alpha-900": "rgba(255, 255, 255, 0.10)", + "--color-theme": "rgb(192, 57, 43)", + "--color-1000": "rgb(33, 33, 33)", + "--color-950": "rgb(44,44,44)", + "--color-900": "rgb(55,55,55)", + "--color-850": "rgb(66,66,66)", + "--color-800": "rgb(77,77,77)", + "--color-750": "rgb(89,89,89)", + "--color-700": "rgb(100,100,100)", + "--color-650": "rgb(111,111,111)", + "--color-600": "rgb(122,122,122)", + "--color-550": "rgb(133,133,133)", + "--color-500": "rgb(144,144,144)", + "--color-450": "rgb(155,155,155)", + "--color-400": "rgb(166,166,166)", + "--color-350": "rgb(177,177,177)", + "--color-300": "rgb(188,188,188)", + "--color-250": "rgb(200,200,200)", + "--color-200": "rgb(211,211,211)", + "--color-150": "rgb(222,222,222)", + "--color-100": "rgb(233,233,233)", + "--color-050": "rgb(244,244,244)", + "--color-000": "rgb(255,255,255)" + }, + "extInfo": { + "--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)" + } + } + } +] \ No newline at end of file diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/theme/utils.js b/plugins/lx-music-ztools/vendor/lx/src/common/theme/utils.js new file mode 100644 index 000000000..68cab69f6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/theme/utils.js @@ -0,0 +1,75 @@ +const { RGB_Linear_Shade, RGB_Alpha_Shade } = require('./colorUtils') + +exports.createThemeColors = (rgbaColor, fontRgbaColor, isDark, isDarkFont) => { + 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.35 : 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, ...createFontColors(fontRgbaColor, isDark, isDarkFont) } +} + +const createFontColors = (rgbaColor, isDark, isDarkFont) => { + // rgb(238, 238, 238) + // let prec = 'rgb(255, 255, 255)' + rgbaColor ??= isDark ? 'rgb(229, 229, 229)' : 'rgb(33, 33, 33)' + if (isDark) return createFontDarkColors(rgbaColor, isDarkFont) + + let colors = { + '--color-1000': rgbaColor, + } + let step = (isDarkFont ? 0.02 : 0.05) * (isDark ? -1 : 1) + for (let i = 1; i < 21; i += 1) { + colors[`--color-${String(1000 - 50 * i).padStart(3, '0')}`] = RGB_Linear_Shade(step * i, rgbaColor) + } + // console.log(colors) + return colors +} + +const createFontDarkColors = (rgbaColor, isDarkFont) => { + // rgb(238, 238, 238) + // let prec = 'rgb(255, 255, 255)' + + let colors = { + '--color-1000': rgbaColor, + } + const step = isDarkFont ? -0.015 : -0.05 + let preColor = rgbaColor + for (let i = 1; i < 21; i += 1) { + preColor = RGB_Linear_Shade(step, preColor) + colors[`--color-${String(1000 - 50 * i).padStart(3, '0')}`] = preColor + } + + // console.log(colors) + return colors +} + +// console.log(createFontColors('rgb(33, 33, 33)', false)) +// console.log(createFontColors('rgb(255, 255, 255)', true)) + +// console.log(createFontDarkColors('rgb(255, 255, 255)')) + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/tsconfig.json b/plugins/lx-music-ztools/vendor/lx/src/common/tsconfig.json new file mode 100644 index 000000000..f77c1c0f4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "typeRoots": [ + "./types" + ], + "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ + "@common/*": ["./*"], + }, + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/app_setting.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/app_setting.d.ts new file mode 100644 index 000000000..14ca61afc --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/app_setting.d.ts @@ -0,0 +1,696 @@ +import type { I18n } from '../../lang/i18n' + +declare global { + + 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': I18n['locale'] | 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' + + /** + * 播放栏进度条样式 + */ + 'common.playBarProgressStyle': 'mini' | 'full' | 'middle' + + /** + * 启用透明窗口 + */ + 'common.transparentWindow': boolean + + /** + * 尝试自动更新 + */ + 'common.tryAutoUpdate': boolean + + /** + * 更新版本后是否显示变更日志 + */ + 'common.showChangeLog': boolean + + /** + * 启动时自动播放歌曲 + */ + 'player.startupAutoPlay': boolean + + /** + * 切歌模式 + */ + 'player.togglePlayMethod': 'listLoop' | 'random' | 'list' | 'singleLoop' | 'none' + + /** + * 优先播放的音质 + */ + 'player.playQuality': LX.Quality + + /** + * 是否显示任务栏进度条 + */ + 'player.isShowTaskProgess': boolean + + + /** + * 是否将歌词显示在状态栏 + */ + 'player.isShowStatusBarLyric': boolean + + /** + * 音量大小 + */ + 'player.volume': number + + /** + * 播放歌曲时是否阻止电脑休眠 + */ + 'player.powerSaveBlocker': boolean + + /** + * 是否静音 + */ + 'player.isMute': boolean + + /** + * 播放速率 + */ + 'player.playbackRate': number + + /** + * 是否自动调整音频的音高以补偿对播放速率设置所做的更改 + */ + 'player.preservesPitch': boolean + + /** + * 使用设备能处理的最大声道数输出音频 + */ + 'player.isMaxOutputChannelCount': boolean + + /** + * 音频输出设备id + */ + 'player.mediaDeviceId': string + + /** + * 是否在音频输出设备更改时暂停播放 + */ + 'player.isMediaDeviceRemovedStopPlay': boolean + + /** + * 是否显示歌词翻译 + */ + 'player.isShowLyricTranslation': boolean + + /** + * 是否显示歌词罗马音 + */ + 'player.isShowLyricRoma': boolean + + /** + * 是否调换翻译歌词与罗马音歌词位置 + */ + 'player.isSwapLyricTranslationAndRoma': boolean + + /** + * 是否将歌词从简体转换为繁体 + */ + 'player.isS2t': boolean + + /** + * 是否播放卡拉OK歌词 + */ + 'player.isPlayLxlrc': boolean + + /** + * 启动软件时是否恢复上次播放进度 + */ + 'player.isSavePlayTime': boolean + + /** + * 是否启用音频可视化 + */ + 'player.audioVisualization': boolean + + /** + * 定时暂停播放-是否等待歌曲播放完毕再暂停 + */ + 'player.waitPlayEndStop': boolean + + /** + * 定时暂停播放-倒计时时间 + */ + 'player.waitPlayEndStopTime': string + + /** + * 环境音效文件名 + */ + 'player.soundEffect.convolution.fileName': string | null + + /** + * 环境音效原始输出增益 + */ + 'player.soundEffect.convolution.mainGain': number + + /** + * 环境音效输出增益 + */ + 'player.soundEffect.convolution.sendGain': number + + /** + * 均衡器 31hz 值 + */ + 'player.soundEffect.biquadFilter.hz31': number + + /** + * 均衡器 62hz 值 + */ + 'player.soundEffect.biquadFilter.hz62': number + + /** + * 均衡器 125hz 值 + */ + 'player.soundEffect.biquadFilter.hz125': number + + /** + * 均衡器 250hz 值 + */ + 'player.soundEffect.biquadFilter.hz250': number + + /** + * 均衡器 500hz 值 + */ + 'player.soundEffect.biquadFilter.hz500': number + + /** + * 均衡器 1000hz 值 + */ + 'player.soundEffect.biquadFilter.hz1000': number + + /** + * 均衡器 2000hz 值 + */ + 'player.soundEffect.biquadFilter.hz2000': number + + /** + * 均衡器 4000hz 值 + */ + 'player.soundEffect.biquadFilter.hz4000': number + + /** + * 均衡器 8000hz 值 + */ + 'player.soundEffect.biquadFilter.hz8000': number + + /** + * 均衡器 16000hz 值 + */ + 'player.soundEffect.biquadFilter.hz16000': number + + /** + * 3D立体环绕是否启用 + */ + 'player.soundEffect.panner.enable': boolean + + /** + * 3D立体环绕声音距离 + */ + 'player.soundEffect.panner.soundR': number + + /** + * 3D立体环绕速度 + */ + 'player.soundEffect.panner.speed': number + + /** + * 升降声调 + */ + 'player.soundEffect.pitchShifter.playbackRate': number + + /** + * 是否启用音频加载失败时自动切歌 + */ + 'player.autoSkipOnError': boolean + + /** + * 点击相同列表内的歌曲切歌时是否清空已播放列表(随机模式下列表内所有歌曲会重新参与随机) + */ + 'player.isAutoCleanPlayedList': boolean + + /** + * 播放详情页-是否缩放当前播放的歌词行 + */ + 'playDetail.isZoomActiveLrc': boolean + + /** + * 播放详情页-是否允许通过歌词调整播放进度 + */ + 'playDetail.isShowLyricProgressSetting': boolean + + /** + * 播放详情页-歌词字体大小 + */ + 'playDetail.style.fontSize': number + + /** + * 播放详情页-歌词对齐方式 + */ + 'playDetail.style.align': 'center' | 'left' | 'right' + + /** + * 播放详情页-是否延迟桌面歌词滚动 + */ + 'playDetail.isDelayScroll': boolean + + + /** + * 是否启用桌面歌词 + */ + 'desktopLyric.enable': boolean + + /** + * 是否锁定桌面歌词 + */ + 'desktopLyric.isLock': boolean + + /** + * 是在置顶桌面 + */ + 'desktopLyric.isAlwaysOnTop': boolean + + /** + * 是否自动刷新歌词置顶 + */ + 'desktopLyric.isAlwaysOnTopLoop': boolean + + /** + * 是否将歌词进程显示在任务栏 + */ + 'desktopLyric.isShowTaskbar': boolean + + /** + * 是否启用音频可视化 + */ + 'desktopLyric.audioVisualization': boolean + + /** + * 是否在全屏时隐藏歌词 + */ + 'desktopLyric.fullscreenHide': boolean + + /** + * 是否在暂停时隐藏歌词 + */ + 'desktopLyric.pauseHide': boolean + + /** + * 桌面歌词窗口宽度 + */ + 'desktopLyric.width': number + + /** + * 桌面歌词窗口高度 + */ + 'desktopLyric.height': number + + /** + * 桌面歌词窗口x坐标 + */ + 'desktopLyric.x': number | null + + /** + * 桌面歌词窗口y坐标 + */ + 'desktopLyric.y': number | null + + /** + * 是否允许桌面歌词窗口拖出主屏幕之外 + */ + 'desktopLyric.isLockScreen': boolean + + /** + * 是否延迟桌面歌词滚动 + */ + 'desktopLyric.isDelayScroll': boolean + + /** + * 歌词滚动位置 + */ + 'desktopLyric.scrollAlign': 'top' | 'center' + + /** + * 是否在鼠标划过桌面歌词窗口时降低歌词透明度 + */ + 'desktopLyric.isHoverHide': boolean + + /** + * 歌词方向 + */ + 'desktopLyric.direction': 'horizontal' | 'vertical' + + /** + * 歌词对齐方式 + */ + 'desktopLyric.style.align': 'center' | 'left' | 'right' + + /** + * 桌面歌词字体 + */ + 'desktopLyric.style.font': string + + /** + * 桌面歌词字体大小 + */ + 'desktopLyric.style.fontSize': number + + /** + * 歌词间距大小 + */ + 'desktopLyric.style.lineGap': 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 + + /** + * 是否加粗逐字歌词字体 + */ + 'desktopLyric.style.isFontWeightFont': boolean + + /** + * 是否加粗逐行歌词字体 + */ + 'desktopLyric.style.isFontWeightLine': boolean + + /** + * 是否加粗翻译、罗马音字体 + */ + 'desktopLyric.style.isFontWeightExtended': boolean + + /** + * 是否启用双击列表里的歌曲时自动切换到当前列表播放(仅对歌单、排行榜有效) + */ + 'list.isClickPlayList': boolean + + /** + * 是否显示歌曲来源(仅对我的列表有效) + */ + 'list.isShowSource': boolean + + /** + * 是否自动恢复列表滚动位置(仅对我的列表有效) + */ + 'list.isSaveScrollLocation': boolean + + /** + * 添加歌曲到我的列表时的方式 + */ + 'list.addMusicLocationType': LX.AddMusicLocationType + + /** + * 是否显示列表操作按钮列 + */ + 'list.actionButtonsVisible': boolean + + /** + * 是否启用下载功能 + */ + 'download.enable': boolean + + /** + * 按列表名分组保存 + */ + 'download.isSavePathGroupByListName': boolean + + /** + * 下载路径 + */ + 'download.savePath': string + + /** + * 文件命名方式 + */ + 'download.fileName': '歌名 - 歌手' | '歌手 - 歌名' | '歌名' + + /** + * 最大并发下载数 + */ + 'download.maxDownloadNum': number + + /** + * 存在同名文件时跳过下载 + */ + 'download.skipExistFile': boolean + + /** + * 是否下载lrc文件 + */ + 'download.isDownloadLrc': boolean + + /** + * 是否在下载 lx 歌词 + */ + 'download.isDownloadLxLrc': boolean + + /** + * 是否下载翻译歌词文件 + */ + 'download.isDownloadTLrc': boolean + + /** + * 是否下载罗马音歌词文件 + */ + 'download.isDownloadRLrc': boolean + + /** + * 保存lrc时的文本编码格式 + */ + 'download.lrcFormat': 'utf8' | 'gbk' + + /** + * 是否在音频文件中嵌入歌曲封面 + */ + 'download.isEmbedPic': boolean + + /** + * 是否在音频文件中嵌入 lx 歌词 + */ + 'download.isEmbedLyricLx': boolean + + /** + * 是否在音频文件中嵌入歌词 + */ + 'download.isEmbedLyric': boolean + + /** + * 是否在音频文件中嵌入翻译歌词 + */ + 'download.isEmbedLyricT': boolean + + /** + * 是否在音频文件中嵌入罗马音歌词 + */ + 'download.isEmbedLyricR': 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 + + /** + * 是否启用托盘 + */ + 'tray.enable': boolean + + /** + * 是否关闭时是否最小化到托盘 + */ + // 'tray.isToTray': boolean + + /** + * 托盘主题id + */ + 'tray.themeId': number + + /** + * 同步服务模式 + */ + 'sync.mode': 'server' | 'client' + + /** + * 是否启用同步服务 + */ + 'sync.enable': boolean + + /** + * 同步服务端口号 + */ + 'sync.server.port': '23332' | string + + /** + * 最大备份快照数 + */ + 'sync.server.maxSsnapshotNum': number + + /** + * 同步服务地址 + */ + 'sync.client.host': string + + + /** + * 是否启用开放API服务 + */ + 'openAPI.enable': boolean + + /** + * API服务端口号 + */ + 'openAPI.port': '23330' | string + + /** + * 是否绑定到局域网 + */ + 'openAPI.bindLan': boolean + + /** + * 是否在离开搜索界面时自动清空搜索框 + */ + 'odc.isAutoClearSearchInput': boolean + + /** + * 是否在离开搜索界面时自动清空搜索结果列表 + */ + 'odc.isAutoClearSearchList': boolean + } + } + +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/common.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/common.d.ts new file mode 100644 index 000000000..484867a17 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/common.d.ts @@ -0,0 +1,115 @@ +// 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=";*.google.com;*foo.com;1.2.3.4:5678"(与-proxy-server一起使用才有效,v1.17.0起新增)。注:此设置对应用内接口请求无效 + */ + 'proxy-bypass-list'?: string + + /** + * 启动时播放指定列表的音乐 + */ + play?: string + + /** + * 启动后最小化到系统托盘 + */ + hidden?: boolean + + [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> + + 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: Record + } + interface HotKeyConfigAll { + local: HotKeyConfig + global: HotKeyConfig + } + interface RegisterKeyInfo { + key: string + info: HotKey + } + type HotKeyState = Map + interface HotKeyActionWrap { + 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 + } + + type UpdateStatus = 'downloaded' | 'downloading' | 'error' | 'checking' | 'idle' + interface VersionInfo { + version: string + desc: string + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/config_files.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/config_files.d.ts new file mode 100644 index 000000000..b61c18806 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/config_files.d.ts @@ -0,0 +1,9 @@ +declare namespace LX { + namespace ConfigFile { + interface MyListInfoPart { + type: 'playListPart_v2' + data: LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull + } + + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/desktop_lyric.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/desktop_lyric.d.ts new file mode 100644 index 000000000..81ebc87cc --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/desktop_lyric.d.ts @@ -0,0 +1,92 @@ +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.isShowTaskbar': LX.AppSetting['desktopLyric.isShowTaskbar'] + 'desktopLyric.pauseHide': LX.AppSetting['desktopLyric.pauseHide'] + '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.scrollAlign': LX.AppSetting['desktopLyric.scrollAlign'] + '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.lineGap': LX.AppSetting['desktopLyric.style.lineGap'] + '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.isFontWeightFont': LX.AppSetting['desktopLyric.style.isFontWeightFont'] + 'desktopLyric.style.isFontWeightLine': LX.AppSetting['desktopLyric.style.isFontWeightLine'] + 'desktopLyric.style.isFontWeightExtended': LX.AppSetting['desktopLyric.style.isFontWeightExtended'] + '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.isSwapLyricTranslationAndRoma': LX.AppSetting['player.isSwapLyricTranslationAndRoma'] + 'player.isPlayLxlrc': LX.AppSetting['player.isPlayLxlrc'] + 'player.playbackRate': LX.AppSetting['player.playbackRate'] + } + + type WinMainActions = 'get_info' | 'get_status' | 'get_analyser_data_array' + + interface LyricActionBase { + action: A + } + interface LyricActionData extends LyricActionBase { + data: D + } + type LyricAction = D extends undefined ? LyricActionBase : LyricActionData + + 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_playbackRate', number> + | LyricAction<'set_play', number> + | LyricAction<'set_pause'> + | LyricAction<'set_stop'> + | LyricAction<'send_analyser_data_array', Uint8Array> + + + interface NewBounds { + x: number + y: number + w: number + h: number + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/dislike_list.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/dislike_list.d.ts new file mode 100644 index 000000000..153b83381 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/dislike_list.d.ts @@ -0,0 +1,37 @@ + + +declare namespace LX { + namespace Dislike { + // interface ListItemMusicText { + // id?: string + // // type: 'music' + // name: string | null + // singer: string | null + // } + // interface ListItemMusic { + // id?: number + // type: 'musicId' + // musicId: string + // meta: LX.Music.MusicInfo + // } + // type ListItem = ListItemMusicText + // type ListItem = string + // type ListItem = ListItemMusic | ListItemMusicText + + interface DislikeMusicInfo { + name: string + singer: string + } + + type DislikeRules = string + + interface DislikeInfo { + // musicIds: Set + names: Set + musicNames: Set + singerNames: Set + // list: LX.Dislike.ListItem[] + rules: DislikeRules + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/dislike_list_sync.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/dislike_list_sync.d.ts new file mode 100644 index 000000000..5a0809967 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/dislike_list_sync.d.ts @@ -0,0 +1,30 @@ +declare namespace LX { + + namespace Sync { + namespace Dislike { + interface ListInfo { + lastSyncDate?: number + snapshotKey: string + } + + interface SyncActionBase { + action: A + } + interface SyncActionData extends SyncActionBase { + data: D + } + type SyncAction = D extends undefined ? SyncActionBase : SyncActionData + type ActionList = SyncAction<'dislike_data_overwrite', LX.Dislike.DislikeRules> + | SyncAction<'dislike_music_add', LX.Dislike.DislikeMusicInfo[]> + | SyncAction<'dislike_music_clear'> + + type SyncMode = 'merge_local_remote' + | 'merge_remote_local' + | 'overwrite_local_remote' + | 'overwrite_remote_local' + // | 'none' + | 'cancel' + } + + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/download_list.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/download_list.d.ts new file mode 100644 index 000000000..ba0f11ed9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/download_list.d.ts @@ -0,0 +1,72 @@ +import { type Message } from '@root/lang' + +// interface DownloadList { + +// } + + +declare global { + 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 + writeQueue: number + } + + interface DownloadTaskActionBase { + action: A + } + interface DownloadTaskActionData extends DownloadTaskActionBase { + data: D + } + type DownloadTaskAction = D extends undefined ? DownloadTaskActionBase : DownloadTaskActionData + + type DownloadTaskActions = DownloadTaskAction<'start'> + | DownloadTaskAction<'complete'> + | DownloadTaskAction<'refreshUrl'> + | DownloadTaskAction<'statusText', string> + | DownloadTaskAction<'progress', ProgressInfo> + | DownloadTaskAction<'error', { + error?: keyof Message + message?: string + }> + + interface ListItem { + id: string + isComplate: boolean + status: DownloadTaskStatus + statusText: string + downloaded: number + total: number + progress: number + speed: string + writeQueue: number + metadata: { + musicInfo: LX.Music.MusicInfoOnline + url: string | null + quality: LX.Quality + ext: FileExt + fileName: string + filePath: string + listId?: string + } + } + + interface saveDownloadMusicInfo { + list: ListItem[] + addMusicLocationType: LX.AddMusicLocationType + } + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/ipc_main.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/ipc_main.d.ts new file mode 100644 index 000000000..1bbee4d22 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/ipc_main.d.ts @@ -0,0 +1,24 @@ +declare namespace LX { + interface IpcMainEvent { + event: Electron.IpcMainEvent + } + interface IpcMainEventParams { + event: Electron.IpcMainEvent + params: T + } + type IpcMainEventListener = (params: LX.IpcMainEvent) => void + type IpcMainEventListenerParams = (params: LX.IpcMainEventParams) => void + + interface IpcMainInvokeEvent { + event: Electron.IpcMainInvokeEvent + } + interface IpcMainInvokeEventParams { + event: Electron.IpcMainInvokeEvent + params: T + } + + type IpcMainInvokeEventListener = (params: LX.IpcMainInvokeEvent) => Promise + type IpcMainInvokeEventListenerParams = (params: LX.IpcMainInvokeEventParams) => Promise + type IpcMainInvokeEventListenerValue = (params: LX.IpcMainInvokeEvent) => Promise + type IpcMainInvokeEventListenerParamsValue = (params: LX.IpcMainInvokeEventParams) => Promise +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/ipc_renderer.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/ipc_renderer.d.ts new file mode 100644 index 000000000..067ce6885 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/ipc_renderer.d.ts @@ -0,0 +1,11 @@ +declare namespace LX { + interface IpcRendererEvent { + event: Electron.IpcRendererEvent + } + interface IpcRendererEventParams { + event: Electron.IpcRendererEvent + params: T + } + type IpcRendererEventListener = (params: LX.IpcRendererEvent) => any + type IpcRendererEventListenerParams = (params: LX.IpcRendererEventParams) => any +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/list.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/list.d.ts new file mode 100644 index 000000000..98a6fe9cd --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/list.d.ts @@ -0,0 +1,143 @@ +declare namespace LX { + namespace List { + interface UserListInfo { + id: string + name: string + // list: LX.Music.MusicInfo[] + source?: LX.OnlineSource + sourceListId?: string + // position?: number + locationUpdateTime: number | null + } + + interface MyDefaultListInfo { + id: 'default' + name: 'list__name_default' + // name: '试听列表' + // list: LX.Music.MusicInfo[] + } + + interface MyLoveListInfo { + id: 'love' + name: 'list__name_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 + type ListUpdateInfo = Record + + type ListSaveType = 'myList' | 'downloadList' + type ListSaveInfo = { + type: 'myList' + data: Partial + } | { + type: 'downloadList' + data: LX.Download.ListItem[] + } + + + type ListActionDataOverwrite = MakeOptional + 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[] + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/list_sync.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/list_sync.d.ts new file mode 100644 index 000000000..36593e47f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/list_sync.d.ts @@ -0,0 +1,34 @@ +declare namespace LX { + + namespace Sync { + namespace List { + interface ListInfo { + lastSyncDate?: number + snapshotKey: string + } + + type ActionList = LX.Sync.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> + + type ListData = Omit + type SyncMode = 'merge_local_remote' + | 'merge_remote_local' + | 'overwrite_local_remote' + | 'overwrite_remote_local' + | 'overwrite_local_remote_full' + | 'overwrite_remote_local_full' + // | 'none' + | 'cancel' + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/music.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/music.d.ts new file mode 100644 index 000000000..efd2b1e36 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/music.d.ts @@ -0,0 +1,124 @@ +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 = Partial> + type _MusicQualityTypeKg = Partial> + + + interface MusicInfoMetaBase { + songId: string | number // 歌曲ID,mg源为copyrightId,local为文件路径 + albumName: string // 歌曲专辑名称 + picUrl?: string | null // 歌曲图片链接 + toggleMusicInfo?: MusicInfoOnline | 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 { + 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 + albumAudioId?: string // 专辑内歌曲ID(MixSongID),用于歌词精确搜索 + } + interface MusicInfo_kg extends MusicInfoBase<'kg'> { + meta: MusicInfoMeta_kg + } + + interface MusicInfoMeta_tx extends MusicInfoMeta_online { + strMediaMid: string // 歌曲strMediaMid + id?: number // 歌曲songId + 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[] + } + + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/music_metadata.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/music_metadata.d.ts new file mode 100644 index 000000000..99869d2a9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/music_metadata.d.ts @@ -0,0 +1,11 @@ +import { + type IAudioMetadata as iAudioMetadata, +} from 'music-metadata' + +declare global { + namespace LX { + namespace MusicMetadataModule { + type IAudioMetadata = iAudioMetadata + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/open_api.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/open_api.d.ts new file mode 100644 index 000000000..1fb00c693 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/open_api.d.ts @@ -0,0 +1,26 @@ +declare namespace LX { + namespace OpenAPI { + interface Status { + status: boolean + message: string + address: string + } + interface EnableServer { + enable: boolean + port: string + bindLan: boolean + } + + interface ActionBase { + action: A + } + interface ActionData extends ActionBase { + data: D + } + type Action = D extends undefined ? ActionBase : ActionData + + type Actions = Action<'status'> + | Action<'enable', EnableServer> + + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/player.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/player.d.ts new file mode 100644 index 000000000..81e4b4285 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/player.d.ts @@ -0,0 +1,42 @@ +declare namespace LX { + namespace Player { + interface ProgressBarOptions { + progress: number + mode?: Electron.ProgressBarOptions['mode'] + } + + type StatusButtonActions = 'unCollect' + | 'collect' + | 'prev' + | 'pause' + | 'play' + | 'next' + | 'seek' + | 'volume' + | 'mute' + + interface LyricInfo extends LX.Music.LyricInfo { + rawlrcInfo: LX.Music.LyricInfo + } + + interface Status { + status: 'playing' | 'paused' | 'error' | 'stoped' + name: string + singer: string + albumName: string + picUrl: string + progress: number + duration: number + playbackRate: number + lyricLineText: string + lyricLineAllText: string + lyric: string + tlyric: string + rlyric: string + lxlyric: string + collect: boolean + volume: number + mute: boolean + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/shims_vue.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/shims_vue.d.ts new file mode 100644 index 000000000..d621a7098 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/shims_vue.d.ts @@ -0,0 +1,10 @@ +// declare module '*.vue' { +// import { App } from 'vue' +// export default App.Component +// } + +declare module '*.vue' { + import { type Component } from 'vue' + const component: Component + export default component +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/sound_effect.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/sound_effect.d.ts new file mode 100644 index 000000000..90f9f815e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/sound_effect.d.ts @@ -0,0 +1,30 @@ +declare namespace LX { + namespace SoundEffect { + interface EQPreset { + id: string + name: string + hz31: number + hz62: number + hz125: number + hz250: number + hz500: number + hz1000: number + hz2000: number + hz4000: number + hz8000: number + hz16000: number + } + interface ConvolutionPreset { + id: string + name: string + source: string + mainGain: number + sendGain: number + } + // interface PitchShifterPreset { + // id: string + // name: string + // playbackRate: number + // } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/sync.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/sync.d.ts new file mode 100644 index 000000000..67b981c33 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/sync.d.ts @@ -0,0 +1,85 @@ +declare namespace LX { + namespace Sync { + + interface EnableServer { + enable: boolean + port: string + } + interface EnableClient { + enable: boolean + host: string + authCode?: string + } + + interface SyncActionBase { + action: A + } + interface SyncActionData extends SyncActionBase { + data: D + } + type SyncAction = D extends undefined ? SyncActionBase : SyncActionData + + + interface ModeTypes { + list: LX.Sync.List.SyncMode + dislike: LX.Sync.Dislike.SyncMode + } + + type ModeType = { [K in keyof ModeTypes]: { type: K, mode: ModeTypes[K] } }[keyof ModeTypes] + + type SyncMainWindowActions = SyncAction<'select_mode', { deviceName: string, type: keyof ModeTypes }> + | SyncAction<'close_select_mode'> + | SyncAction<'client_status', ClientStatus> + | SyncAction<'server_status', ServerStatus> + + type SyncServiceActions = SyncAction<'select_mode', ModeType> + | SyncAction<'get_server_status'> + | SyncAction<'get_client_status'> + | SyncAction<'generate_code'> + | SyncAction<'enable_server', EnableServer> + | SyncAction<'enable_client', EnableClient> + + type ServerDevices = ServerKeyInfo[] + + interface ServerStatus { + status: boolean + message: string + address: string[] + code: string + devices: ServerKeyInfo[] + } + + interface ClientStatus { + status: boolean + message: string + address: string[] + } + + interface ClientKeyInfo { + clientId: string + key: string + serverName: string + } + + interface ServerKeyInfo { + clientId: string + key: string + deviceName: string + lastConnectDate?: number + isMobile: boolean + } + + interface ListConfig { + skipSnapshot: boolean + } + interface DislikeConfig { + skipSnapshot: boolean + } + type ServerType = 'desktop-app' | 'server' + interface EnabledFeatures { + list?: false | ListConfig + dislike?: false | DislikeConfig + } + type SupportedFeatures = Partial<{ [k in keyof EnabledFeatures]: number }> + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/theme.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/theme.d.ts new file mode 100644 index 000000000..c91498395 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/theme.d.ts @@ -0,0 +1,306 @@ +declare namespace LX { + + interface ThemeColors { + '--color-000': string + '--color-050': string + '--color-100': string + '--color-150': string + '--color-200': string + '--color-250': string + '--color-300': string + '--color-350': string + '--color-400': string + '--color-450': string + '--color-500': string + '--color-550': string + '--color-600': string + '--color-650': string + '--color-700': string + '--color-750': string + '--color-800': string + '--color-850': string + '--color-900': string + '--color-950': string + '--color-1000': 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 + isDarkFont: 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 + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/user_api.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/user_api.d.ts new file mode 100644 index 000000000..841bd0997 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/user_api.d.ts @@ -0,0 +1,61 @@ +declare namespace LX { + namespace UserApi { + type UserApiSourceInfoType = 'music' + type UserApiSourceInfoActions = 'musicUrl' | 'lyric' | 'pic' + + interface UserApiSourceInfo { + name: string + type: UserApiSourceInfoType + actions: UserApiSourceInfoActions[] + qualitys: LX.Quality[] + } + + type UserApiSources = Record + + + interface UserApiInfoFull { + id: string + name: string + description: string + script: string + allowShowUpdateAlert: boolean + author?: string + homepage?: string + version?: string + sources?: UserApiSources + } + + type UserApiInfo = Omit + + 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[] + } + + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/types/utils.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/types/utils.d.ts new file mode 100644 index 000000000..e0afc0eec --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/types/utils.d.ts @@ -0,0 +1,22 @@ +type MakeOptional = Omit & Partial> + +type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +} + +type Modify = Omit & R + +// type UndefinedOrNever = undefined +type Actions = { + [U in T as U['action']]: 'data' extends keyof U ? U['data'] : undefined +} + +type WarpPromiseValue = T extends ((...args: infer P) => Promise) + ? ((...args: P) => Promise) + : T extends ((...args: infer P2) => infer R2) + ? ((...args: P2) => Promise) + : Promise + +type WarpPromiseRecord> = { + [K in keyof T]: WarpPromiseValue +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/common.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/common.ts new file mode 100644 index 000000000..b5fbbfe8e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/common.ts @@ -0,0 +1,235 @@ +// 非业务工具方法 + +import { pathToFileURL } from 'url' +/** + * 获取两个数之间的随机整数,大于等于min,小于max + * @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', 'KiB', 'MiB', 'GiB', 'TiB'] + let number = Math.floor(Math.log(size) / Math.log(1024)) + return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}` +} + +/** + * 将字符串、时间戳等格式转成时间对象 + * @param date 时间 + * @returns 时间对象或空字符串 + */ +export const toDateObj = (date: any): Date | '' => { + // console.log(date) + if (!date) return '' + switch (typeof date) { + case 'string': + if (!date.includes('T')) date = date.split('.')[0].replace(/-/g, '/') + // eslint-disable-next-line no-fallthrough + case 'number': + date = new Date(date) + // eslint-disable-next-line no-fallthrough + case 'object': + break + default: return '' + } + return date +} + +const numFix = (n: number): string => n < 10 ? (`0${n}`) : n.toString() +/** + * 时间格式化 + * @param _date 时间 + * @param format Y-M-D h:m:s Y年 M月 D日 h时 m分 s秒 + */ +export const dateFormat = (_date: any, format = 'Y-M-D h:m:s') => { + // console.log(date) + const date = toDateObj(_date) + if (!date) return '' + return format + .replace('Y', date.getFullYear().toString()) + .replace('M', numFix(date.getMonth() + 1)) + .replace('D', numFix(date.getDate())) + .replace('h', numFix(date.getHours())) + .replace('m', numFix(date.getMinutes())) + .replace('s', 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) +} + + +export const isUrl = (path: string) => /https?:\/\//.test(path) + +// 解析URL参数为对象 +export const parseUrlParams = (str: string): Record => { + const params: Record = {} + 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(fn: (...args: Args) => void | Promise, 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(fn: (...args: Args) => void | Promise, 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: T }>, data: { num: number, data: T }) => { + 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) +} + +export const encodePath = (path: string) => { + return pathToFileURL(path).href +} + + +export const arrPush = (list: T[], newList: T[]) => { + for (let i = 0; i * 1000 < newList.length; i++) { + list.push(...newList.slice(i * 1000, (i + 1) * 1000)) + } + return list +} + +export const arrUnshift = (list: T[], newList: T[]) => { + for (let i = 0; i * 1000 < newList.length; i++) { + list.splice(i * 1000, 0, ...newList.slice(i * 1000, (i + 1) * 1000)) + } + return list +} + +export const arrPushByPosition = (list: T[], newList: T[], position: number) => { + for (let i = 0; i * 1000 < newList.length; i++) { + list.splice(position + i * 1000, 0, ...newList.slice(i * 1000, (i + 1) * 1000)) + } + return list +} + + +// https://stackoverflow.com/a/2450976 +export const arrShuffle = (array: T[]) => { + let currentIndex = array.length + let randomIndex + + // While there remain elements to shuffle. + while (currentIndex != 0) { + // Pick a remaining element. + randomIndex = Math.floor(Math.random() * currentIndex) + currentIndex--; + + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], array[currentIndex]] + } + + return array +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/Downloader.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/Downloader.ts new file mode 100644 index 000000000..d382ea1d4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/Downloader.ts @@ -0,0 +1,422 @@ +import fs from 'fs' +import path from 'path' +import { EventEmitter } from 'events' +import { performance } from 'perf_hooks' +import { STATUS } from './util' +import type http from 'http' +import { request, type Options as RequestOptions } from './request' + +export interface Options { + forceResume: boolean + timeout: number + requestOptions: RequestOptions +} + +const defaultChunkInfo = { + path: '', + startByte: '0', + endByte: '', +} + +const defaultRequestOptions: Options['requestOptions'] = { + method: 'get', + headers: {}, +} +const defaultOptions: Options = { + forceResume: true, + timeout: 20_000, + requestOptions: { ...defaultRequestOptions }, +} + +class Task extends EventEmitter { + 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 + maxRedirectNum = 2 + private redirectNum = 0 + private dataWriteQueueLength = 0 + private closeWaiting = false + private timeout: null | NodeJS.Timeout = null + + + constructor(url: string, savePath: string, filename: string, options: Partial = {}) { + super() + + this.resumeLastChunk = null + this.downloadUrl = url + this.chunkInfo = Object.assign({}, defaultChunkInfo, { + path: path.join(savePath, filename), + startByte: '0', + }) + // if (!this.chunkInfo.endByte) this.chunkInfo.endByte = '' + + this.options = Object.assign({}, defaultOptions, options) + this.requestOptions = Object.assign({}, defaultRequestOptions, this.options.requestOptions || {}) + this.requestOptions.headers = this.requestOptions.headers ? { ...this.requestOptions.headers } : {} + + this.status = STATUS.idle + } + + async __init() { + const { path, startByte, endByte } = this.chunkInfo + this.redirectNum = 0 + this.progress.downloaded = 0 + this.progress.progress = 0 + this.progress.speed = 0 + this.dataWriteQueueLength = 0 + this.closeWaiting = false + this.__clearTimeout() + this.__startTimeout() + if (startByte) this.requestOptions.headers!.range = `bytes=${startByte}-${endByte}` + + if (!path) return + return new Promise((resolve, reject) => { + fs.stat(path, (errStat, stats) => { + if (errStat) { + // console.log(errStat.code) + if (errStat.code !== 'ENOENT') { + this.__handleError(errStat) + reject(errStat) + return + } + } else if (stats.size >= 10) { + fs.open(path, 'r', (errOpen, fd) => { + if (errOpen) { + this.__handleError(errOpen) + reject(errOpen) + return + } + fs.read(fd, Buffer.alloc(10), 0, 10, stats.size - 10, (errRead, bytesRead, buffer) => { + if (errRead) { + this.__handleError(errRead) + reject(errRead) + return + } + fs.close(fd, errClose => { + if (errClose) { + this.__handleError(errClose) + reject(errClose) + return + } + + // resume download + // console.log(buffer) + this.resumeLastChunk = buffer + this.progress.downloaded = stats.size + this.requestOptions.headers!.range = `bytes=${stats.size - 10}-${endByte || ''}` + resolve() + }) + }) + }) + return + } + resolve() + }) + }) + } + + __httpFetch(url: string, options: Options['requestOptions']) { + // console.log(options) + let redirected = false + this.requestInstance = request(url, options) + .on('response', response => { + if (response.statusCode !== 200 && response.statusCode !== 206) { + if (response.statusCode == 416) { + 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) + }) + return + } + if ((response.statusCode == 301 || response.statusCode == 302) && response.headers.location && this.redirectNum < this.maxRedirectNum) { + console.log('current url:', url) + console.log('redirect to:', response.headers.location) + redirected = true + this.redirectNum++ + const location = response.headers.location + this.__httpFetch(location, options) + return + } + this.status = STATUS.failed + this.emit('fail', response) + this.__clearTimeout() + this.__closeRequest() + void this.__closeWriteStream() + return + } + this.emit('response', response) + try { + this.__initDownload(response) + } catch (error: any) { + this.__handleError(error) + return + } + this.status = STATUS.running + this.__startTimeout() + response + .on('data', this.__handleWriteData.bind(this)) + .on('error', err => { this.__handleError(err) }) + .on('end', () => { + if (response.complete) { + this.__handleComplete() + } else { + // this.__handleError(new Error('The connection was terminated while the message was still being sent')) + void this.stop() + } + }) + }) + .on('error', err => { this.__handleError(err) }) + .on('close', () => { + if (redirected) return + void this.__closeWriteStream() + }) + .end() + } + + __initDownload(response: http.IncomingMessage) { + this.progress.total = response.headers['content-length'] ? parseInt(response.headers['content-length']) : 0 + if (!this.progress.total) { + this.__handleError(new Error('Content length is 0')) + return + } + 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') { + this.__handleError(new Error('The resource cannot be resumed download.')) + return + } + } + this.progress.total += this.progress.downloaded + this.statsEstimate.prevBytes = this.progress.downloaded + if (!this.chunkInfo.path) { + this.__handleError(new Error('Chunk save Path is not set.')) + return + } + this.ws = fs.createWriteStream(this.chunkInfo.path, options) + + this.ws.on('finish', () => { + if (this.closeWaiting) return + void this.__closeWriteStream() + }) + this.ws.on('error', err => { + 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) + }) + }) + } + + __handleComplete() { + if (this.status == STATUS.error) return + this.__clearTimeout() + if (this.progress.progress <= 0) { + this.status = STATUS.error + this.emit('error', new Error('Progress is 0, download failed.')) + return + } + void this.__closeWriteStream().then(() => { + if (this.progress.downloaded == this.progress.total) { + this.status = STATUS.completed + this.emit('completed') + } else { + this.status = STATUS.stopped + this.emit('stop') + } + }) + // console.log('end') + } + + __handleError(error: Error) { + if (this.status == STATUS.error) return + this.status = STATUS.error + this.__clearTimeout() + this.__closeRequest() + void this.__closeWriteStream() + if (error.message == 'aborted') return + this.emit('error', error) + } + + async __closeWriteStream() { + return new Promise((resolve, reject) => { + if (!this.ws) { + resolve() + return + } + // console.log('close write stream') + if (this.closeWaiting || this.dataWriteQueueLength) { + this.closeWaiting ||= true + this.ws.on('close', resolve) + } else { + this.ws.close(err => { + if (err) { + this.status = STATUS.error + this.emit('error', err) + reject(err) + return + } + this.ws = null + resolve() + }) + } + }) + } + + __closeRequest() { + if (!this.requestInstance || this.requestInstance.destroyed) return + // console.log('close request') + this.requestInstance.destroy() + this.requestInstance = null + } + + __handleWriteData(chunk: Buffer) { + if (this.resumeLastChunk) { + const result = this.__handleDiffChunk(chunk) + if (result) chunk = result + else { + void this.__handleStop().finally(() => { + // 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') { + this.__handleError(unlinkErr) + return + } + void this.start() + }) + }) + return + } + } + // console.log('data', chunk) + if (this.status == STATUS.stopped || this.ws == null) { + console.log('cancel write') + return + } + this.dataWriteQueueLength++ + this.__startTimeout() + this.__calculateProgress(chunk.length) + this.ws.write(chunk, err => { + this.dataWriteQueueLength-- + if (this.status == STATUS.running) this.__calculateProgress(0) + if (err) { + console.log(err) + this.__handleError(err) + return + } + if (this.closeWaiting && !this.dataWriteQueueLength) this.ws?.close() + }) + } + + __handleDiffChunk(chunk: Buffer): Buffer | null { + // console.log('diff', chunk) + let resumeLastChunkLen = this.resumeLastChunk!.length + let chunkLen = chunk.length + let isOk + if (chunkLen >= resumeLastChunkLen) { + isOk = chunk.subarray(0, resumeLastChunkLen).toString('hex') === this.resumeLastChunk!.toString('hex') + if (!isOk) return null + + this.resumeLastChunk = null + return chunk.subarray(resumeLastChunkLen) + } else { + isOk = chunk.subarray(0, chunkLen).toString('hex') === this.resumeLastChunk!.subarray(0, chunkLen).toString('hex') + if (!isOk) return null + this.resumeLastChunk = this.resumeLastChunk!.subarray(chunkLen) + return chunk.subarray(chunkLen) + } + } + + async __handleStop() { + this.__clearTimeout() + this.__closeRequest() + return this.__closeWriteStream() + } + + private __clearTimeout() { + if (!this.timeout) return + clearTimeout(this.timeout) + this.timeout = null + } + + private __startTimeout() { + this.__clearTimeout() + this.timeout = setTimeout(() => { + this.__handleError(new Error('download timeout')) + }, this.options.timeout) + } + + __calculateProgress(receivedBytes: number) { + const currentTime = performance.now() + const elaspsedTime = currentTime - this.statsEstimate.time + + const progress = this.progress + progress.downloaded += receivedBytes + progress.progress = progress.total ? (progress.downloaded / progress.total) * 100 : -1 + + + // emit the progress every second or if finished + if ((progress.downloaded === progress.total && this.dataWriteQueueLength == 0) || elaspsedTime > 1000) { + this.statsEstimate.time = currentTime + this.statsEstimate.bytes = progress.downloaded - this.statsEstimate.prevBytes + this.statsEstimate.prevBytes = progress.downloaded + this.emit('progress', { + total: progress.total, + downloaded: progress.downloaded, + progress: progress.progress, + speed: this.statsEstimate.bytes, + writeQueue: this.dataWriteQueueLength, + }) + } + } + + async start() { + 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 || this.status == STATUS.completed) return + this.status = STATUS.stopped + await this.__handleStop() + this.emit('stop') + } + + refreshUrl(url: string) { + this.downloadUrl = url + } + + updateSaveInfo(filePath: string, fileName: string) { + this.chunkInfo.path = path.join(filePath, fileName) + } +} + +export default Task diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/index.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/index.ts new file mode 100644 index 000000000..d6eba61d6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/index.ts @@ -0,0 +1,101 @@ +import Downloader, { type Options as DownloaderOptions } from './Downloader' +import { getRequestAgent } from './util' +import { sizeFormate } from '@common/utils' +import type 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, + writeQueue: stats.writeQueue, + }) + // 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 diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/request.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/request.ts new file mode 100644 index 000000000..e4cc2ba67 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/request.ts @@ -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 + // body?: Record + headers?: Record + 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!) += `${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, callback: HttpCallback) +export function request(url: string, _options: Partial, callback?: HttpCallback) { + let options: Options = { ...defaultOptions, ..._options } + const request = sendRequest(url, options, callback) + if (options.timeout) applyTimeout(request, options.timeout) + return request +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/util.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/util.ts new file mode 100644 index 000000000..51ee3bcb6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/download/util.ts @@ -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 +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/effects/cursor-effects/bubbleCursor.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/effects/cursor-effects/bubbleCursor.js new file mode 100644 index 000000000..084b5fe1e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/effects/cursor-effects/bubbleCursor.js @@ -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 + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/effects/snow.min.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/effects/snow.min.js new file mode 100644 index 000000000..9d0524307 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/effects/snow.min.js @@ -0,0 +1,91 @@ +// ------------------------------------------------------------------------------------------------- +// JavaScript Snow Effect - © Copyright 2020 - Jam-Es.com +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +// Change the following variables to customize the appearance of the particles +let numParticles = window.innerWidth / 50 +let maxSpeed = 2.5 +let minSpeed = 1 +let maxSize = 5 +let minSize = 3 +let maxOpacity = 1 +let minOpacity = 0.2 + +// If the following is true, the canvas resolution will be set to match the full viewport width and height. +// var fixCanvasResolution = true; + +let snow_particles = [] + +let canvas + +window.onload = function() { + canvas = document.createElement('canvas') + canvas.style.position = 'fixed' + canvas.style.top = '0px' + canvas.style.left = '0px' + canvas.style.pointerEvents = 'none' + canvas.style.zIndex = 100 + canvas.width = document.documentElement.clientWidth + canvas.height = document.documentElement.clientHeight + document.body.appendChild(canvas) + + // if (fixCanvasResolution) { + // // canvas.width = window.innerWidth; + // canvas.height = window.innerHeight; + // } + + InitPoints() + + window.requestAnimationFrame(Redraw, 15) + window.addEventListener('resize', this.onWindowResize) +} +// function onWindowResize(e) { +// canvas.width = document.documentElement.clientWidth +// canvas.height = document.documentElement.clientHeight +// } + +function Redraw() { + let ctx = canvas.getContext('2d') + ctx.clearRect(0, 0, canvas.width, canvas.height) + for (let i = 0; i < snow_particles.length; i++) { + let newYPos = snow_particles[i].yPos + snow_particles[i].speed + if (newYPos > window.innerHeight) { + newYPos = getRandomInt(-100, -10) + snow_particles[i].xPos = getRandomInt(0, window.innerWidth) + snow_particles[i].speed = Math.random() * (maxSpeed - minSpeed) + minSpeed + snow_particles[i].opacity = Math.random() * (maxOpacity - minOpacity) + minOpacity + snow_particles[i].size = Math.random() * (maxSize - minSize) + minSize + } + snow_particles[i].yPos = newYPos + ctx.beginPath() + ctx.arc(snow_particles[i].xPos, newYPos, snow_particles[i].size, 0, 2 * Math.PI) + ctx.fillStyle = 'rgba(255, 255, 255, ' + snow_particles[i].opacity + ')' + ctx.fill() + } + window.requestAnimationFrame(Redraw) +} + +function InitPoints() { + for (let i = 0; i < numParticles; i++) { + let startX = getRandomInt(0, window.innerWidth) + let startY = getRandomInt(0, window.innerHeight) + let speed = Math.random() * (maxSpeed - minSpeed) + minSpeed + let opacity = Math.random() * (maxOpacity - minOpacity) + minOpacity + let size = Math.random() * (maxSize - minSize) + minSize + + snow_particles.push({ + xPos: startX, + yPos: startY, + speed, + opacity, + size, + }) + } +} + +function getRandomInt(min, max) { + min = Math.ceil(min) + max = Math.floor(max) + return Math.floor(Math.random() * (max - min)) + min +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/electron.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/electron.ts new file mode 100644 index 000000000..5a3373bbc --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/electron.ts @@ -0,0 +1,44 @@ +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() +} + + +export const encodePath = (path: string) => { + // https://github.com/lyswhut/lx-music-desktop/issues/963 + // https://github.com/lyswhut/lx-music-desktop/issues/1461 + return path.replaceAll('%', '%25').replaceAll('#', '%23') +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/index.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/index.ts new file mode 100644 index 000000000..85a9dd2f6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/index.ts @@ -0,0 +1,48 @@ +import log from 'electron-log/node' +import os from 'node:os' + + +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' + +export const getPlatform = (platform: NodeJS.Platform = process.platform) => { + switch (platform) { + case 'win32': return 'windows' + case 'darwin': return 'mac' + default: return 'linux' + } +} +export const getOSVersion = (): string => { + const release = os.release() + const versionMatch = /^(\d+)/.exec(release) + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return versionMatch?.[1] || release +} + +// 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 = ('' + currentVer).replace(/[^0-9.]/g, fix).split('.') + const targetVerArr: Array = ('' + 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 { + log, +} + +export * from './common' diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/font-player.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/font-player.js new file mode 100644 index 000000000..24fafb7d3 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/font-player.js @@ -0,0 +1,366 @@ +import { getNow, TimeoutTools } from './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, isVertical) => new window.Animation(new window.KeyframeEffect(dom, isVertical + ? [ + { backgroundSize: '100% 0' }, + { backgroundSize: '100% 100%' }, + ] + : [ + { backgroundSize: '0 100%' }, + { backgroundSize: '100% 100%' }, + ], { + duration, + easing: 'linear', +}, +), document.timeline) + + +// https://jsfiddle.net/ceqpnbky/ +// https://jsfiddle.net/ceqpnbky/1/ + +export default class FontPlayer { + constructor({ + time = 0, + rate = 1, + 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._rate = rate + + this.isVertical = isVertical + + this.lineContentClassName = lineContentClassName + this.lineClassName = lineClassName + + 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.lineContent = null + + this.timeoutTools = new TimeoutTools(50) + this.waitPlayTimeout = new TimeoutTools(50) + + this._init() + } + + _init() { + if (this.lyric == null) this.lyric = '' + + this.isLineMode = false + + this.lineContent = document.createElement('div') + this.lineContent.time = this.time + 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 + 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) + // } + + const lineContent = document.createElement('div') + lineContent.className = this.fontLrcClassName + lineContent.textContent = lrc.replace(timeRxpAll, '') + extendedLrcContent.appendChild(lineContent) + } + this._parseLyric() + } + + _parseLyric() { + const fonts = this.lyric.split(fontSplitRxp) + // console.log(fonts) + + this.maxFontNum = fonts.length - 1 + this.fonts = [] + let text + // let lineText = '' + let lrcShadowContent + for (const font of fonts) { + if (!timeRxp.test(font)) return this._handleLineParse() + text = font.replace(timeRxp, '') + const time = parseInt(RegExp.$2) + + const dom = document.createElement('span') + dom.textContent = text + const animation = createAnimation(dom, time / this._rate, this.isVertical) + this.lrcContent.appendChild(dom) + // lineText += text + + if (this.shadowContent) { + lrcShadowContent ??= document.createElement('div') + const shadowDom = document.createElement('span') + shadowDom.textContent = text + lrcShadowContent.appendChild(shadowDom) + } + // dom.style = shadowDom.style = this.fontStyle + // dom.className = shadowDom.className = this.fontClassName + + this.fonts.push({ + text, + startTime: parseInt(RegExp.$1), + time, + dom, + animation, + }) + } + + if (this.shadowContent && lrcShadowContent) { + lrcShadowContent.style = 'position:absolute;top:0;left:0;right:0;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 + 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, + }) + } + + _currentTime() { + return (getNow() - this._performanceTime) * this._rate + this._startTime + } + + _findcurFontNum(curTime, startIndex = 0) { + const length = this.fonts.length + for (let index = startIndex; index < length; index++) if (curTime < this.fonts[index].startTime) return index == 0 ? 0 : index - 1 + return length - 1 + } + + _handlePlayMaxFontNum() { + let curFont = this.fonts[this.curFontNum] + // console.log(curFont.text) + const currentTime = this._currentTime() + const driftTime = currentTime - curFont.startTime + if (currentTime > curFont.startTime + curFont.time) { + this._handlePlayFont(curFont, driftTime / this._rate, true) + this.lineContent.classList.add('played') + this.isPlay = false + this.pause() + } else { + this._handlePlayFont(curFont, driftTime) + } + } + + _handlePlayFont(font, currentTime, toFinishe) { + switch (font.animation.playState) { + case 'finished': + break + case 'idle': + font.dom.style.backgroundSize = '100% 100%' + if (!toFinishe) font.animation.play() + break + default: + if (toFinishe) { + font.animation.cancel() + } else { + font.animation.currentTime = currentTime + font.animation.play() + } + break + } + } + + _handlePlayLine(isPlayed) { + this.isPlay = false + 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) { + if (font.animation.playState == 'running') font.animation.pause() + } + + _refresh() { + this.curFontNum++ + // console.log('curFontNum time', this.fonts[this.curFontNum].time) + if (this.curFontNum >= this.maxFontNum) return this._handlePlayMaxFontNum() + let curFont = this.fonts[this.curFontNum] + // console.log(curFont, nextFont, this.curFontNum, this.maxFontNum) + const currentTime = this._currentTime() + // console.log(curFont.text) + const driftTime = currentTime - curFont.startTime + + // console.log(currentTime, driftTime) + + if (driftTime >= 0 || this.curFontNum == 0) { + let nextFont = this.fonts[this.curFontNum + 1] + const delay = (nextFont.startTime - curFont.startTime - driftTime) / this._rate + if (delay > 0) { + if (this.isPlay) { + this.timeoutTools.start(() => { + if (!this.isPlay) return + this._refresh() + }, delay) + } + this._handlePlayFont(curFont, driftTime) + return + } else { + let newCurLineNum = this._findcurFontNum(currentTime, this.curFontNum + 1) + if (newCurLineNum > this.curFontNum) this.curFontNum = newCurLineNum - 1 + for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true) + this._refresh() + return + } + } else if (this.curFontNum == 0) { + this.curFontNum-- + if (this.isPlay) { + this.waitPlayTimeout.start(() => { + if (!this.isPlay) return + this._refresh() + }, -driftTime) + } + return + } + + this.curFontNum = this._findcurFontNum(currentTime, this.curFontNum) - 1 + for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true) + // this.curFontNum-- + this._refresh() + } + + play(curTime = 0) { + // console.log('play', curTime) + if (!this.fonts.length) return + this.pause() + + if (this.isLineMode) return this._handlePlayLine(true) + this.lineContent.classList.remove('played') + this.isPlay = true + this._performanceTime = getNow() + this._startTime = curTime + + this.curFontNum = this._findcurFontNum(curTime) + + for (let i = this.curFontNum; i > -1; i--) { + this._handlePlayFont(this.fonts[i], 0, true) + } + for (let i = this.curFontNum, len = this.fonts.length; i < len; i++) { + let font = this.fonts[i] + font.animation.cancel() + font.dom.style.backgroundSize = '0 100%' + } + + this.curFontNum-- + + this._refresh() + } + + pause() { + if (!this.isPlay) return + this.isPlay = false + this.timeoutTools.clear() + this.waitPlayTimeout.clear() + this._handlePauseFont(this.fonts[this.curFontNum]) + if (this.curFontNum === this.maxLine) return + const curFontNum = this._findcurFontNum(this._currentTime()) + if (this.curFontNum === curFontNum) return + for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true) + } + + finish() { + this.pause() + if (this.isLineMode) return this._handlePlayLine(true) + this.lineContent.classList.add('played') + + for (const font of this.fonts) { + font.animation.cancel() + font.dom.style.backgroundSize = '100% 100%' + } + this.curFontNum = this.maxFontNum + } + + setPlaybackRate(rate) { + this._rate = rate + if (!this.lines.length) return + if (!this.isPlay) return + this.play(this._currentTime()) + } + + 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%' + } + this.curFontNum = 0 + } +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/index.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/index.js new file mode 100644 index 000000000..72fa25fcf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/index.js @@ -0,0 +1,234 @@ +import LinePlayer from './line-player' +import FontPlayer from './font-player' + +const fontTimeExp = /<(\d+),(\d+)>/g + +export default class Lyric { + constructor({ + lyric = '', + extendedLyrics = [], + offset = 0, + rate = 1, + lineContentClassName = 'line-content', + lineClassName = 'line', + shadowClassName = 'shadow', + fontModeClassName = 'font-mode', + lineModeClassName = 'line-mode', + fontLrcClassName = 'font-lrc', + extendedLrcClassName = 'extended', + activeLineClassName = 'active', + shadowContent = false, + isVertical = false, + onPlay = function(line, text) { }, + onSetLyric = function(lines, offset) { }, + onUpdateLyric = function(lines) { }, + }) { + this.lyric = lyric + this.extendedLyrics = extendedLyrics + this.offset = offset + this.rate = rate + this.onPlay = onPlay + this.onSetLyric = onSetLyric + this.onUpdateLyric = onUpdateLyric + + this.lineContentClassName = lineContentClassName + this.lineClassName = lineClassName + this.shadowClassName = shadowClassName + this.fontModeClassName = fontModeClassName + this.lineModeClassName = lineModeClassName + this.fontLrcClassName = fontLrcClassName + this.extendedLrcClassName = extendedLrcClassName + this.activeLineClassName = activeLineClassName + this.shadowContent = shadowContent + + this.isVertical = isVertical + this.playingLineNum = -1 + this.isLineMode = false + + this.initInfo = { + lines: [], + offset: 0, + } + + this.linePlayer = new LinePlayer({ + offset: this.offset, + rate: this.rate, + onPlay: this._handleLinePlayerOnPlay, + onSetLyric: this._handleLinePlayerOnSetLyric, + }) + } + + _init() { + this.playingLineNum = -1 + this.isLineMode = false + + this.linePlayer.setLyric(this.lyric, this.extendedLyrics) + } + + _handleLinePlayerOnPlay = (num, text, curTime) => { + if (this.isLineMode) { + if (num < this.playingLineNum + 1) { + for (let i = this.playingLineNum, minNum = Math.max(num, 0) - 1; i > minNum; i--) { + const font = this._lineFonts[i] + font.reset() + font.lineContent.classList.remove(this.activeLineClassName) + } + } else if (num > this.playingLineNum) { + for (let i = Math.max(this.playingLineNum, 0); i < num; i++) { + const font = this._lineFonts[i] + font.reset() + font.lineContent.classList.remove(this.activeLineClassName) + } + } else if (this.playingLineNum > -1) { + const font = this._lineFonts[this.playingLineNum] + font.reset() + font.lineContent.classList.remove(this.activeLineClassName) + } + } else { + if (num < this.playingLineNum + 1) { + for (let i = this.playingLineNum, minNum = Math.max(num, 0) - 1; i > minNum; i--) { + const font = this._lineFonts[i] + font.lineContent.classList.remove(this.activeLineClassName) + font.reset() + } + } else if (num > this.playingLineNum) { + for (let i = Math.max(this.playingLineNum, 0); i < num; i++) { + const font = this._lineFonts[i] + font.lineContent.classList.remove(this.activeLineClassName) + font.finish() + } + } else if (this.playingLineNum > -1) { + const font = this._lineFonts[this.playingLineNum] + font.lineContent.classList.remove(this.activeLineClassName) + } + } + this.playingLineNum = num + if (num > -1) { + const font = this._lineFonts[num] + font.lineContent.classList.add(this.activeLineClassName) + font.play(curTime - this._lines[num].time) + } + this.onPlay(num, this._lines[num]?.text ?? '') + } + + _initLines = (lyricLines, offset, isUpdate) => { + // console.log(lyricLines) + // this._lines = lyricsLines + this.isLineMode = lyricLines.length && !/^<\d+,\d+>/.test(lyricLines[0].text) + + this._lineFonts = [] + if (this.isLineMode) { + this._lines = lyricLines.map(line => { + const fontPlayer = new FontPlayer({ + time: line.time, + rate: this.rate, + lyric: line.text, + extendedLyrics: line.extendedLyrics, + lineContentClassName: this.lineContentClassName, + lineClassName: this.lineClassName, + 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) + return { + text: line.text, + time: line.time, + extendedLyrics: line.extendedLyrics, + dom_line: fontPlayer.lineContent, + } + }) + } else { + this._lines = lyricLines.map(line => { + const fontPlayer = new FontPlayer({ + time: line.time, + rate: this.rate, + lyric: line.text, + extendedLyrics: line.extendedLyrics, + lineContentClassName: this.lineContentClassName, + lineClassName: this.lineClassName, + 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) + return { + text: line.text.replace(fontTimeExp, ''), + time: line.time, + extendedLyrics: line.extendedLyrics, + dom_line: fontPlayer.lineContent, + } + }) + } + + // 如果是逐行歌词,则添加 60ms 的偏移 + let newOffset = this.isLineMode ? this.offset + 60 : this.offset + offset = offset - this.linePlayer.offset + newOffset + this.linePlayer.offset = newOffset + 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) { + if (!this.linePlayer) return + this.linePlayer.play(curTime) + } + + pause() { + if (!this.linePlayer) return + this.linePlayer.pause() + if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum]?.pause() + } + + setOffset(offset) { + this.linePlayer.offset = offset + } + + setLyric(lyric, extendedLyrics) { + this.lyric = lyric + this.extendedLyrics = extendedLyrics + this._init() + } + + setPlaybackRate(rate) { + this.rate = rate + this.linePlayer.setPlaybackRate(rate) + 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 + } + + 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 + } + + setDisabledAutoPause(autoPause) { + this.linePlayer.setDisabledAutoPause(autoPause) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/line-player.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/line-player.js new file mode 100644 index 000000000..4bd83e3f7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/line-player.js @@ -0,0 +1,242 @@ +import { getNow, TimeoutTools } from './utils' + +const timeFieldExp = /^(?:\[[\d:.]+\])+/g +const timeExp = /\d{1,3}(:\d{1,3}){0,2}(?:\.\d{1,3})/g +const msTimeRxp = /\[\d{1,3}(:\d{1,3}){0,2}\.\d{3}]/ +const tagRegMap = { + title: 'ti', + artist: 'ar', + album: 'al', + offset: 'offset', + by: 'by', +} + +const timeoutTools = new TimeoutTools() + +const t_rxp_1 = /^0+(\d+)/ +const t_rxp_2 = /:0+(\d+)/g +const t_rxp_3 = /\.0+(\d+)/ +const formatTimeLabel = (label) => { + return label.replace(t_rxp_1, '$1') + .replace(t_rxp_2, ':$1') + .replace(t_rxp_3, '.$1') +} + +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 = timeFieldExp.exec(line) + if (result) { + const timeField = result[0] + const text = line.replace(timeFieldExp, '').trim() + // https://github.com/lyswhut/lx-music-desktop/issues/1499 + if (text && text != '//') { + const times = timeField.match(timeExp) + if (times == null) continue + for (let time of times) { + const timeStr = formatTimeLabel(time) + const targetLine = lrcLinesMap[timeStr] + if (targetLine) targetLine.extendedLyrics.push(text) + } + } + } + } +} + +export default class LinePlayer { + constructor({ offset = 0, rate = 1, onPlay = function() { }, onSetLyric = function() { } } = {}) { + this.tags = {} + this.lines = null + this.onPlay = onPlay + this.onSetLyric = onSetLyric + this.isPlay = false + this.curLineNum = 0 + this.maxLine = 0 + this.offset = offset + this._performanceTime = 0 + this._startTime = 0 + this._rate = rate + } + + _init() { + if (this.lyric == null) this.lyric = '' + if (this.extendedLyrics == null) this.extendedLyrics = [] + this._initTag() + this._initLines() + this.onSetLyric(this.lines, this.tags.offset + this.offset) + } + + _initTag() { + this.tags = {} + for (let tag in tagRegMap) { + const matches = this.lyric.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i')) + this.tags[tag] = (matches && matches[1]) || '' + } + if (this.tags.offset) { + let offset = parseInt(this.tags.offset) + this.tags.offset = Number.isNaN(offset) ? 0 : offset + } else { + this.tags.offset = 0 + } + } + + _initLines() { + this.lines = [] + const isMsTime = msTimeRxp.test(this.lyric) + const lines = this.lyric.split(/\r\n|\r|\n/) + const linesMap = {} + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim() + let result = timeFieldExp.exec(line) + if (result) { + const timeField = result[0] + const text = line.replace(timeFieldExp, '').trim() + if (text) { + const times = timeField.match(timeExp) + if (times == null) continue + for (let time of times) { + const timeStr = formatTimeLabel(time) + if (linesMap[timeStr]) { + linesMap[timeStr].extendedLyrics.push(text) + continue + } + const timeArr = timeStr.split(':') + if (timeArr.length > 3) continue + else if (timeArr.length < 3) for (let i = 3 - timeArr.length; i--;) timeArr.unshift('0') + if (timeArr[2].indexOf('.') > -1) timeArr.splice(2, 1, ...timeArr[2].split('.')) + const msTime = timeArr[3] || '0' + + linesMap[timeStr] = { + time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(isMsTime ? msTime : msTime.padEnd(3, '0')), + text, + extendedLyrics: [], + } + } + } + } + } + + for (const lrc of this.extendedLyrics) parseExtendedLyric(linesMap, lrc) + this.lines = Object.values(linesMap) + this.lines.sort((a, b) => { + return a.time - b.time + }) + this.maxLine = this.lines.length - 1 + } + + _currentTime() { + return (getNow() - this._performanceTime) * this._rate + this._startTime + } + + _findCurLineNum(curTime, startIndex = 0) { + if (curTime <= 0) return 0 + const length = this.lines.length + for (let index = startIndex; index < length; index++) if (curTime <= this.lines[index].time) return index === 0 ? 0 : index - 1 + return length - 1 + } + + _handleMaxLine() { + this.onPlay(this.curLineNum, this.lines[this.curLineNum].text, this._currentTime()) + this.pause() + } + + _refresh() { + this.curLineNum++ + // console.log('curLineNum time', this.lines[this.curLineNum].time) + if (this.curLineNum >= this.maxLine) return this._handleMaxLine() + + let curLine = this.lines[this.curLineNum] + + const currentTime = this._currentTime() + const driftTime = currentTime - curLine.time + + if (driftTime >= 0) { + let nextLine = this.lines[this.curLineNum + 1] + const delay = (nextLine.time - curLine.time - driftTime) / this._rate + + if (delay > 0) { + if (this.isPlay) { + timeoutTools.start(() => { + if (!this.isPlay) return + this._refresh() + }, delay) + } + this.onPlay(this.curLineNum, curLine.text, currentTime) + return + } else { + let newCurLineNum = this._findCurLineNum(currentTime, this.curLineNum + 1) + if (newCurLineNum > this.curLineNum) this.curLineNum = newCurLineNum - 1 + this._refresh() + return + } + } else if (this.curLineNum == 0) { + let firstLine = this.lines[0] + const delay = (firstLine.time - currentTime) / this._rate + if (this.isPlay) { + timeoutTools.start(() => { + if (!this.isPlay) return + this._refresh() + }, delay) + } + this.onPlay(-1, '', currentTime) + return + } + + this.curLineNum = this._findCurLineNum(currentTime, this.curLineNum) - 1 + this._refresh() + } + + play(curTime = 0) { + if (!this.lines.length) return + this.pause() + this.isPlay = true + + this._performanceTime = getNow() - parseInt(this.tags.offset + this.offset) + this._startTime = curTime + + this.curLineNum = this._findCurLineNum(this._currentTime()) - 1 + + this._refresh() + } + + pause() { + if (!this.isPlay) return + this.isPlay = false + timeoutTools.clear() + if (this.curLineNum === this.maxLine) return + const currentTime = this._currentTime() + const curLineNum = this._findCurLineNum(currentTime) + if (this.curLineNum !== curLineNum) { + this.curLineNum = curLineNum + this.onPlay(curLineNum, this.lines[curLineNum].text, currentTime) + } + } + + setPlaybackRate(rate) { + this._rate = rate + if (!this.lines.length) return + if (!this.isPlay) return + this.play(this._currentTime()) + } + + setLyric(lyric, extendedLyrics) { + // console.log(extendedLyrics) + if (this.isPlay) this.pause() + this.lyric = lyric + this.extendedLyrics = extendedLyrics + this._init() + } + + setDisabledAutoPause(disabledAutoPause) { + if (disabledAutoPause) { + timeoutTools.nextTick = (handler) => { + return setTimeout(handler, 20) + } + timeoutTools.cancelNextTick = clearTimeout.bind(global) + } else { + timeoutTools.nextTick = window.requestAnimationFrame.bind(window) + timeoutTools.cancelNextTick = window.cancelAnimationFrame.bind(window) + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/utils.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/utils.js new file mode 100644 index 000000000..47ec21d71 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyric-font-player/utils.js @@ -0,0 +1,53 @@ + +export const getNow = typeof performance == 'object' && window.performance.now ? window.performance.now.bind(window.performance) : Date.now.bind(Date) + +export class TimeoutTools { + constructor(thresholdTime = 80) { + this.nextTick = window.requestAnimationFrame.bind(window) + this.cancelNextTick = window.cancelAnimationFrame.bind(window) + this.invokeTime = 0 + this.animationFrameId = null + this.timeoutId = null + this.callback = null + this.thresholdTime = thresholdTime + } + + run() { + this.animationFrameId = this.nextTick(() => { + this.animationFrameId = null + let diff = this.invokeTime - getNow() + // console.log('diff', diff) + if (diff > 0) { + if (diff < this.thresholdTime) return this.run() + // console.log('run timeout', diff, diff - this.thresholdTime) + return this.timeoutId = window.setTimeout(() => { + this.timeoutId = null + this.run() + }, diff - this.thresholdTime) + } + + // console.log('diff', diff) + this.callback(diff) + }) + } + + start(callback = () => {}, timeout = 0) { + // console.log(timeout) + this.callback = callback + this.invokeTime = getNow() + timeout + + this.run() + } + + clear() { + if (this.animationFrameId) { + this.cancelNextTick(this.animationFrameId) + this.animationFrameId = null + } + if (this.timeoutId) { + window.clearTimeout(this.timeoutId) + this.timeoutId = null + } + } +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyricUtils/kg.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyricUtils/kg.js new file mode 100644 index 000000000..47d54e26b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyricUtils/kg.js @@ -0,0 +1,74 @@ +import { inflate } from 'zlib' +import { decodeName } from './util' + +// https://github.com/lyswhut/lx-music-desktop/issues/296#issuecomment-683285784 +const enc_key = Buffer.from([0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69], 'binary') +const decodeLyric = str => new Promise((resolve, reject) => { + if (!str.length) return + const buf_str = Buffer.from(str, 'base64').subarray(4) + for (let i = 0, len = buf_str.length; i < len; i++) { + buf_str[i] = buf_str[i] ^ enc_key[i % 16] + } + inflate(buf_str, (err, result) => { + if (err) return reject(err) + resolve(result.toString()) + }) +}) + +const headExp = /^.*\[id:\$\w+\]\n/ + +const parseLyric = str => { + str = str.replace(/\r/g, '') + if (headExp.test(str)) str = str.replace(headExp, '') + let trans = str.match(/\[language:([\w=\\/+]+)\]/) + let lyric + let rlyric + let tlyric + if (trans) { + str = str.replace(/\[language:[\w=\\/+]+\]\n/, '') + let json = JSON.parse(Buffer.from(trans[1], 'base64').toString()) + for (const item of json.content) { + switch (item.type) { + case 0: + rlyric = item.lyricContent + break + case 1: + tlyric = item.lyricContent + break + } + } + } + let i = 0 + let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => { + let result = str.match(/\[((\d+),\d+)\].*/) + let time = parseInt(result[2]) + let ms = (time % 1000).toString().padStart(3, '0') + time /= 1000 + let m = parseInt(time / 60).toString().padStart(2, '0') + time %= 60 + let s = parseInt(time).toString().padStart(2, '0') + time = `${m}:${s}.${ms}` + if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}` + if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}` + i++ + return str.replace(result[1], time) + }) + rlyric = rlyric ? rlyric.join('\n') : '' + tlyric = tlyric ? tlyric.join('\n') : '' + lxlyric = lxlyric.replace(/<(\d+,\d+),\d+>/g, '<$1>') + lxlyric = decodeName(lxlyric) + lyric = lxlyric.replace(/<\d+,\d+>/g, '') + rlyric = decodeName(rlyric) + tlyric = decodeName(tlyric) + return { + lyric, + tlyric, + rlyric, + lxlyric, + } +} + + +export const decodeKrc = async(data) => { + return decodeLyric(data).then(parseLyric) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyricUtils/util.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyricUtils/util.ts new file mode 100644 index 000000000..ab08d2088 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/lyricUtils/util.ts @@ -0,0 +1,13 @@ +const encodeNames = { + ' ': ' ', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + ''': "'", +} as const + +export const decodeName = (str: string | null = '') => { + return str?.replace(/(?:&|<|>|"|'|'| )/gm, (s: string) => encodeNames[s as keyof typeof encodeNames]) ?? '' +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/migrateSetting.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/migrateSetting.ts new file mode 100644 index 000000000..6c06630f6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/migrateSetting.ts @@ -0,0 +1,145 @@ +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 => { + setting = { ...setting } + + // 迁移 v2.0.0 之前的配置 + if (compareVer(setting.version, '2.0.0') < 0) { + // 迁移列表滚动位置设置 ~0.18.3 + if (setting.list?.scroll) { + let scroll = setting.list.scroll + 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 + } + + if (setting.tray?.isShow != null) 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.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 + if (setting.desktopLyric?.style?.fontSize) 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['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 + + setting.version = '2.0.0' + } + + // 迁移 v2.2.0 之前的设置数据 + if (compareVer(setting.version, '2.1.0') < 0) { + setting['sync.erver.port'] = setting['sync.port'] + setting.version = '2.1.0' + } + + + return setting +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/downloader.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/downloader.js new file mode 100644 index 000000000..1eaae9a8f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/downloader.js @@ -0,0 +1,78 @@ +const http = require('http') +const https = require('https') +const fs = require('fs') +const { httpOverHttp, httpsOverHttp } = require('tunnel') + +const httpsRxp = /^https:/ +const getRequestAgent = (url, proxy) => { + return proxy ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)({ proxy }) : undefined +} + +const sendRequest = (url, proxy) => { + const urlParse = new URL(url) + const httpOptions = { + method: 'get', + host: urlParse.hostname, + port: urlParse.port, + path: urlParse.pathname + urlParse.search, + agent: getRequestAgent(url, proxy), + 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, proxy) => { + return new Promise((resolve) => { + sendRequest(url, proxy) + .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) +// }) diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/index.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/index.js new file mode 100644 index 000000000..5ebbd4838 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/index.js @@ -0,0 +1,281 @@ +// Fork from https://github.com/claus/flac-metadata +// 在 flac-metadata 的基础上修复与改进标签与封面写入逻辑 + + +const Transform = require('stream').Transform + +const MetaDataBlock = require('./lib/MetaDataBlock') +const MetaDataBlockStreamInfo = require('./lib/MetaDataBlockStreamInfo') +const MetaDataBlockVorbisComment = require('./lib/MetaDataBlockVorbisComment') +const MetaDataBlockPicture = require('./lib/MetaDataBlockPicture') + +const STATE_IDLE = 0 +const STATE_MARKER = 1 +const STATE_MDB_HEADER = 2 +const STATE_MDB = 3 +const STATE_PASS_THROUGH = 4 + +class Processor extends Transform { + constructor(options) { + super(options) + // MDB types + this.MDB_TYPE_STREAMINFO = 0 + this.MDB_TYPE_PADDING = 1 + this.MDB_TYPE_APPLICATION = 2 + this.MDB_TYPE_SEEKTABLE = 3 + this.MDB_TYPE_VORBIS_COMMENT = 4 + this.MDB_TYPE_CUESHEET = 5 + this.MDB_TYPE_PICTURE = 6 + this.MDB_TYPE_INVALID = 127 + + this.state = STATE_IDLE + + this.isFlac = false + + this.buf = null + this.bufPos = 0 + + this.mdb = null + this.mdbLen = 0 + this.mdbLast = false + this.mdbPush = false + this.mdbLastWritten = false + + this.parseMetaDataBlocks = false + + this.waitWriteVorbis = null + this.waitWritePicture = null + this.tasks = 0 + + if (!(this instanceof Processor)) return new Processor(options) + if (options && !!options.parseMetaDataBlocks) { this.parseMetaDataBlocks = true } + } + + writeMeta({ vorbis, picture }) { + if (vorbis != null) { + this.waitWriteVorbis = vorbis + this.tasks++ + } + if (picture != null) { + this.waitWritePicture = picture + this.tasks++ + } + } + + // clearMeta() { + // this.mdbLastWritten = true + // } + + readMeta(callback) { + this.parseMetaDataBlocks = true + this.readCallBack = callback + } + + _transform(chunk, enc, done) { + let chunkPos = 0 + let chunkLen = chunk.length + let isChunkProcessed = false + let _this = this + + function _safePush(minCapacity, persist, validate) { + let slice + let chunkAvailable = chunkLen - chunkPos + let isDone = (chunkAvailable + this.bufPos >= minCapacity) + validate = (typeof validate === 'function') ? validate : function() { return true } + if (isDone) { + // Enough data available + if (persist) { + // Persist the entire block so it can be parsed + if (this.bufPos > 0) { + // Part of this block's data is in backup buffer, copy rest over + chunk.copy(this.buf, this.bufPos, chunkPos, chunkPos + minCapacity - this.bufPos) + slice = this.buf.slice(0, minCapacity) + } else { + // Entire block fits in current chunk + slice = chunk.slice(chunkPos, chunkPos + minCapacity) + } + } else { + slice = chunk.slice(chunkPos, chunkPos + minCapacity - this.bufPos) + } + // Push block after validation + validate(slice, isDone) && _this.push(slice) + chunkPos += minCapacity - this.bufPos + this.bufPos = 0 + this.buf = null + } else { + // Not enough data available + if (persist) { + // Copy/append incomplete block to backup buffer + this.buf = this.buf || Buffer.alloc(minCapacity) + chunk.copy(this.buf, this.bufPos, chunkPos, chunkLen) + } else { + // Push incomplete block after validation + slice = chunk.slice(chunkPos, chunkLen) + validate(slice, isDone) && _this.push(slice) + } + this.bufPos += chunkLen - chunkPos + } + return isDone + }; + let safePush = _safePush.bind(this) + + while (!isChunkProcessed) { + switch (this.state) { + case STATE_IDLE: + this.state = STATE_MARKER + break + case STATE_MARKER: + if (safePush(4, true, this._validateMarker.bind(this))) { + this.state = this.isFlac ? STATE_MDB_HEADER : STATE_PASS_THROUGH + } else { + isChunkProcessed = true + } + break + case STATE_MDB_HEADER: + if (safePush(4, true, this._validateMDBHeader.bind(this))) { + this.state = STATE_MDB + } else { + isChunkProcessed = true + } + break + case STATE_MDB: + if (safePush(this.mdbLen, this.parseMetaDataBlocks, this._validateMDB.bind(this))) { + if (this.mdb.isLast) { + // This MDB has the isLast flag set to true. + // Ignore all following MDBs. + this.mdbLastWritten = true + } + this.readCallBack && this.readCallBack(this.mdb) + if (this.mdbLast) { + this._writeVorbisComment() + this._writePicture() + this.state = STATE_PASS_THROUGH + } else { + this.state = STATE_MDB_HEADER + } + } else { + isChunkProcessed = true + } + break + case STATE_PASS_THROUGH: + safePush(chunkLen - chunkPos, false) + isChunkProcessed = true + break + } + } + + done() + } + + _validateMarker(slice, isDone) { + this.isFlac = (slice.toString('utf8', 0) === 'fLaC') + // TODO: completely bail out if file is not a FLAC? + return true + } + + _validateMDBHeader(slice, isDone) { + // Parse MDB header + let header = slice.readUInt32BE(0) + let type = (header >>> 24) & 0x7f + this.mdbLast = (((header >>> 24) & 0x80) !== 0) + this.mdbLen = header & 0xffffff + // Create appropriate MDB object + // (data is injected later in _validateMDB, if parseMetaDataBlocks option is set to true) + switch (type) { + case this.MDB_TYPE_STREAMINFO: + this.mdb = new MetaDataBlockStreamInfo(this.mdbLast) + break + case this.MDB_TYPE_VORBIS_COMMENT: + if (this.waitWriteVorbis) { + this._writeVorbisComment(slice, header) + this.mdbPush = false + return this.mdbPush + } else { + this.mdb = new MetaDataBlockVorbisComment(this.mdbLast) + this.readCallback && this.readCallback(this.mdb) + } + break + case this.MDB_TYPE_PICTURE: + if (this.waitWritePicture) { + this._writePicture(slice, header) + this.mdbPush = false + return this.mdbPush + } else { + this.mdb = new MetaDataBlockPicture(this.mdbLast) + this.readCallback && this.readCallback(this.mdb) + } + break + case this.MDB_TYPE_PADDING: + case this.MDB_TYPE_APPLICATION: + case this.MDB_TYPE_SEEKTABLE: + case this.MDB_TYPE_CUESHEET: + case this.MDB_TYPE_INVALID: + default: + this.mdb = new MetaDataBlock(this.mdbLast, type) + break + } + + // this.emit('preprocess', this.mdb) + + if (this.mdbLastWritten) { + // A previous MDB had the isLast flag set to true. + // Ignore all following MDBs. + this.mdb.remove() + } else { + if (this.mdbLast && this.tasks > 0) { + header &= 0x7fffffff + slice.writeUInt32BE(header >>> 0, 0) + } + // The consumer may change the MDB's isLast flag in the preprocess handler. + // Here that flag is updated in the MDB header. + } + this.mdbPush = !this.mdb.removed + return this.mdbPush + } + + _validateMDB(slice, isDone) { + // Parse the MDB if parseMetaDataBlocks option is set to true + if (this.parseMetaDataBlocks && isDone) { + this.mdb.parse(slice) + } + return this.mdbPush + } + + _flush(done) { + // All chunks have been processed + // Clean up + this.state = STATE_IDLE + this.mdbLastWritten = false + this.isFlac = false + this.bufPos = 0 + this.buf = null + this.mdb = null + done() + } + + _writeVorbisComment() { + if (this.waitWriteVorbis == null) return + let isLast = this.mdbLast && this.tasks === 1 + this.tasks-- + this.push(MetaDataBlockVorbisComment.create(isLast, this.waitWriteVorbis.vendor, this.waitWriteVorbis.comments).publish()) + this.waitWriteVorbis = null + } + + _writePicture() { + if (this.waitWritePicture == null) return + let isLast = this.mdbLast && this.tasks === 1 + this.tasks-- + this.push( + MetaDataBlockPicture.create( + isLast, this.waitWritePicture.pictureType, + this.waitWritePicture.mimeType, this.waitWritePicture.description, + this.waitWritePicture.width, this.waitWritePicture.height, + this.waitWritePicture.bitsPerPixel, this.waitWritePicture.colors, + this.waitWritePicture.pictureData, + ).publish(), + ) + this.waitWritePicture = null + } +} + +module.exports = Processor diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlock.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlock.js new file mode 100644 index 000000000..790dbc12c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlock.js @@ -0,0 +1,25 @@ +class MetaDataBlock { + constructor(isLast, type) { + this.isLast = isLast + this.type = type + this.error = null + this.hasData = false + this.removed = false + } + + remove() { + this.removed = true + } + + parse(buffer) { + } + + toString() { + let str = '[MetaDataBlock]' + str += ' type: ' + this.type + str += ', isLast: ' + this.isLast + return str + } +} + +module.exports = MetaDataBlock diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockPicture.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockPicture.js new file mode 100644 index 000000000..bf54476f0 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockPicture.js @@ -0,0 +1,130 @@ +const MetaDataBlock = require('./MetaDataBlock') + +class MetaDataBlockPicture extends MetaDataBlock { + constructor(isLast) { + super(isLast, 6) + + this.pictureType = 0 + this.mimeType = '' + this.description = '' + this.width = 0 + this.height = 0 + this.bitsPerPixel = 0 + this.colors = 0 + this.pictureData = null + } + + static create(isLast, pictureType, mimeType, description, width, height, bitsPerPixel, colors, pictureData) { + let mdb = new MetaDataBlockPicture(isLast) + mdb.pictureType = pictureType + mdb.mimeType = mimeType + mdb.description = description + mdb.width = width + mdb.height = height + mdb.bitsPerPixel = bitsPerPixel + mdb.colors = colors + mdb.pictureData = pictureData + mdb.hasData = true + return mdb + } + + + parse(buffer) { + try { + let pos = 0 + + this.pictureType = buffer.readUInt32BE(pos) + pos += 4 + + let mimeTypeLength = buffer.readUInt32BE(pos) + this.mimeType = buffer.toString('utf8', pos + 4, pos + 4 + mimeTypeLength) + pos += 4 + mimeTypeLength + + let descriptionLength = buffer.readUInt32BE(pos) + this.description = buffer.toString('utf8', pos + 4, pos + 4 + descriptionLength) + pos += 4 + descriptionLength + + this.width = buffer.readUInt32BE(pos) + this.height = buffer.readUInt32BE(pos + 4) + this.bitsPerPixel = buffer.readUInt32BE(pos + 8) + this.colors = buffer.readUInt32BE(pos + 12) + pos += 16 + + let pictureDataLength = buffer.readUInt32BE(pos) + this.pictureData = Buffer.alloc(pictureDataLength) + buffer.copy(this.pictureData, 0, pos + 4, pictureDataLength) + + this.hasData = true + } catch (e) { + this.error = e + this.hasData = false + } + } + + publish() { + let pos = 0 + let size = this.getSize() + let buffer = Buffer.alloc(4 + size) + + let header = size + header |= (this.type << 24) + header |= (this.isLast ? 0x80000000 : 0) + buffer.writeUInt32BE(header >>> 0, pos) + pos += 4 + + buffer.writeUInt32BE(this.pictureType, pos) + pos += 4 + + let mimeTypeLen = Buffer.byteLength(this.mimeType) + buffer.writeUInt32BE(mimeTypeLen, pos) + buffer.write(this.mimeType, pos + 4) + pos += 4 + mimeTypeLen + + let descriptionLen = Buffer.byteLength(this.description) + buffer.writeUInt32BE(descriptionLen, pos) + buffer.write(this.description, pos + 4) + pos += 4 + descriptionLen + + buffer.writeUInt32BE(this.width, pos) + buffer.writeUInt32BE(this.height, pos + 4) + buffer.writeUInt32BE(this.bitsPerPixel, pos + 8) + buffer.writeUInt32BE(this.colors, pos + 12) + pos += 16 + + buffer.writeUInt32BE(this.pictureData.length, pos) + this.pictureData.copy(buffer, pos + 4) + + return buffer + } + + getSize() { + let size = 4 + size += 4 + Buffer.byteLength(this.mimeType) + size += 4 + Buffer.byteLength(this.description) + size += 16 + size += 4 + this.pictureData.length + return size + } + + toString() { + let str = '[MetaDataBlockPicture]' + str += ' type: ' + this.type + str += ', isLast: ' + this.isLast + if (this.error) { + str += '\n ERROR: ' + this.error + } + if (this.hasData) { + str += '\n pictureType: ' + this.pictureType + str += '\n mimeType: ' + this.mimeType + str += '\n description: ' + this.description + str += '\n width: ' + this.width + str += '\n height: ' + this.height + str += '\n bitsPerPixel: ' + this.bitsPerPixel + str += '\n colors: ' + this.colors + str += '\n pictureData: ' + (this.pictureData ? this.pictureData.length : '') + } + return str + } +} + +module.exports = MetaDataBlockPicture diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockStreamInfo.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockStreamInfo.js new file mode 100644 index 000000000..645eef785 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockStreamInfo.js @@ -0,0 +1,84 @@ +const MetaDataBlock = require('./MetaDataBlock') + +function pad(n, width) { + n = '' + n + return (n.length >= width) ? n : new Array(width - n.length + 1).join('0') + n +} + +class MetaDataBlockStreamInfo extends MetaDataBlock { + constructor(isLast) { + super(isLast, 0) + + this.minBlockSize = 0 + this.maxBlockSize = 0 + this.minFrameSize = 0 + this.maxFrameSize = 0 + this.sampleRate = 0 + this.channels = 0 + this.bitsPerSample = 0 + this.samples = 0 + this.checksum = null + this.duration = 0 + this.durationStr = '0:00.000' + } + + remove() { + console.error("WARNING: Can't remove StreamInfo block!") + } + + parse(buffer) { + try { + let pos = 0 + + this.minBlockSize = buffer.readUInt16BE(pos) + this.maxBlockSize = buffer.readUInt16BE(pos + 2) + this.minFrameSize = (buffer.readUInt8(pos + 4) << 16) | buffer.readUInt16BE(pos + 5) + this.maxFrameSize = (buffer.readUInt8(pos + 7) << 16) | buffer.readUInt16BE(pos + 8) + + let tmp = buffer.readUInt32BE(pos + 10) + this.sampleRate = tmp >>> 12 + this.channels = (tmp >>> 9) & 0x07 + this.bitsPerSample = (tmp >>> 4) & 0x1f + this.samples = +((tmp & 0x0f) << 4) + buffer.readUInt32BE(pos + 14) + + this.checksum = Buffer.alloc(16) + buffer.copy(this.checksum, 0, 18, 34) + + this.duration = this.samples / this.sampleRate + + let minutes = '' + Math.floor(this.duration / 60) + let seconds = pad(Math.floor(this.duration % 60), 2) + let milliseconds = pad(Math.round(((this.duration % 60) - Math.floor(this.duration % 60)) * 1000), 3) + this.durationStr = minutes + ':' + seconds + '.' + milliseconds + + this.hasData = true + } catch (e) { + this.error = e + this.hasData = false + } + } + + toString() { + let str = '[MetaDataBlockStreamInfo]' + str += ' type: ' + this.type + str += ', isLast: ' + this.isLast + if (this.error) { + str += '\n ERROR: ' + this.error + } + if (this.hasData) { + str += '\n minBlockSize: ' + this.minBlockSize + str += '\n maxBlockSize: ' + this.maxBlockSize + str += '\n minFrameSize: ' + this.minFrameSize + str += '\n maxFrameSize: ' + this.maxFrameSize + str += '\n samples: ' + this.samples + str += '\n sampleRate: ' + this.sampleRate + str += '\n channels: ' + (this.channels + 1) + str += '\n bitsPerSample: ' + (this.bitsPerSample + 1) + str += '\n duration: ' + this.durationStr + str += '\n checksum: ' + (this.checksum ? this.checksum.toString('hex') : '') + } + return str + } +} + +module.exports = MetaDataBlockStreamInfo diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockVorbisComment.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockVorbisComment.js new file mode 100644 index 000000000..f7b7b21e6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockVorbisComment.js @@ -0,0 +1,105 @@ +const MetaDataBlock = require('./MetaDataBlock') + +class MetaDataBlockVorbisComment extends MetaDataBlock { + constructor(isLast) { + super(isLast, 4) + + this.vendor = '' + this.comments = [] + } + + static create(isLast, vendor, comments) { + let mdb = new MetaDataBlockVorbisComment(isLast) + mdb.vendor = vendor + mdb.comments = comments + mdb.hasData = true + return mdb + } + + parse(buffer) { + try { + let pos = 0 + + let vendorLen = buffer.readUInt32LE(pos) + let vendor = buffer.toString('utf8', pos + 4, pos + 4 + vendorLen) + this.vendor = vendor + pos += 4 + vendorLen + + let commentCount = buffer.readUInt32LE(pos) + pos += 4 + + while (commentCount-- > 0) { + let commentLen = buffer.readUInt32LE(pos) + let comment = buffer.toString('utf8', pos + 4, pos + 4 + commentLen) + this.comments.push(comment) + pos += 4 + commentLen + } + + this.hasData = true + } catch (e) { + this.error = e + this.hasData = false + } + } + + publish() { + let pos = 0 + let size = this.getSize() + let buffer = Buffer.alloc(4 + size) + + let header = size + header |= (this.type << 24) + header |= (this.isLast ? 0x80000000 : 0) + buffer.writeUInt32BE(header >>> 0, pos) + pos += 4 + let vendorLen = Buffer.byteLength(this.vendor) + buffer.writeUInt32LE(vendorLen, pos) + buffer.write(this.vendor, pos + 4) + pos += 4 + vendorLen + + let commentCount = this.comments.length + buffer.writeUInt32LE(commentCount, pos) + pos += 4 + + for (let i = 0; i < commentCount; i++) { + let comment = this.comments[i] + let commentLen = Buffer.byteLength(comment) + buffer.writeUInt32LE(commentLen, pos) + buffer.write(comment, pos + 4) + pos += 4 + commentLen + } + + return buffer + } + + getSize() { + let size = 8 + Buffer.byteLength(this.vendor) + for (let i = 0; i < this.comments.length; i++) { + size += 4 + Buffer.byteLength(this.comments[i]) + } + return size + } + + toString() { + let str = '[MetaDataBlockVorbisComment]' + str += ' type: ' + this.type + str += ', isLast: ' + this.isLast + if (this.error) { + str += '\n ERROR: ' + this.error + } + if (this.hasData) { + str += '\n vendor: ' + this.vendor + if (this.comments.length) { + str += '\n comments:' + for (let i = 0; i < this.comments.length; i++) { + str += '\n ' + this.comments[i].split('=').join(': ') + } + } else { + str += '\n comments: none' + } + } + return str + } +} + +module.exports = MetaDataBlockVorbisComment diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flacMeta.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flacMeta.js new file mode 100644 index 000000000..e2711fec2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/flacMeta.js @@ -0,0 +1,81 @@ +const fs = require('fs') +const fsPromises = fs.promises +const path = require('path') +const getImgSize = require('image-size') +const download = require('./downloader') + +const FlacProcessor = require('./flac-metadata/index') + +const extReg = /^(\.(?:jpe?g|png)).*$/ +const vendor = 'reference libFLAC 1.2.1 20070917' + +const writeMeta = async(filePath, meta, picPath) => { + const comments = Object.keys(meta).map(key => `${key.toUpperCase()}=${meta[key] || ''}`) + const data = { + vorbis: { + vendor, + comments, + }, + } + if (picPath) { + const apicData = await fsPromises.readFile(picPath) + let imgSize = getImgSize(apicData) + let mime_type + let bitsPerPixel + if (apicData[0] == 0xff && apicData[1] == 0xd8 && apicData[2] == 0xff) { + mime_type = 'image/jpeg' + bitsPerPixel = 24 + } else { + mime_type = 'image/png' + bitsPerPixel = 32 + } + data.picture = { + pictureType: 3, + mimeType: mime_type, + description: '', + width: imgSize.width, + height: imgSize.height, + bitsPerPixel, + colors: 0, + pictureData: apicData, + } + } + + const reader = fs.createReadStream(filePath) + const tempPath = filePath + '.lxmtemp' + const writer = fs.createWriteStream(tempPath) + const flacProcessor = new FlacProcessor() + flacProcessor.writeMeta(data) + + reader.pipe(flacProcessor).pipe(writer).on('finish', () => { + fs.unlink(filePath, err => { + if (err) return console.log(err.message) + fs.rename(tempPath, filePath, err => { + if (err) console.log(err.message) + }) + }) + }) +} + +module.exports = (filePath, meta, proxy) => { + if (!meta.APIC) return writeMeta(filePath, meta) + let picUrl = meta.APIC + delete meta.APIC + if (!/^http/.test(picUrl)) { + return writeMeta(filePath, meta) + } + let ext = path.extname(picUrl) + let picPath = filePath.replace(/\.flac$/, '') + (ext ? ext.replace(extReg, '$1') : '.jpg') + + if (picUrl.includes('music.126.net')) picUrl += `${picUrl.includes('?') ? '&' : '?'}param=500y500` + download(picUrl, picPath, proxy).then(success => { + if (success) { + writeMeta(filePath, meta, picPath).finally(() => { + fs.unlink(picPath, err => { + if (err) console.log(err.message) + }) + }) + } else writeMeta(filePath, meta) + }) +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/index.d.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/index.d.ts new file mode 100644 index 000000000..7dd60bbec --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/index.d.ts @@ -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, proxy?: { host: string, port: number }): void diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/index.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/index.js new file mode 100644 index 000000000..46877f685 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/index.js @@ -0,0 +1,14 @@ +const path = require('path') +const mp3Meta = require('./mp3Meta') +const flacMeta = require('./flacMeta') + +exports.setMeta = (filePath, meta, proxy) => { + switch (path.extname(filePath)) { + case '.mp3': + mp3Meta(filePath, meta, proxy) + break + case '.flac': + flacMeta(filePath, meta, proxy) + break + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/mp3Meta.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/mp3Meta.js new file mode 100644 index 000000000..e5e55b792 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/musicMeta/mp3Meta.js @@ -0,0 +1,41 @@ +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, proxy) => { + 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') + + let picUrl = meta.APIC + if (picUrl.includes('music.126.net')) picUrl += `${picUrl.includes('?') ? '&' : '?'}param=500y500` + download(picUrl, picPath, proxy).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) + } + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/nodejs.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/nodejs.ts new file mode 100644 index 000000000..f4f22a16a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/nodejs.ts @@ -0,0 +1,224 @@ +import fs from 'node:fs' +import crypto from 'node:crypto' +import { gzip, gunzip } from 'node:zlib' +import path from 'node:path' +import { networkInterfaces } from 'node:os' +import { log } from '@common/utils' + +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 => { + return new Promise(resolve => { + if (!path) { + resolve(false) + return + } + fs.access(path, fs.constants.F_OK, err => { + if (err) { + resolve(false) + return + } + resolve(true) + }) + }) +} + +/** + * 检查路径并创建目录 + * @param path + * @returns + */ +export const checkAndCreateDir = async(path: string) => { + return fs.promises.access(path, fs.constants.F_OK | fs.constants.W_OK) + .catch(async(err: NodeJS.ErrnoException) => { + if (err.code != 'ENOENT') throw err as Error + return fs.promises.mkdir(path, { recursive: true }) + }) + .then(() => true) + .catch((err) => { + console.error(err) + return false + }) +} + + +export const getFileStats = async(path: string): Promise => { + return new Promise(resolve => { + if (!path) { + resolve(null) + return + } + fs.stat(path, (err, stats) => { + if (err) { + resolve(null) + return + } + resolve(stats) + }) + }) +} + +/** + * 检查路径并创建目录 + * @param path + * @returns + */ +export const createDir = async(path: string) => 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) { + reject(err) + return + } + resolve() + }) + return + } + reject(err) + return + } + resolve() + }) +}) + +export const removeFile = async(path: string) => new Promise((resolve, reject) => { + fs.access(path, fs.constants.F_OK, err => { + if (err) { + err.code == 'ENOENT' ? resolve() : reject(err) + return + } + fs.unlink(path, err => { + if (err) { + reject(err) + return + } + 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 => { + return new Promise((resolve, reject) => { + gzip(str, (err, result) => { + if (err) { + reject(err) + return + } + resolve(result) + }) + }) +} + +export const gunzipData = async(buf: Buffer): Promise => { + return new Promise((resolve, reject) => { + gunzip(buf, (err, result) => { + if (err) { + reject(err) + return + } + 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 => { + let isJSON = path.endsWith('.json') + let data: string | Buffer = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary') + if (!data) return data + if (!isJSON) 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 => { + await new Promise((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) +} + +export const getAddress = (): string[] => { + const nets = networkInterfaces() + const results: string[] = [] + // console.log(nets) + + for (const interfaceInfos of Object.values(nets)) { + if (!interfaceInfos) continue + // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses + for (const interfaceInfo of interfaceInfos) { + if (interfaceInfo.family === 'IPv4' && !interfaceInfo.internal) { + results.push(interfaceInfo.address) + } + } + } + return results +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/pinyin/kMandarin_8105.txt b/plugins/lx-music-ztools/vendor/lx/src/common/utils/pinyin/kMandarin_8105.txt new file mode 100644 index 000000000..3a57e2d33 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/pinyin/kMandarin_8105.txt @@ -0,0 +1,8117 @@ +# https://github.com/mozillazg/pinyin-data +# 《通用规范汉字表》(2013 年版)里 8105 个汉字的最常用读音 +# -> 表示需要验证 +# ? 表示有争议,无法确定哪个是最常用的 +# <- 表示需要补充拼音信息 +# URO (7,832) +U+4E00: yī # 一 +U+4E01: dīng # 丁 +U+4E03: qī # 七 +U+4E07: wàn # 万 +U+4E08: zhàng # 丈 +U+4E09: sān # 三 +U+4E0A: shàng # 上 +U+4E0B: xià # 下 +U+4E0D: bù # 不 +U+4E0E: yǔ # 与 +U+4E0F: miǎn # 丏 +U+4E10: gài # 丐 +U+4E11: chǒu # 丑 +U+4E13: zhuān # 专 +U+4E14: qiě # 且 +U+4E15: pī # 丕 +U+4E16: shì # 世 +U+4E18: qiū # 丘 +U+4E19: bǐng # 丙 +U+4E1A: yè # 业 +U+4E1B: cóng # 丛 +U+4E1C: dōng # 东 +U+4E1D: sī # 丝 +U+4E1E: chéng # 丞 +U+4E22: diū # 丢 +U+4E24: liǎng # 两 +U+4E25: yán # 严 +U+4E27: sàng # 丧 ?-> sāng +U+4E2A: gè # 个 +U+4E2B: yā # 丫 +U+4E2D: zhōng # 中 +U+4E30: fēng # 丰 +U+4E32: chuàn # 串 +U+4E34: lín # 临 +U+4E38: wán # 丸 +U+4E39: dān # 丹 +U+4E3A: wèi # 为 ? -> wéi +U+4E3B: zhǔ # 主 +U+4E3D: lì # 丽 +U+4E3E: jǔ # 举 +U+4E42: yì # 乂 +U+4E43: nǎi # 乃 +U+4E45: jiǔ # 久 +U+4E48: me # 么 +U+4E49: yì # 义 +U+4E4B: zhī # 之 +U+4E4C: wū # 乌 +U+4E4D: zhà # 乍 +U+4E4E: hū # 乎 +U+4E4F: fá # 乏 +U+4E50: lè # 乐 +U+4E52: pīng # 乒 +U+4E53: pāng # 乓 +U+4E54: qiáo # 乔 +U+4E56: guāi # 乖 +U+4E58: chéng # 乘 +U+4E59: yǐ # 乙 +U+4E5C: miē # 乜 +U+4E5D: jiǔ # 九 +U+4E5E: qǐ # 乞 +U+4E5F: yě # 也 +U+4E60: xí # 习 +U+4E61: xiāng # 乡 +U+4E66: shū # 书 +U+4E69: jī # 乩 +U+4E70: mǎi # 买 +U+4E71: luàn # 乱 +U+4E73: rǔ # 乳 +U+4E78: nǎ # 乸 +U+4E7E: qián # 乾 ?-> gān +U+4E86: le # 了 +U+4E88: yǔ # 予 ?-> yú +U+4E89: zhēng # 争 +U+4E8B: shì # 事 +U+4E8C: èr # 二 +U+4E8D: chù # 亍 +U+4E8E: yú # 于 +U+4E8F: kuī # 亏 +U+4E91: yún # 云 +U+4E92: hù # 互 +U+4E93: qí # 亓 +U+4E94: wǔ # 五 +U+4E95: jǐng # 井 +U+4E98: gèn # 亘 +U+4E9A: yà # 亚 +U+4E9B: xiē # 些 +U+4E9F: jí # 亟 +U+4EA1: wáng # 亡 +U+4EA2: kàng # 亢 +U+4EA4: jiāo # 交 +U+4EA5: hài # 亥 +U+4EA6: yì # 亦 +U+4EA7: chǎn # 产 +U+4EA8: hēng # 亨 +U+4EA9: mǔ # 亩 +U+4EAB: xiǎng # 享 +U+4EAC: jīng # 京 +U+4EAD: tíng # 亭 +U+4EAE: liàng # 亮 +U+4EB2: qīn # 亲 +U+4EB3: bó # 亳 +U+4EB5: xiè # 亵 +U+4EB6: dǎn # 亶 +U+4EB8: duǒ # 亸 +U+4EB9: wěi # 亹 ?-> mén +U+4EBA: rén # 人 +U+4EBF: yì # 亿 +U+4EC0: shén # 什 +U+4EC1: rén # 仁 +U+4EC2: lè # 仂 +U+4EC3: dīng # 仃 +U+4EC4: zè # 仄 +U+4EC5: jǐn # 仅 +U+4EC6: pū # 仆 +U+4EC7: chóu # 仇 +U+4EC9: zhǎng # 仉 +U+4ECA: jīn # 今 +U+4ECB: jiè # 介 +U+4ECD: réng # 仍 +U+4ECE: cóng # 从 +U+4ED1: lún # 仑 +U+4ED3: cāng # 仓 +U+4ED4: zǎi # 仔 ?-> zǐ zī +U+4ED5: shì # 仕 +U+4ED6: tā # 他 +U+4ED7: zhàng # 仗 +U+4ED8: fù # 付 +U+4ED9: xiān # 仙 +U+4EDD: tóng # 仝 +U+4EDE: rèn # 仞 +U+4EDF: qiān # 仟 +U+4EE1: gē # 仡 ?-> yì +U+4EE3: dài # 代 +U+4EE4: lìng # 令 +U+4EE5: yǐ # 以 +U+4EE8: sā # 仨 +U+4EEA: yí # 仪 +U+4EEB: mù # 仫 +U+4EEC: men # 们 ?-> mén +U+4EF0: yǎng # 仰 +U+4EF2: zhòng # 仲 +U+4EF3: pǐ # 仳 +U+4EF5: wǔ # 仵 +U+4EF6: jiàn # 件 +U+4EF7: jià # 价 +U+4EFB: rèn # 任 +U+4EFD: fèn # 份 +U+4EFF: fǎng # 仿 +U+4F01: qǐ # 企 +U+4F08: xǐn # 伈 +U+4F09: kàng # 伉 +U+4F0A: yī # 伊 +U+4F0B: jí # 伋 +U+4F0D: wǔ # 伍 +U+4F0E: jì # 伎 +U+4F0F: fú # 伏 +U+4F10: fá # 伐 +U+4F11: xiū # 休 +U+4F17: zhòng # 众 +U+4F18: yōu # 优 +U+4F19: huǒ # 伙 +U+4F1A: huì # 会 +U+4F1B: yǔ # 伛 +U+4F1E: sǎn # 伞 +U+4F1F: wěi # 伟 +U+4F20: chuán # 传 +U+4F22: yá # 伢 +U+4F23: qiàn # 伣 +U+4F24: shāng # 伤 +U+4F25: chāng # 伥 +U+4F26: lún # 伦 +U+4F27: cāng # 伧 +U+4F2A: wěi # 伪 +U+4F2B: zhù # 伫 +U+4F2D: xián # 伭 +U+4F2F: bó # 伯 +U+4F30: gū # 估 +U+4F32: nì # 伲 ?-> nǐ +U+4F34: bàn # 伴 +U+4F36: líng # 伶 +U+4F38: shēn # 伸 +U+4F3A: cì # 伺 +U+4F3C: shì # 似 ?-> sì +U+4F3D: gā # 伽 ?-> jiā qié +U+4F3E: pī # 伾 +U+4F41: yǐ # 佁 +U+4F43: diàn # 佃 +U+4F46: dàn # 但 +U+4F4D: wèi # 位 +U+4F4E: dī # 低 +U+4F4F: zhù # 住 +U+4F50: zuǒ # 佐 +U+4F51: yòu # 佑 +U+4F53: tǐ # 体 +U+4F55: hé # 何 +U+4F56: bì # 佖 +U+4F57: tuó # 佗 +U+4F58: shé # 佘 +U+4F59: yú # 余 +U+4F5A: yì # 佚 +U+4F5B: fú # 佛 ?-> fó +U+4F5C: zuò # 作 +U+4F5D: gōu # 佝 +U+4F5E: nìng # 佞 +U+4F5F: tóng # 佟 +U+4F60: nǐ # 你 +U+4F63: yōng # 佣 +U+4F64: wǎ # 佤 +U+4F65: qiān # 佥 +U+4F69: pèi # 佩 +U+4F6C: lǎo # 佬 +U+4F6F: yáng # 佯 +U+4F70: bǎi # 佰 +U+4F73: jiā # 佳 +U+4F74: èr # 佴 +U+4F76: jí # 佶 +U+4F78: huó # 佸 +U+4F7A: quán # 佺 +U+4F7B: tiāo # 佻 +U+4F7C: jiǎo # 佼 +U+4F7D: cì # 佽 +U+4F7E: yì # 佾 +U+4F7F: shǐ # 使 +U+4F81: shēn # 侁 +U+4F82: tuō # 侂 +U+4F83: kǎn # 侃 +U+4F84: zhí # 侄 +U+4F88: chǐ # 侈 +U+4F89: kuǎ # 侉 +U+4F8B: lì # 例 +U+4F8D: shì # 侍 +U+4F8F: zhū # 侏 +U+4F91: yòu # 侑 +U+4F94: móu # 侔 +U+4F97: dòng # 侗 +U+4F98: chà # 侘 +U+4F9B: gōng # 供 +U+4F9D: yī # 依 +U+4FA0: xiá # 侠 +U+4FA3: lǚ # 侣 +U+4FA5: jiǎo # 侥 +U+4FA6: zhēn # 侦 +U+4FA7: cè # 侧 +U+4FA8: qiáo # 侨 +U+4FA9: kuài # 侩 +U+4FAA: chái # 侪 +U+4FAC: nóng # 侬 +U+4FAE: wǔ # 侮 +U+4FAF: hóu # 侯 +U+4FB4: chǒu # 侴 ?-> hào +U+4FB5: qīn # 侵 +U+4FB9: tǐng # 侹 +U+4FBF: biàn # 便 +U+4FC3: cù # 促 +U+4FC4: é # 俄 +U+4FC5: qiú # 俅 +U+4FCA: jùn # 俊 +U+4FCD: liáng # 俍 +U+4FCE: zǔ # 俎 +U+4FCF: qiào # 俏 +U+4FD0: lì # 俐 +U+4FD1: yǒng # 俑 +U+4FD7: sú # 俗 +U+4FD8: fú # 俘 +U+4FD9: xī # 俙 +U+4FDA: lǐ # 俚 +U+4FDC: pīng # 俜 +U+4FDD: bǎo # 保 +U+4FDE: yú # 俞 +U+4FDF: qí # 俟 ?-> sì +U+4FE1: xìn # 信 +U+4FE3: yǔ # 俣 +U+4FE6: chóu # 俦 +U+4FE8: yǎn # 俨 +U+4FE9: liǎ # 俩 ?-> liǎng +U+4FEA: lì # 俪 +U+4FEB: lái # 俫 +U+4FED: jiǎn # 俭 +U+4FEE: xiū # 修 +U+4FEF: fǔ # 俯 +U+4FF1: jù # 俱 +U+4FF3: pái # 俳 +U+4FF5: biào # 俵 +U+4FF6: chù # 俶 +U+4FF8: fèng # 俸 +U+4FFA: ǎn # 俺 +U+4FFE: bǐ # 俾 +U+500C: guān # 倌 +U+500D: bèi # 倍 +U+500F: shū # 倏 +U+5012: dào # 倒 ?-> dǎo +U+5013: tán # 倓 +U+5014: jué # 倔 +U+5015: chuí # 倕 +U+5018: tǎng # 倘 +U+5019: hòu # 候 +U+501A: yǐ # 倚 +U+501C: tì # 倜 +U+501E: jìng # 倞 -> liàng +U+501F: jiè # 借 +U+5021: chàng # 倡 +U+5025: kōng # 倥 +U+5026: juàn # 倦 +U+5027: zōng # 倧 +U+5028: jù # 倨 +U+5029: qiàn # 倩 +U+502A: ní # 倪 +U+502C: zhuō # 倬 +U+502D: wō # 倭 +U+502E: luǒ # 倮 +U+5034: bèn # 倴 +U+503A: zhài # 债 +U+503B: yē # 倻 +U+503C: zhí # 值 +U+503E: qīng # 倾 +U+5041: chēng # 偁 +U+5043: yǎn # 偃 +U+5047: jiǎ # 假 +U+5048: jì # 偈 +U+504C: ruò # 偌 +U+504E: wēi # 偎 +U+504F: piān # 偏 +U+5053: wò # 偓 +U+5055: xié # 偕 +U+505A: zuò # 做 +U+505C: tíng # 停 +U+5061: zhàn # 偡 +U+5065: jiàn # 健 +U+506C: zǒng # 偬 +U+506D: miǎn # 偭 +U+5070: xiè # 偰 +U+5072: cāi # 偲 +U+5076: ǒu # 偶 +U+5077: tōu # 偷 +U+507B: lóu # 偻 -> lǚ +U+507E: fèn # 偾 +U+507F: cháng # 偿 +U+5080: guī # 傀 -> kuǐ +U+5083: sù # 傃 +U+5085: fù # 傅 +U+5088: lì # 傈 +U+5089: nù # 傉 +U+508D: bàng # 傍 +U+5092: xī # 傒 +U+5095: jué # 傕 -> què +U+50A3: dǎi # 傣 +U+50A5: tǎng # 傥 +U+50A7: bīn # 傧 +U+50A8: chǔ # 储 +U+50A9: nuó # 傩 +U+50AC: cuī # 催 +U+50B2: ào # 傲 +U+50BA: chì # 傺 +U+50BB: shǎ # 傻 +U+50C7: lù # 僇 +U+50CE: zhuàn # 僎 +U+50CF: xiàng # 像 +U+50D4: zǔn # 僔 +U+50D6: xī # 僖 +U+50DA: liáo # 僚 +U+50E6: jiù # 僦 +U+50E7: sēng # 僧 +U+50EC: jiāo # 僬 +U+50ED: jiàn # 僭 +U+50EE: tóng # 僮 +U+50F0: bó # 僰 +U+50F3: sù # 僳 +U+50F5: jiāng # 僵 +U+50FB: pì # 僻 +U+5106: jǐng # 儆 +U+5107: xuān # 儇 +U+510B: dān # 儋 +U+5112: rú # 儒 +U+5121: lěi # 儡 +U+5126: biāo # 儦 +U+5133: chán # 儳 +U+5134: ráng # 儴 +U+513F: ér # 儿 +U+5140: wù # 兀 +U+5141: yǔn # 允 +U+5143: yuán # 元 +U+5144: xiōng # 兄 +U+5145: chōng # 充 +U+5146: zhào # 兆 +U+5148: xiān # 先 +U+5149: guāng # 光 +U+514B: kè # 克 +U+514D: miǎn # 免 +U+5151: duì # 兑 +U+5154: tù # 兔 +U+5155: sì # 兕 +U+5156: yǎn # 兖 +U+515A: dǎng # 党 +U+515C: dōu # 兜 +U+5162: jīng # 兢 +U+5165: rù # 入 +U+5168: quán # 全 +U+516B: bā # 八 +U+516C: gōng # 公 +U+516D: liù # 六 +U+516E: xī # 兮 +U+5170: lán # 兰 +U+5171: gòng # 共 +U+5173: guān # 关 +U+5174: xīng # 兴 -> xìng +U+5175: bīng # 兵 +U+5176: qí # 其 +U+5177: jù # 具 +U+5178: diǎn # 典 +U+5179: zī # 兹 +U+517B: yǎng # 养 +U+517C: jiān # 兼 +U+517D: shòu # 兽 +U+5180: jì # 冀 +U+5181: chǎn # 冁 +U+5185: nèi # 内 +U+5188: gāng # 冈 +U+5189: rǎn # 冉 +U+518C: cè # 册 +U+518D: zài # 再 +U+518F: jiǒng # 冏 +U+5192: mào # 冒 +U+5194: xǔ # 冔 -> xú +U+5195: miǎn # 冕 +U+5197: rǒng # 冗 +U+5199: xiě # 写 +U+519B: jūn # 军 +U+519C: nóng # 农 +U+51A0: guān # 冠 +U+51A2: zhǒng # 冢 +U+51A4: yuān # 冤 +U+51A5: míng # 冥 +U+51AC: dōng # 冬 +U+51AE: gāng # 冮 +U+51AF: féng # 冯 +U+51B0: bīng # 冰 +U+51B1: hù # 冱 +U+51B2: chōng # 冲 +U+51B3: jué # 决 +U+51B5: kuàng # 况 +U+51B6: yě # 冶 +U+51B7: lěng # 冷 +U+51BB: dòng # 冻 +U+51BC: xiǎn # 冼 +U+51BD: liè # 冽 +U+51C0: jìng # 净 +U+51C4: qī # 凄 +U+51C6: zhǔn # 准 +U+51C7: sōng # 凇 +U+51C9: liáng # 凉 +U+51CB: diāo # 凋 +U+51CC: líng # 凌 +U+51CF: jiǎn # 减 +U+51D1: còu # 凑 +U+51D3: lì # 凓 +U+51D8: sī # 凘 +U+51DB: lǐn # 凛 +U+51DD: níng # 凝 +U+51E0: jǐ # 几 -> jī +U+51E1: fán # 凡 +U+51E4: fèng # 凤 +U+51EB: fú # 凫 +U+51ED: píng # 凭 +U+51EF: kǎi # 凯 +U+51F0: huáng # 凰 +U+51F3: dèng # 凳 +U+51F6: xiōng # 凶 +U+51F8: tū # 凸 +U+51F9: āo # 凹 +U+51FA: chū # 出 +U+51FB: jī # 击 +U+51FC: dàng # 凼 +U+51FD: hán # 函 +U+51FF: záo # 凿 +U+5200: dāo # 刀 +U+5201: diāo # 刁 +U+5203: rèn # 刃 +U+5206: fēn # 分 +U+5207: qiè # 切 -> qiē +U+5208: yì # 刈 +U+520A: kān # 刊 +U+520D: chú # 刍 +U+520E: wěn # 刎 +U+5211: xíng # 刑 +U+5212: huà # 划 -> huá +U+5216: yuè # 刖 +U+5217: liè # 列 +U+5218: liú # 刘 +U+5219: zé # 则 +U+521A: gāng # 刚 +U+521B: chuàng # 创 +U+521D: chū # 初 +U+5220: shān # 删 +U+5224: pàn # 判 +U+5228: páo # 刨 +U+5229: lì # 利 +U+522B: bié # 别 +U+522C: chǎn # 刬 +U+522D: jǐng # 刭 +U+522E: guā # 刮 +U+5230: dào # 到 +U+5233: kū # 刳 +U+5236: zhì # 制 +U+5237: shuā # 刷 +U+5238: quàn # 券 +U+5239: shā # 刹 -> chà +U+523A: cì # 刺 +U+523B: kè # 刻 +U+523D: guì # 刽 +U+523F: guì # 刿 +U+5240: kǎi # 剀 +U+5241: duò # 剁 +U+5242: jì # 剂 +U+5243: tì # 剃 +U+5245: lóu # 剅 +U+524A: xuē # 削 -> xiāo +U+524B: kè # 剋 -> kēi +U+524C: lá # 剌 -> là +U+524D: qián # 前 +U+5250: guǎ # 剐 +U+5251: jiàn # 剑 +U+5254: tī # 剔 +U+5255: fèi # 剕 +U+5256: pōu # 剖 +U+525C: wān # 剜 +U+525E: jī # 剞 +U+525F: duō # 剟 +U+5261: shàn # 剡 -> yǎn +U+5265: bō # 剥 -> bāo +U+5267: jù # 剧 +U+5269: shèng # 剩 +U+526A: jiǎn # 剪 +U+526F: fù # 副 +U+5272: gē # 割 +U+527D: piāo # 剽 +U+527F: jiǎo # 剿 +U+5281: qiāo # 劁 +U+5282: jué # 劂 +U+5284: zhā # 劄 +U+5288: pī # 劈 +U+5290: huō # 劐 +U+5293: yì # 劓 +U+529B: lì # 力 +U+529D: quàn # 劝 +U+529E: bàn # 办 +U+529F: gōng # 功 +U+52A0: jiā # 加 +U+52A1: wù # 务 +U+52A2: mài # 劢 +U+52A3: liè # 劣 +U+52A8: dòng # 动 +U+52A9: zhù # 助 +U+52AA: nǔ # 努 +U+52AB: jié # 劫 +U+52AC: qú # 劬 +U+52AD: shào # 劭 +U+52B1: lì # 励 +U+52B2: jìn # 劲 +U+52B3: láo # 劳 +U+52BC: jié # 劼 +U+52BE: hé # 劾 +U+52BF: shì # 势 +U+52C3: bó # 勃 +U+52C7: yǒng # 勇 +U+52C9: miǎn # 勉 +U+52CB: xūn # 勋 +U+52CD: qíng # 勍 +U+52D0: měng # 勐 +U+52D2: lēi # 勒 -> lè +U+52D4: miǎn # 勔 +U+52D6: xù # 勖 +U+52D8: kān # 勘 +U+52DA: yì # 勚 +U+52DF: mù # 募 +U+52E0: lù # 勠 +U+52E4: qín # 勤 +U+52F0: xié # 勰 +U+52FA: sháo # 勺 +U+52FE: gōu # 勾 +U+52FF: wù # 勿 +U+5300: yún # 匀 +U+5305: bāo # 包 +U+5306: cōng # 匆 +U+5308: xiōng # 匈 +U+530D: pú # 匍 +U+530F: páo # 匏 +U+5310: fú # 匐 +U+5315: bǐ # 匕 +U+5316: huà # 化 +U+5317: běi # 北 +U+5319: shi # 匙 -> chí +U+531C: yí # 匜 +U+531D: zā # 匝 +U+5320: jiàng # 匠 +U+5321: kuāng # 匡 +U+5323: xiá # 匣 +U+5326: guǐ # 匦 +U+532A: fěi # 匪 +U+532E: kuì # 匮 +U+5339: pǐ # 匹 +U+533A: qū # 区 +U+533B: yī # 医 +U+533C: kē # 匼 +U+533E: biǎn # 匾 +U+533F: nì # 匿 +U+5341: shí # 十 +U+5343: qiān # 千 +U+5345: sà # 卅 +U+5347: shēng # 升 +U+5348: wǔ # 午 +U+5349: huì # 卉 +U+534A: bàn # 半 +U+534E: huá # 华 +U+534F: xié # 协 +U+5351: bēi # 卑 +U+5352: zú # 卒 +U+5353: zhuó # 卓 +U+5355: dān # 单 +U+5356: mài # 卖 +U+5357: nán # 南 +U+535A: bó # 博 +U+535C: bo # 卜 -> bǔ +U+535E: biàn # 卞 +U+535F: bǔ # 卟 +U+5360: zhàn # 占 -> zhān +U+5361: kǎ # 卡 +U+5362: lú # 卢 +U+5363: yǒu # 卣 +U+5364: lǔ # 卤 +U+5366: guà # 卦 +U+5367: wò # 卧 +U+536B: wèi # 卫 +U+536C: áng # 卬 -> yǎng +U+536E: zhī # 卮 +U+536F: mǎo # 卯 +U+5370: yìn # 印 +U+5371: wēi # 危 +U+5373: jí # 即 +U+5374: què # 却 +U+5375: luǎn # 卵 +U+5377: juǎn # 卷 -> juàn +U+5378: xiè # 卸 +U+537A: jǐn # 卺 +U+537F: qīng # 卿 +U+5382: chǎng # 厂 +U+5384: è # 厄 +U+5385: tīng # 厅 +U+5386: lì # 历 +U+5389: lì # 厉 +U+538B: yā # 压 +U+538C: yàn # 厌 +U+538D: shè # 厍 +U+5395: cè # 厕 +U+5396: páng # 厖 -> máng +U+5398: lí # 厘 +U+539A: hòu # 厚 +U+539D: cuò # 厝 +U+539F: yuán # 原 +U+53A2: xiāng # 厢 +U+53A3: yǎn # 厣 +U+53A5: jué # 厥 +U+53A6: shà # 厦 +U+53A8: chú # 厨 +U+53A9: jiù # 厩 +U+53AE: sī # 厮 +U+53BB: qù # 去 +U+53BE: dū # 厾 +U+53BF: xiàn # 县 +U+53C1: sān # 叁 +U+53C2: cān # 参 +U+53C6: ài # 叆 +U+53C7: dài # 叇 +U+53C8: yòu # 又 +U+53C9: chā # 叉 +U+53CA: jí # 及 +U+53CB: yǒu # 友 +U+53CC: shuāng # 双 +U+53CD: fǎn # 反 +U+53D1: fā # 发 +U+53D4: shū # 叔 +U+53D5: zhuó # 叕 +U+53D6: qǔ # 取 +U+53D7: shòu # 受 +U+53D8: biàn # 变 +U+53D9: xù # 叙 +U+53DA: jiǎ # 叚 +U+53DB: pàn # 叛 +U+53DF: sǒu # 叟 +U+53E0: dié # 叠 +U+53E3: kǒu # 口 +U+53E4: gǔ # 古 +U+53E5: jù # 句 +U+53E6: lìng # 另 +U+53E8: dāo # 叨 -> tāo +U+53E9: kòu # 叩 +U+53EA: zhǐ # 只 -> zhī +U+53EB: jiào # 叫 +U+53EC: zhào # 召 +U+53ED: bā # 叭 +U+53EE: dīng # 叮 +U+53EF: kě # 可 +U+53F0: tái # 台 +U+53F1: chì # 叱 +U+53F2: shǐ # 史 +U+53F3: yòu # 右 +U+53F5: pǒ # 叵 +U+53F6: yè # 叶 +U+53F7: hào # 号 +U+53F8: sī # 司 +U+53F9: tàn # 叹 +U+53FB: lè # 叻 +U+53FC: diāo # 叼 +U+53FD: jī # 叽 +U+5401: xū # 吁 +U+5403: chī # 吃 +U+5404: gè # 各 +U+5406: yāo # 吆 +U+5408: hé # 合 +U+5409: jí # 吉 +U+540A: diào # 吊 +U+540C: tóng # 同 +U+540D: míng # 名 +U+540E: hòu # 后 +U+540F: lì # 吏 +U+5410: tǔ # 吐 +U+5411: xiàng # 向 +U+5412: zhā # 吒 -> zhà +U+5413: xià # 吓 +U+5415: lǚ # 吕 +U+5416: yā # 吖 +U+5417: ma # 吗 -> má +U+541B: jūn # 君 +U+541D: lìn # 吝 +U+541E: tūn # 吞 +U+541F: yín # 吟 +U+5420: fèi # 吠 +U+5421: bǐ # 吡 -> pǐ +U+5423: qìn # 吣 +U+5426: fǒu # 否 +U+5427: ba # 吧 -> bā +U+5428: dūn # 吨 +U+5429: fēn # 吩 +U+542B: hán # 含 +U+542C: tīng # 听 +U+542D: kēng # 吭 -> háng +U+542E: shǔn # 吮 +U+542F: qǐ # 启 +U+5431: zhī # 吱 +U+5432: yǐn # 吲 +U+5434: wú # 吴 +U+5435: chǎo # 吵 +U+5438: xī # 吸 +U+5439: chuī # 吹 +U+543B: wěn # 吻 +U+543C: hǒu # 吼 +U+543D: hōng # 吽 -> hǒu +U+543E: wú # 吾 +U+5440: ya # 呀 -> yā +U+5443: è # 呃 +U+5446: dāi # 呆 +U+5447: qǐ # 呇 +U+5448: chéng # 呈 +U+544A: gào # 告 +U+544B: fū # 呋 +U+5450: nà # 呐 +U+5452: wǔ # 呒 -> fǔ +U+5453: yì # 呓 +U+5454: dāi # 呔 +U+5455: ǒu # 呕 +U+5456: lì # 呖 +U+5457: bei # 呗 +U+5458: yuán # 员 +U+5459: guō # 呙 -> wāi +U+545B: qiāng # 呛 +U+545C: wū # 呜 +U+5462: ne # 呢 +U+5463: ḿ # 呣 +U+5464: lìng # 呤 -> líng +U+5466: yōu # 呦 +U+5468: zhōu # 周 +U+5471: gū # 呱 -> guā +U+5472: cī # 呲 -> zī +U+5473: wèi # 味 +U+5475: hē # 呵 +U+5476: náo # 呶 +U+5477: gā # 呷 -> xiā +U+5478: pēi # 呸 +U+547B: shēn # 呻 +U+547C: hū # 呼 +U+547D: mìng # 命 +U+5480: jǔ # 咀 +U+5482: zā # 咂 +U+5484: duō # 咄 +U+5486: páo # 咆 +U+5487: bié # 咇 -> bì +U+5489: yāng # 咉 -> yǎng +U+548B: zǎ # 咋 +U+548C: hé # 和 +U+548D: hāi # 咍 +U+548E: jiù # 咎 +U+548F: yǒng # 咏 +U+5490: fù # 咐 +U+5492: zhòu # 咒 +U+5494: kā # 咔 -> kǎ +U+5495: gū # 咕 +U+5496: kā # 咖 +U+5499: lóng # 咙 +U+549A: dōng # 咚 +U+549B: níng # 咛 +U+549D: sī # 咝 +U+54A1: èr # 咡 +U+54A3: guāng # 咣 +U+54A4: zhà # 咤 +U+54A5: xì # 咥 -> dié +U+54A6: yí # 咦 +U+54A7: liě # 咧 +U+54A8: zī # 咨 +U+54A9: miē # 咩 +U+54AA: mī # 咪 +U+54AB: zhǐ # 咫 +U+54AC: yǎo # 咬 +U+54AF: gē # 咯 -> lo +U+54B1: zán # 咱 +U+54B3: ké # 咳 +U+54B4: huī # 咴 +U+54B8: xián # 咸 +U+54BA: xuǎn # 咺 +U+54BB: xiū # 咻 +U+54BD: yàn # 咽 -> yān +U+54BF: yī # 咿 +U+54C0: āi # 哀 +U+54C1: pǐn # 品 +U+54C2: shěn # 哂 +U+54C3: tóng # 哃 +U+54C4: hǒng # 哄 +U+54C6: duō # 哆 +U+54C7: wa # 哇 -> wā +U+54C8: hā # 哈 +U+54C9: zāi # 哉 +U+54CC: pài # 哌 +U+54CD: xiǎng # 响 +U+54CE: āi # 哎 +U+54CF: gén # 哏 +U+54D0: kuāng # 哐 +U+54D1: yǎ # 哑 +U+54D2: dá # 哒 -> dā +U+54D3: xiāo # 哓 +U+54D4: bì # 哔 +U+54D5: huì # 哕 -> yuě +U+54D7: huā # 哗 -> huá +U+54D9: kuài # 哙 +U+54DA: duǒ # 哚 +U+54DD: nóng # 哝 +U+54DE: mōu # 哞 +U+54DF: yō # 哟 +U+54E2: lòng # 哢 +U+54E5: gē # 哥 +U+54E6: ó # 哦 +U+54E7: chī # 哧 +U+54E8: shào # 哨 +U+54E9: lī # 哩 -> li +U+54EA: nǎ # 哪 +U+54ED: kū # 哭 +U+54EE: xiāo # 哮 -> xiào +U+54F1: bō # 哱 -> pò +U+54F2: zhé # 哲 +U+54F3: zhā # 哳 +U+54FA: bǔ # 哺 +U+54FC: hēng # 哼 +U+54FD: gěng # 哽 +U+54FF: gě # 哿 +U+5501: yàn # 唁 +U+5506: suō # 唆 +U+5507: chún # 唇 +U+5509: āi # 唉 +U+550F: xī # 唏 +U+5510: táng # 唐 +U+5511: zuò # 唑 +U+5514: wú # 唔 -> wù +U+551B: mà # 唛 +U+551D: gòng # 唝 +U+5520: láo # 唠 -> lào +U+5522: suǒ # 唢 +U+5523: zào # 唣 +U+5524: huàn # 唤 +U+5527: jī # 唧 +U+552A: fěng # 唪 +U+552C: hǔ # 唬 +U+552E: shòu # 售 +U+552F: wéi # 唯 +U+5530: shuā # 唰 +U+5531: chàng # 唱 +U+5533: lì # 唳 +U+5535: ǎn # 唵 +U+5537: yō # 唷 +U+553C: shà # 唼 +U+553E: tuò # 唾 +U+553F: hū # 唿 +U+5541: zhāo # 啁 -> zhōu +U+5543: kěn # 啃 +U+5544: zhuó # 啄 +U+5546: shāng # 商 +U+5549: lín # 啉 -> lán +U+554A: a # 啊 -> ā +U+5550: cuì # 啐 +U+5555: táo # 啕 +U+5556: dàn # 啖 +U+555C: chuài # 啜 -> chuò +U+5561: fēi # 啡 +U+5564: pí # 啤 +U+5565: shá # 啥 +U+5566: la # 啦 -> lā +U+5567: zé # 啧 +U+556A: pā # 啪 +U+556B: zhě # 啫 +U+556C: sè # 啬 +U+556D: zhuàn # 啭 +U+556E: niè # 啮 +U+5570: luō # 啰 +U+5574: chǎn # 啴 -> tān +U+5575: bō # 啵 -> bo +U+5576: dìng # 啶 +U+5577: lāng # 啷 +U+5578: xiào # 啸 +U+557B: chì # 啻 +U+557C: tí # 啼 +U+557E: jiū # 啾 +U+5580: kā # 喀 +U+5581: yóng # 喁 +U+5582: wèi # 喂 +U+5583: nán # 喃 +U+5584: shàn # 善 +U+5586: zhé # 喆 +U+5587: lǎ # 喇 +U+5588: jiē # 喈 +U+5589: hóu # 喉 +U+558A: hǎn # 喊 +U+558B: dié # 喋 +U+558F: nuò # 喏 +U+5591: yīn # 喑 +U+5594: ō # 喔 +U+5598: chuǎn # 喘 +U+5599: huì # 喙 +U+559C: xǐ # 喜 +U+559D: hē # 喝 +U+559F: kuì # 喟 +U+55A4: huáng # 喤 +U+55A7: xuān # 喧 +U+55B1: lí # 喱 +U+55B3: zhā # 喳 +U+55B5: miāo # 喵 +U+55B7: pēn # 喷 +U+55B9: kuí # 喹 +U+55BB: yù # 喻 +U+55BD: lóu # 喽 +U+55BE: kù # 喾 +U+55C4: á # 嗄 -> shà +U+55C5: xiù # 嗅 +U+55C9: sù # 嗉 +U+55CC: ài # 嗌 -> yì +U+55CD: suō # 嗍 +U+55D0: hài # 嗐 +U+55D1: kē # 嗑 +U+55D2: dā # 嗒 -> tà +U+55D3: sǎng # 嗓 +U+55D4: chēn # 嗔 +U+55D6: sōu # 嗖 +U+55DC: shì # 嗜 +U+55DD: gé # 嗝 +U+55DE: zī # 嗞 +U+55DF: jiē # 嗟 +U+55E1: wēng # 嗡 +U+55E3: sì # 嗣 +U+55E4: chī # 嗤 +U+55E5: háo # 嗥 +U+55E6: suo # 嗦 -> suō +U+55E8: hāi # 嗨 +U+55EA: qín # 嗪 +U+55EB: niè # 嗫 +U+55EC: hē # 嗬 +U+55EF: ń # 嗯 -> ǹg +U+55F2: diē # 嗲 -> diǎ +U+55F3: āi # 嗳 -> ǎi +U+55F5: tōng # 嗵 +U+55F7: áo # 嗷 +U+55FD: sòu # 嗽 +U+55FE: sǒu # 嗾 +U+5600: dí # 嘀 +U+5601: qī # 嘁 +U+5608: cáo # 嘈 +U+5609: jiā # 嘉 +U+560C: piào # 嘌 -> piāo +U+560E: gā # 嘎 +U+560F: gǔ # 嘏 +U+5618: xū # 嘘 +U+561A: dē # 嘚 +U+561B: ma # 嘛 -> má +U+561E: lei # 嘞 +U+561F: dū # 嘟 +U+5621: tāng # 嘡 +U+5623: bēng # 嘣 +U+5624: yīng # 嘤 +U+5627: mì # 嘧 +U+562C: chuài # 嘬 -> zuō +U+562D: pēng # 嘭 +U+5631: zhǔ # 嘱 +U+5632: cháo # 嘲 +U+5634: zuǐ # 嘴 +U+5636: sī # 嘶 +U+5639: liáo # 嘹 +U+563B: xī # 嘻 +U+563F: hēi # 嘿 +U+5640: xùn # 噀 +U+5642: zǔn # 噂 -> zūn +U+5647: chuáng # 噇 +U+564C: cēng # 噌 +U+564D: jiào # 噍 +U+564E: yē # 噎 +U+5654: dēng # 噔 +U+5657: pū # 噗 +U+5658: juē # 噘 +U+5659: qín # 噙 +U+565C: lū # 噜 +U+5662: ō # 噢 +U+5664: jìn # 噤 +U+5668: qì # 器 +U+5669: è # 噩 +U+566A: zào # 噪 +U+566B: yī # 噫 +U+566C: shì # 噬 +U+5671: jué # 噱 +U+5676: gá # 噶 +U+567B: sāi # 噻 +U+567C: pī # 噼 +U+5684: huō # 嚄 -> huò +U+5685: rú # 嚅 +U+5686: hāo # 嚆 +U+568E: háo # 嚎 +U+568F: tì # 嚏 +U+5693: cā # 嚓 +U+569A: yín # 嚚 +U+56A3: xiāo # 嚣 +U+56AD: pǐ # 嚭 +U+56AF: huò # 嚯 +U+56B7: rǎng # 嚷 +U+56BC: jué # 嚼 -> jiáo +U+56CA: náng # 囊 +U+56D4: nāng # 囔 +U+56DA: qiú # 囚 +U+56DB: sì # 四 +U+56DE: huí # 回 +U+56DF: xìn # 囟 +U+56E0: yīn # 因 +U+56E1: nān # 囡 +U+56E2: tuán # 团 +U+56E4: dùn # 囤 +U+56EB: hú # 囫 +U+56ED: yuán # 园 +U+56F0: kùn # 困 +U+56F1: cōng # 囱 +U+56F4: wéi # 围 +U+56F5: lún # 囵 +U+56F7: qūn # 囷 +U+56F9: líng # 囹 +U+56FA: gù # 固 +U+56FD: guó # 国 +U+56FE: tú # 图 +U+56FF: yòu # 囿 +U+5703: pǔ # 圃 +U+5704: yǔ # 圄 +U+5706: yuán # 圆 +U+5708: quān # 圈 +U+5709: yǔ # 圉 +U+570A: qīng # 圊 +U+570C: chuán # 圌 +U+5710: kū # 圐 +U+5719: lüè # 圙 +U+571C: huán # 圜 +U+571F: tǔ # 土 +U+5722: tǐng # 圢 +U+5723: shèng # 圣 +U+5728: zài # 在 +U+5729: wéi # 圩 +U+572A: gē # 圪 +U+572B: yù # 圫 +U+572C: wū # 圬 +U+572D: guī # 圭 +U+572E: pǐ # 圮 +U+572F: yí # 圯 +U+5730: dì # 地 +U+5732: qiān # 圲 +U+5733: zhèn # 圳 +U+5739: kuàng # 圹 +U+573A: chǎng # 场 -> cháng +U+573B: qí # 圻 +U+573E: jī # 圾 +U+5740: zhǐ # 址 +U+5742: bǎn # 坂 +U+5747: jūn # 均 +U+5749: tún # 坉 +U+574A: fāng # 坊 +U+574B: bèn # 坋 +U+574C: bèn # 坌 +U+574D: tān # 坍 +U+574E: kǎn # 坎 +U+574F: huài # 坏 +U+5750: zuò # 坐 +U+5751: kēng # 坑 +U+5752: bì # 坒 +U+5757: kuài # 块 +U+575A: jiān # 坚 +U+575B: tán # 坛 +U+575C: lì # 坜 +U+575D: bà # 坝 +U+575E: wù # 坞 +U+575F: fén # 坟 +U+5760: zhuì # 坠 +U+5761: pō # 坡 +U+5764: kūn # 坤 +U+5765: qū # 坥 +U+5766: tǎn # 坦 +U+5768: tuó # 坨 +U+5769: gān # 坩 +U+576A: píng # 坪 +U+576B: diàn # 坫 +U+576C: guà # 坬 +U+576D: ní # 坭 +U+576F: pī # 坯 +U+5770: jiōng # 坰 +U+5773: ào # 坳 +U+5777: kě # 坷 -> kē +U+577B: chí # 坻 -> dǐ +U+577C: chè # 坼 +U+577D: líng # 坽 +U+5782: chuí # 垂 +U+5783: lā # 垃 +U+5784: lǒng # 垄 +U+5786: lú # 垆 +U+5788: dài # 垈 +U+578B: xíng # 型 +U+578C: dòng # 垌 +U+578D: jì # 垍 +U+578E: hè # 垎 +U+578F: lǜ # 垏 +U+5792: lěi # 垒 +U+5793: gāi # 垓 +U+5795: hòu # 垕 +U+5799: guāng # 垙 +U+579A: yáo # 垚 +U+579B: duǒ # 垛 +U+579E: chá # 垞 +U+579F: yáng # 垟 +U+57A0: yín # 垠 +U+57A1: fá # 垡 +U+57A2: gòu # 垢 +U+57A3: yuán # 垣 +U+57A4: dié # 垤 +U+57A6: kěn # 垦 +U+57A7: shǎng # 垧 +U+57A9: è # 垩 +U+57AB: diàn # 垫 +U+57AD: yā # 垭 +U+57AE: kuǎ # 垮 +U+57AF: da # 垯 -> dá +U+57B1: dàng # 垱 +U+57B2: kǎi # 垲 +U+57B4: nǎo # 垴 +U+57B5: ǎn # 垵 +U+57B8: yuàn # 垸 +U+57BA: fū # 垺 -> póu +U+57BE: hàn # 垾 +U+57BF: xù # 垿 +U+57C2: gěng # 埂 +U+57C3: āi # 埃 +U+57C6: què # 埆 +U+57C7: yǒng # 埇 +U+57CB: mái # 埋 +U+57CC: làng # 埌 +U+57CE: chéng # 城 +U+57CF: shān # 埏 -> yán +U+57D2: liè # 埒 +U+57D4: pǔ # 埔 +U+57D5: chéng # 埕 +U+57D7: bù # 埗 +U+57D8: shí # 埘 +U+57D9: xūn # 埙 +U+57DA: guō # 埚 +U+57DD: niàn # 埝 +U+57DF: yù # 域 +U+57E0: bù # 埠 +U+57E4: pí # 埤 +U+57EA: kōng # 埪 +U+57EB: chǒng # 埫 +U+57ED: dài # 埭 +U+57EF: ǎn # 埯 +U+57F4: zhí # 埴 +U+57F5: duǒ # 埵 +U+57F8: yì # 埸 +U+57F9: péi # 培 +U+57FA: jī # 基 +U+57FC: qí # 埼 +U+57FD: sào # 埽 +U+5802: táng # 堂 +U+5803: kūn # 堃 +U+5806: duī # 堆 +U+5807: jǐn # 堇 -> jīn +U+5809: yù # 堉 +U+580B: péng # 堋 +U+580C: gù # 堌 +U+580D: tù # 堍 +U+580E: lèng # 堎 +U+5810: yá # 堐 +U+5811: qiàn # 堑 +U+5815: duò # 堕 +U+5819: yīn # 堙 +U+581E: dié # 堞 +U+5820: hòu # 堠 +U+5821: bǎo # 堡 +U+5824: dī # 堤 +U+5827: ruán # 堧 +U+5828: yè # 堨 -> è +U+582A: kān # 堪 +U+5830: yàn # 堰 +U+5832: cí # 堲 -> jí +U+5835: dǔ # 堵 +U+583C: hèng # 堼 -> fēng +U+583D: gāng # 堽 +U+583E: chūn # 堾 -> chuǎn +U+5844: léng # 塄 +U+5845: duàn # 塅 +U+5846: wān # 塆 +U+584C: tā # 塌 +U+584D: chéng # 塍 +U+5851: sù # 塑 +U+5854: tǎ # 塔 +U+5858: táng # 塘 +U+585D: bàng # 塝 +U+585E: sāi # 塞 +U+5865: gé # 塥 +U+586B: tián # 填 +U+586C: yuán # 塬 +U+5871: lǎng # 塱 +U+587E: shú # 塾 +U+5880: chí # 墀 +U+5881: màn # 墁 +U+5883: jìng # 境 +U+5885: shù # 墅 +U+5888: kàn # 墈 +U+5889: yōng # 墉 +U+5890: jìn # 墐 +U+5892: shāng # 墒 +U+5893: mù # 墓 +U+5895: yàn # 墕 +U+5898: qián # 墘 +U+5899: qiáng # 墙 +U+589A: liáng # 墚 +U+589E: zēng # 增 +U+589F: xū # 墟 +U+58A1: shàn # 墡 +U+58A3: pú # 墣 +U+58A6: fán # 墦 +U+58A8: mò # 墨 +U+58A9: dūn # 墩 +U+58BC: jī # 墼 +U+58C1: bì # 壁 +U+58C5: yōng # 壅 +U+58D1: hè # 壑 +U+58D5: háo # 壕 +U+58E4: rǎng # 壤 +U+58EB: shì # 士 +U+58EC: rén # 壬 +U+58EE: zhuàng # 壮 +U+58F0: shēng # 声 +U+58F3: ké # 壳 +U+58F6: hú # 壶 +U+58F8: kǔn # 壸 +U+58F9: yī # 壹 +U+5904: chù # 处 -> chǔ +U+5907: bèi # 备 +U+590D: fù # 复 +U+590F: xià # 夏 +U+5910: xiòng # 夐 +U+5914: kuí # 夔 +U+5915: xī # 夕 +U+5916: wài # 外 +U+5919: sù # 夙 +U+591A: duō # 多 +U+591C: yè # 夜 +U+591F: gòu # 够 +U+5924: yín # 夤 +U+5925: huǒ # 夥 +U+5927: dà # 大 +U+5929: tiān # 天 +U+592A: tài # 太 +U+592B: fū # 夫 +U+592C: guài # 夬 +U+592D: yāo # 夭 +U+592E: yāng # 央 +U+592F: hāng # 夯 +U+5931: shī # 失 +U+5934: tóu # 头 +U+5937: yí # 夷 +U+5938: kuā # 夸 +U+5939: jiā # 夹 +U+593A: duó # 夺 +U+593C: kuǎng # 夼 +U+5941: lián # 奁 +U+5942: huàn # 奂 +U+5944: yǎn # 奄 +U+5947: qí # 奇 +U+5948: nài # 奈 +U+5949: fèng # 奉 +U+594B: fèn # 奋 +U+594E: kuí # 奎 +U+594F: zòu # 奏 +U+5951: qì # 契 +U+5953: zhā # 奓 -> shē +U+5954: bēn # 奔 +U+5955: yì # 奕 +U+5956: jiǎng # 奖 +U+5957: tào # 套 +U+5958: zàng # 奘 +U+595A: xī # 奚 +U+5960: diàn # 奠 +U+5961: ào # 奡 +U+5962: shē # 奢 +U+5965: ào # 奥 +U+596D: shì # 奭 +U+5973: nǚ # 女 +U+5974: nú # 奴 +U+5976: nǎi # 奶 +U+5978: jiān # 奸 +U+5979: tā # 她 +U+597D: hǎo # 好 +U+5981: shuò # 妁 +U+5982: rú # 如 +U+5983: fēi # 妃 +U+5984: wàng # 妄 +U+5986: zhuāng # 妆 +U+5987: fù # 妇 +U+5988: mā # 妈 +U+598A: rèn # 妊 +U+598D: yán # 妍 +U+5992: dù # 妒 +U+5993: jì # 妓 +U+5996: yāo # 妖 +U+5997: jìn # 妗 +U+5998: yún # 妘 +U+5999: miào # 妙 +U+599E: niū # 妞 +U+59A3: bǐ # 妣 +U+59A4: yú # 妤 +U+59A5: tuǒ # 妥 +U+59A7: wàn # 妧 +U+59A8: fáng # 妨 +U+59A9: wǔ # 妩 +U+59AA: yù # 妪 +U+59AB: guī # 妫 +U+59AD: bá # 妭 +U+59AE: nī # 妮 +U+59AF: zhóu # 妯 +U+59B2: dá # 妲 +U+59B9: mèi # 妹 +U+59BB: qī # 妻 +U+59BE: qiè # 妾 +U+59C6: mǔ # 姆 +U+59C8: líng # 姈 +U+59CA: zǐ # 姊 +U+59CB: shǐ # 始 +U+59D0: jiě # 姐 +U+59D1: gū # 姑 +U+59D2: sì # 姒 +U+59D3: xìng # 姓 +U+59D4: wěi # 委 +U+59D7: shān # 姗 +U+59D8: pīn # 姘 +U+59DA: yáo # 姚 +U+59DC: jiāng # 姜 +U+59DD: shū # 姝 +U+59DE: jí # 姞 +U+59E3: jiāo # 姣 +U+59E4: gòu # 姤 +U+59E5: lǎo # 姥 -> mǔ +U+59E8: yí # 姨 +U+59EC: jī # 姬 +U+59EE: héng # 姮 +U+59F1: kuā # 姱 +U+59F6: è # 姶 +U+59F9: chà # 姹 +U+59FB: yīn # 姻 +U+59FD: guǐ # 姽 +U+59FF: zī # 姿 +U+5A00: sōng # 娀 +U+5A01: wēi # 威 +U+5A03: wá # 娃 +U+5A04: lóu # 娄 +U+5A05: yà # 娅 +U+5A06: ráo # 娆 +U+5A07: jiāo # 娇 +U+5A08: luán # 娈 +U+5A09: pīng # 娉 +U+5A0C: lǐ # 娌 +U+5A11: suō # 娑 +U+5A13: wěi # 娓 +U+5A18: niáng # 娘 +U+5A1C: nà # 娜 +U+5A1F: juān # 娟 +U+5A20: shēn # 娠 +U+5A23: dì # 娣 +U+5A25: é # 娥 +U+5A29: miǎn # 娩 +U+5A31: yú # 娱 +U+5A32: wā # 娲 +U+5A34: xián # 娴 +U+5A35: jū # 娵 +U+5A36: qǔ # 娶 +U+5A3C: chāng # 娼 +U+5A40: ē # 婀 +U+5A46: pó # 婆 +U+5A49: wǎn # 婉 +U+5A4A: biǎo # 婊 +U+5A4C: shú # 婌 -> shū +U+5A4D: qǐ # 婍 +U+5A55: jié # 婕 +U+5A58: quán # 婘 +U+5A5A: hūn # 婚 +U+5A5E: xìng # 婞 +U+5A60: wān # 婠 +U+5A62: bì # 婢 +U+5A64: chōu # 婤 -> zhōu +U+5A67: jìng # 婧 +U+5A6A: lán # 婪 +U+5A6B: kūn # 婫 -> hùn +U+5A73: huà # 婳 +U+5A74: yīng # 婴 +U+5A75: chán # 婵 +U+5A76: shěn # 婶 +U+5A77: tíng # 婷 +U+5A7A: wù # 婺 +U+5A7B: nàn # 婻 +U+5A7C: chuò # 婼 -> ruò +U+5A7F: xù # 婿 +U+5A82: dì # 媂 +U+5A84: měi # 媄 +U+5A86: ruǎn # 媆 +U+5A92: méi # 媒 +U+5A93: huáng # 媓 +U+5A96: yīng # 媖 +U+5A9A: mèi # 媚 +U+5A9B: yuàn # 媛 +U+5A9E: shì # 媞 -> tí +U+5AAA: ǎo # 媪 +U+5AAD: xū # 媭 +U+5AB1: yáo # 媱 +U+5AB2: pì # 媲 +U+5AB3: xí # 媳 +U+5AB5: yìng # 媵 +U+5AB8: chī # 媸 +U+5ABE: gòu # 媾 +U+5AC1: jià # 嫁 +U+5AC2: sǎo # 嫂 +U+5AC4: yuán # 嫄 +U+5AC9: jí # 嫉 +U+5ACC: xián # 嫌 +U+5AD2: ài # 嫒 +U+5AD4: pín # 嫔 +U+5AD5: yì # 嫕 +U+5AD6: piáo # 嫖 +U+5AD8: léi # 嫘 +U+5ADA: mān # 嫚 -> màn +U+5ADC: zhāng # 嫜 +U+5AE0: lí # 嫠 +U+5AE1: dí # 嫡 +U+5AE3: yān # 嫣 +U+5AE6: cháng # 嫦 +U+5AE9: nèn # 嫩 +U+5AEA: lào # 嫪 +U+5AEB: mó # 嫫 +U+5AED: hù # 嫭 +U+5AF1: qiáng # 嫱 +U+5AFD: liáo # 嫽 +U+5B09: xī # 嬉 +U+5B16: bì # 嬖 +U+5B17: shàn # 嬗 +U+5B1B: huán # 嬛 +U+5B25: tiǎo # 嬥 +U+5B2C: rú # 嬬 +U+5B34: yíng # 嬴 +U+5B37: mā # 嬷 -> mó +U+5B3F: yàn # 嬿 +U+5B40: shuāng # 孀 +U+5B45: qiān # 孅 -> xiān +U+5B50: zi # 子 -> zǐ +U+5B51: jié # 孑 +U+5B53: jué # 孓 +U+5B54: kǒng # 孔 +U+5B55: yùn # 孕 +U+5B56: mā # 孖 -> zī +U+5B57: zì # 字 +U+5B58: cún # 存 +U+5B59: sūn # 孙 +U+5B5A: fú # 孚 +U+5B5B: bèi # 孛 +U+5B5C: zī # 孜 +U+5B5D: xiào # 孝 +U+5B5F: mèng # 孟 +U+5B62: bāo # 孢 +U+5B63: jì # 季 +U+5B64: gū # 孤 +U+5B65: nú # 孥 +U+5B66: xué # 学 +U+5B69: hái # 孩 +U+5B6A: luán # 孪 +U+5B6C: nāo # 孬 +U+5B70: shú # 孰 +U+5B71: càn # 孱 -> chán +U+5B73: zī # 孳 +U+5B75: fū # 孵 +U+5B7A: rú # 孺 +U+5B7D: niè # 孽 +U+5B81: níng # 宁 +U+5B83: tā # 它 +U+5B84: guǐ # 宄 +U+5B85: zhái # 宅 +U+5B87: yǔ # 宇 +U+5B88: shǒu # 守 +U+5B89: ān # 安 +U+5B8B: sòng # 宋 +U+5B8C: wán # 完 +U+5B8F: hóng # 宏 +U+5B93: mì # 宓 +U+5B95: dàng # 宕 +U+5B97: zōng # 宗 +U+5B98: guān # 官 +U+5B99: zhòu # 宙 +U+5B9A: dìng # 定 +U+5B9B: wǎn # 宛 +U+5B9C: yí # 宜 +U+5B9D: bǎo # 宝 +U+5B9E: shí # 实 +U+5BA0: chǒng # 宠 +U+5BA1: shěn # 审 +U+5BA2: kè # 客 +U+5BA3: xuān # 宣 +U+5BA4: shì # 室 +U+5BA5: yòu # 宥 +U+5BA6: huàn # 宦 +U+5BA7: yí # 宧 +U+5BAA: xiàn # 宪 +U+5BAB: gōng # 宫 +U+5BAC: chéng # 宬 +U+5BB0: zǎi # 宰 +U+5BB3: hài # 害 +U+5BB4: yàn # 宴 +U+5BB5: xiāo # 宵 +U+5BB6: jiā # 家 +U+5BB8: chén # 宸 +U+5BB9: róng # 容 +U+5BBD: kuān # 宽 +U+5BBE: bīn # 宾 +U+5BBF: sù # 宿 +U+5BC1: zǎn # 寁 +U+5BC2: jì # 寂 +U+5BC4: jì # 寄 +U+5BC5: yín # 寅 +U+5BC6: mì # 密 +U+5BC7: kòu # 寇 +U+5BCC: fù # 富 +U+5BD0: mèi # 寐 +U+5BD2: hán # 寒 +U+5BD3: yù # 寓 +U+5BDD: qǐn # 寝 +U+5BDE: mò # 寞 +U+5BDF: chá # 察 +U+5BE1: guǎ # 寡 +U+5BE4: wù # 寤 +U+5BE5: liáo # 寥 +U+5BE8: zhài # 寨 +U+5BEE: liáo # 寮 +U+5BF0: huán # 寰 +U+5BF8: cùn # 寸 +U+5BF9: duì # 对 +U+5BFA: sì # 寺 +U+5BFB: xún # 寻 +U+5BFC: dǎo # 导 +U+5BFF: shòu # 寿 +U+5C01: fēng # 封 +U+5C04: shè # 射 +U+5C06: jiāng # 将 +U+5C09: wèi # 尉 +U+5C0A: zūn # 尊 +U+5C0F: xiǎo # 小 +U+5C11: shǎo # 少 +U+5C14: ěr # 尔 +U+5C15: gǎ # 尕 +U+5C16: jiān # 尖 +U+5C18: chén # 尘 +U+5C1A: shàng # 尚 +U+5C1C: gá # 尜 +U+5C1D: cháng # 尝 +U+5C22: yóu # 尢 +U+5C24: yóu # 尤 +U+5C25: liào # 尥 +U+5C27: yáo # 尧 +U+5C28: máng # 尨 -> lóng +U+5C2A: wāng # 尪 +U+5C2C: gà # 尬 +U+5C31: jiù # 就 +U+5C34: gān # 尴 +U+5C38: shī # 尸 +U+5C39: yǐn # 尹 +U+5C3A: chǐ # 尺 +U+5C3B: kāo # 尻 +U+5C3C: ní # 尼 +U+5C3D: jǐn # 尽 -> jìn +U+5C3E: wěi # 尾 +U+5C3F: niào # 尿 +U+5C40: jú # 局 +U+5C41: pì # 屁 +U+5C42: céng # 层 +U+5C43: xì # 屃 +U+5C45: jū # 居 +U+5C48: qū # 屈 +U+5C49: tì # 屉 +U+5C4A: jiè # 届 +U+5C4B: wū # 屋 +U+5C4E: shǐ # 屎 +U+5C4F: píng # 屏 +U+5C50: jī # 屐 +U+5C51: xiè # 屑 +U+5C55: zhǎn # 展 +U+5C59: ē # 屙 +U+5C5E: shǔ # 属 +U+5C60: tú # 屠 +U+5C61: lǚ # 屡 +U+5C63: xǐ # 屣 +U+5C65: lǚ # 履 +U+5C66: jù # 屦 +U+5C6F: tún # 屯 +U+5C71: shān # 山 +U+5C79: yì # 屹 +U+5C7A: qǐ # 屺 +U+5C7C: wù # 屼 +U+5C7E: shēn # 屾 +U+5C7F: yǔ # 屿 +U+5C81: suì # 岁 +U+5C82: qǐ # 岂 +U+5C88: yá # 岈 +U+5C8A: jié # 岊 +U+5C8C: jí # 岌 +U+5C8D: qiān # 岍 +U+5C90: qí # 岐 +U+5C91: cén # 岑 +U+5C94: chà # 岔 +U+5C96: qū # 岖 +U+5C97: gǎng # 岗 +U+5C98: xiàn # 岘 +U+5C99: ào # 岙 +U+5C9A: lán # 岚 +U+5C9B: dǎo # 岛 +U+5C9C: bā # 岜 +U+5C9E: zuò # 岞 +U+5CA0: jù # 岠 +U+5CA2: kě # 岢 +U+5CA3: gǒu # 岣 +U+5CA8: qū # 岨 -> jū +U+5CA9: yán # 岩 +U+5CAB: xiù # 岫 +U+5CAC: jiǎ # 岬 +U+5CAD: lǐng # 岭 +U+5CB1: dài # 岱 +U+5CB3: yuè # 岳 +U+5CB5: hù # 岵 +U+5CB7: mín # 岷 +U+5CB8: àn # 岸 +U+5CBD: dōng # 岽 +U+5CBF: kuī # 岿 +U+5CC1: mǎo # 峁 +U+5CC2: tóng # 峂 +U+5CC3: xué # 峃 +U+5CC4: yì # 峄 +U+5CCB: xún # 峋 +U+5CD2: dòng # 峒 -> tóng +U+5CD7: wéi # 峗 +U+5CD8: huán # 峘 +U+5CD9: zhì # 峙 +U+5CDB: lǐ # 峛 +U+5CE1: xiá # 峡 +U+5CE3: yáo # 峣 +U+5CE4: jiào # 峤 +U+5CE5: zhēng # 峥 +U+5CE6: luán # 峦 +U+5CE7: jiāo # 峧 +U+5CE8: é # 峨 +U+5CEA: yù # 峪 +U+5CED: qiào # 峭 +U+5CF0: fēng # 峰 +U+5CF1: náo # 峱 +U+5CFB: jùn # 峻 +U+5CFF: yǔ # 峿 -> wú +U+5D00: làng # 崀 -> lǎng +U+5D01: kàn # 崁 +U+5D02: láo # 崂 +U+5D03: lái # 崃 +U+5D04: xiǎn # 崄 +U+5D06: kōng # 崆 +U+5D07: chóng # 崇 +U+5D0C: jū # 崌 +U+5D0E: qí # 崎 +U+5D12: zú # 崒 +U+5D14: cuī # 崔 +U+5D16: yá # 崖 +U+5D1A: léng # 崚 -> líng +U+5D1B: jué # 崛 +U+5D1E: guō # 崞 +U+5D1F: yín # 崟 +U+5D21: hán # 崡 +U+5D24: xiáo # 崤 +U+5D26: yān # 崦 +U+5D27: sōng # 崧 +U+5D29: bēng # 崩 +U+5D2D: zhǎn # 崭 +U+5D2E: gù # 崮 +U+5D34: wǎi # 崴 +U+5D36: fēng # 崶 +U+5D3D: zǎi # 崽 +U+5D3E: yǎo # 崾 +U+5D3F: è # 崿 +U+5D41: kān # 嵁 +U+5D45: hán # 嵅 +U+5D47: jī # 嵇 +U+5D4A: shèng # 嵊 +U+5D4B: méi # 嵋 +U+5D4C: qiàn # 嵌 +U+5D4E: yú # 嵎 +U+5D56: chá # 嵖 +U+5D58: róng # 嵘 +U+5D5A: qīn # 嵚 +U+5D5B: yú # 嵛 +U+5D5D: lǒu # 嵝 +U+5D69: sōng # 嵩 +U+5D6B: zī # 嵫 +U+5D6C: wéi # 嵬 +U+5D6F: cuó # 嵯 +U+5D72: niè # 嵲 +U+5D74: jǐ # 嵴 +U+5D82: zhàng # 嶂 +U+5D85: áo # 嶅 +U+5D8D: xí # 嶍 +U+5D92: céng # 嶒 +U+5D93: bō # 嶓 +U+5D99: lín # 嶙 +U+5D9D: dèng # 嶝 +U+5D9F: zūn # 嶟 +U+5DA6: zhān # 嶦 +U+5DB2: xī # 嶲 -> guī +U+5DB7: yí # 嶷 +U+5DC5: diān # 巅 +U+5DC7: xī # 巇 +U+5DC9: chán # 巉 +U+5DCD: wēi # 巍 +U+5DDD: chuān # 川 +U+5DDE: zhōu # 州 +U+5DE1: xún # 巡 +U+5DE2: cháo # 巢 +U+5DE5: gōng # 工 +U+5DE6: zuǒ # 左 +U+5DE7: qiǎo # 巧 +U+5DE8: jù # 巨 +U+5DE9: gǒng # 巩 +U+5DEB: wū # 巫 +U+5DEE: chà # 差 +U+5DEF: qiú # 巯 +U+5DF1: jǐ # 己 +U+5DF2: yǐ # 已 +U+5DF3: sì # 巳 +U+5DF4: bā # 巴 +U+5DF7: xiàng # 巷 +U+5DFD: xùn # 巽 +U+5DFE: jīn # 巾 +U+5E01: bì # 币 +U+5E02: shì # 市 +U+5E03: bù # 布 +U+5E05: shuài # 帅 +U+5E06: fān # 帆 +U+5E08: shī # 师 +U+5E0C: xī # 希 +U+5E0F: wéi # 帏 +U+5E10: zhàng # 帐 +U+5E11: tǎng # 帑 +U+5E14: pèi # 帔 +U+5E15: pà # 帕 +U+5E16: tiē # 帖 -> tiè +U+5E18: lián # 帘 +U+5E19: zhì # 帙 +U+5E1A: zhǒu # 帚 +U+5E1B: bó # 帛 +U+5E1C: zhì # 帜 +U+5E1D: dì # 帝 +U+5E21: píng # 帡 +U+5E26: dài # 带 +U+5E27: zhēn # 帧 +U+5E28: shuì # 帨 +U+5E2D: xí # 席 +U+5E2E: bāng # 帮 +U+5E31: chóu # 帱 +U+5E37: wéi # 帷 +U+5E38: cháng # 常 +U+5E3B: zé # 帻 +U+5E3C: guó # 帼 +U+5E3D: mào # 帽 +U+5E42: mì # 幂 +U+5E44: wò # 幄 +U+5E45: fú # 幅 +U+5E4C: huǎng # 幌 +U+5E54: màn # 幔 +U+5E55: mù # 幕 +U+5E56: biāo # 幖 +U+5E5B: zhàng # 幛 +U+5E5E: fú # 幞 +U+5E61: fān # 幡 +U+5E62: chuáng # 幢 +U+5E6A: méng # 幪 +U+5E72: gàn # 干 -> gān +U+5E73: píng # 平 +U+5E74: nián # 年 +U+5E76: bìng # 并 +U+5E78: xìng # 幸 +U+5E7A: yāo # 幺 +U+5E7B: huàn # 幻 +U+5E7C: yòu # 幼 +U+5E7D: yōu # 幽 +U+5E7F: guǎng # 广 +U+5E84: zhuāng # 庄 +U+5E86: qìng # 庆 +U+5E87: bì # 庇 +U+5E8A: chuáng # 床 +U+5E8B: guǐ # 庋 +U+5E8F: xù # 序 +U+5E90: lú # 庐 +U+5E91: wǔ # 庑 +U+5E93: kù # 库 +U+5E94: yīng # 应 +U+5E95: dǐ # 底 +U+5E96: páo # 庖 +U+5E97: diàn # 店 +U+5E99: miào # 庙 +U+5E9A: gēng # 庚 +U+5E9C: fǔ # 府 +U+5E9E: páng # 庞 +U+5E9F: fèi # 废 +U+5EA0: xiáng # 庠 +U+5EA4: zhì # 庤 +U+5EA5: xiū # 庥 +U+5EA6: dù # 度 +U+5EA7: zuò # 座 +U+5EAD: tíng # 庭 +U+5EB1: chěng # 庱 +U+5EB3: bì # 庳 -> bēi +U+5EB5: ān # 庵 +U+5EB6: shù # 庶 +U+5EB7: kāng # 康 +U+5EB8: yōng # 庸 +U+5EB9: tuǒ # 庹 +U+5EBC: qǐng # 庼 +U+5EBE: yǔ # 庾 +U+5EC6: guī # 廆 +U+5EC9: lián # 廉 +U+5ECA: láng # 廊 +U+5ECB: sōu # 廋 +U+5ED1: jǐn # 廑 +U+5ED2: áo # 廒 +U+5ED3: kuò # 廓 +U+5ED6: liào # 廖 +U+5ED9: yì # 廙 +U+5EDB: chán # 廛 +U+5EE8: xiè # 廨 +U+5EEA: lǐn # 廪 +U+5EF6: yán # 延 +U+5EF7: tíng # 廷 +U+5EFA: jiàn # 建 +U+5EFF: niàn # 廿 +U+5F00: kāi # 开 +U+5F01: biàn # 弁 +U+5F02: yì # 异 +U+5F03: qì # 弃 +U+5F04: nòng # 弄 +U+5F06: jǔ # 弆 +U+5F07: yǎn # 弇 +U+5F08: yì # 弈 +U+5F0A: bì # 弊 +U+5F0B: yì # 弋 +U+5F0F: shì # 式 +U+5F11: shì # 弑 +U+5F13: gōng # 弓 +U+5F15: yǐn # 引 +U+5F17: fú # 弗 +U+5F18: hóng # 弘 +U+5F1B: chí # 弛 +U+5F1F: dì # 弟 +U+5F20: zhāng # 张 +U+5F22: tāo # 弢 +U+5F25: mí # 弥 +U+5F26: xián # 弦 +U+5F27: hú # 弧 +U+5F28: chāo # 弨 +U+5F29: nǔ # 弩 +U+5F2D: mǐ # 弭 +U+5F2F: wān # 弯 +U+5F31: ruò # 弱 +U+5F36: jiàng # 弶 +U+5F38: péng # 弸 +U+5F39: dàn # 弹 +U+5F3A: qiáng # 强 +U+5F3C: bì # 弼 +U+5F40: gòu # 彀 +U+5F52: guī # 归 +U+5F53: dāng # 当 +U+5F55: lù # 录 +U+5F56: tuàn # 彖 +U+5F57: huì # 彗 +U+5F58: zhì # 彘 +U+5F5D: yí # 彝 +U+5F5F: yuē # 彟 -> huò +U+5F62: xíng # 形 +U+5F64: tóng # 彤 +U+5F66: yàn # 彦 +U+5F67: yù # 彧 +U+5F69: cǎi # 彩 +U+5F6A: biāo # 彪 +U+5F6C: bīn # 彬 +U+5F6D: péng # 彭 +U+5F70: zhāng # 彰 +U+5F71: yǐng # 影 +U+5F73: chì # 彳 +U+5F77: páng # 彷 +U+5F79: yì # 役 +U+5F7B: chè # 彻 +U+5F7C: bǐ # 彼 +U+5F80: wǎng # 往 +U+5F81: zhēng # 征 +U+5F82: cú # 徂 +U+5F84: jìng # 径 +U+5F85: dài # 待 +U+5F87: xùn # 徇 +U+5F88: hěn # 很 +U+5F89: yáng # 徉 +U+5F8A: huái # 徊 +U+5F8B: lǜ # 律 +U+5F90: xú # 徐 +U+5F92: tú # 徒 +U+5F95: lái # 徕 -> lài +U+5F97: dé # 得 +U+5F98: pái # 徘 +U+5F99: xǐ # 徙 +U+5F9B: jì # 徛 +U+5F9C: cháng # 徜 +U+5FA1: yù # 御 +U+5FA8: huáng # 徨 +U+5FAA: xún # 循 +U+5FAD: yáo # 徭 +U+5FAE: wēi # 微 +U+5FB5: zhēng # 徵 -> zhǐ +U+5FB7: dé # 德 +U+5FBC: jiǎo # 徼 +U+5FBD: huī # 徽 +U+5FC3: xīn # 心 +U+5FC5: bì # 必 +U+5FC6: yì # 忆 +U+5FC9: dāo # 忉 +U+5FCC: jì # 忌 +U+5FCD: rěn # 忍 +U+5FCF: chàn # 忏 +U+5FD0: tǎn # 忐 +U+5FD1: tè # 忑 +U+5FD2: tè # 忒 +U+5FD6: cǔn # 忖 +U+5FD7: zhì # 志 +U+5FD8: wàng # 忘 +U+5FD9: máng # 忙 +U+5FDD: tiǎn # 忝 +U+5FDE: mín # 忞 -> mǐn +U+5FE0: zhōng # 忠 +U+5FE1: chōng # 忡 +U+5FE4: wǔ # 忤 +U+5FE7: yōu # 忧 +U+5FEA: sōng # 忪 +U+5FEB: kuài # 快 +U+5FED: biàn # 忭 +U+5FEE: zhì # 忮 +U+5FF1: chén # 忱 +U+5FF3: tún # 忳 +U+5FF5: niàn # 念 +U+5FF8: niǔ # 忸 +U+5FFA: xiān # 忺 +U+5FFB: xīn # 忻 +U+5FFD: hū # 忽 +U+5FFE: kài # 忾 +U+5FFF: fèn # 忿 +U+6000: huái # 怀 +U+6001: tài # 态 +U+6002: sǒng # 怂 +U+6003: wǔ # 怃 +U+6004: òu # 怄 +U+6005: chàng # 怅 +U+6006: chuàng # 怆 +U+600A: chāo # 怊 +U+600D: zuò # 怍 +U+600E: zěn # 怎 +U+600F: yàng # 怏 +U+6012: nù # 怒 +U+6014: zhēng # 怔 +U+6015: pà # 怕 +U+6016: bù # 怖 +U+6019: hù # 怙 +U+601B: dá # 怛 +U+601C: lián # 怜 +U+601D: sī # 思 +U+6020: dài # 怠 +U+6021: yí # 怡 +U+6025: jí # 急 +U+6026: pēng # 怦 +U+6027: xìng # 性 +U+6028: yuàn # 怨 +U+6029: ní # 怩 +U+602A: guài # 怪 +U+602B: fú # 怫 +U+602F: qiè # 怯 +U+6035: chù # 怵 +U+603B: zǒng # 总 +U+603C: duì # 怼 +U+603F: yì # 怿 +U+6041: nèn # 恁 +U+6042: xún # 恂 +U+6043: shì # 恃 +U+604B: liàn # 恋 +U+604D: huǎng # 恍 +U+6050: kǒng # 恐 +U+6052: héng # 恒 +U+6053: xī # 恓 +U+6054: jiǎo # 恔 -> xiào +U+6055: shù # 恕 +U+6059: yàng # 恙 +U+605A: huì # 恚 +U+605D: jiá # 恝 +U+6062: huī # 恢 +U+6063: zì # 恣 +U+6064: xù # 恤 +U+6067: nǜ # 恧 +U+6068: hèn # 恨 +U+6069: ēn # 恩 +U+606A: kè # 恪 +U+606B: dòng # 恫 +U+606C: tián # 恬 +U+606D: gōng # 恭 +U+606F: xī # 息 +U+6070: qià # 恰 +U+6073: kěn # 恳 +U+6076: è # 恶 +U+6078: tòng # 恸 +U+6079: yān # 恹 +U+607A: kǎi # 恺 +U+607B: cè # 恻 +U+607C: nǎo # 恼 +U+607D: yùn # 恽 +U+607F: yǒng # 恿 +U+6083: kǔn # 悃 +U+6084: qiāo # 悄 -> qiǎo +U+6086: yù # 悆 +U+6088: jiè # 悈 +U+6089: xī # 悉 +U+608C: tì # 悌 +U+608D: hàn # 悍 +U+6092: yì # 悒 +U+6094: huǐ # 悔 +U+6096: bèi # 悖 +U+609A: sǒng # 悚 +U+609B: quān # 悛 +U+609D: kuī # 悝 +U+609F: wù # 悟 +U+60A0: yōu # 悠 +U+60A2: liàng # 悢 +U+60A3: huàn # 患 +U+60A6: yuè # 悦 +U+60A8: nín # 您 +U+60AB: què # 悫 +U+60AC: xuán # 悬 +U+60AD: qiān # 悭 +U+60AF: mǐn # 悯 +U+60B0: cóng # 悰 +U+60B1: fěi # 悱 +U+60B2: bēi # 悲 +U+60B4: cuì # 悴 +U+60B8: jì # 悸 +U+60BB: xìng # 悻 +U+60BC: dào # 悼 +U+60C5: qíng # 情 +U+60C6: chóu # 惆 +U+60C7: dūn # 惇 +U+60CA: jīng # 惊 +U+60CB: wǎn # 惋 +U+60CE: jì # 惎 +U+60D1: huò # 惑 +U+60D4: tán # 惔 +U+60D5: tì # 惕 +U+60D8: wǎng # 惘 +U+60D9: chuò # 惙 +U+60DA: hū # 惚 +U+60DB: hūn # 惛 +U+60DC: xī # 惜 +U+60DD: chǎng # 惝 +U+60DF: wéi # 惟 +U+60E0: huì # 惠 +U+60E6: diàn # 惦 +U+60E7: jù # 惧 +U+60E8: cǎn # 惨 +U+60E9: chéng # 惩 +U+60EB: bèi # 惫 +U+60EC: qiè # 惬 +U+60ED: cán # 惭 +U+60EE: dàn # 惮 +U+60EF: guàn # 惯 +U+60F0: duò # 惰 +U+60F3: xiǎng # 想 +U+60F4: zhuì # 惴 +U+60F6: huáng # 惶 +U+60F9: rě # 惹 +U+60FA: xīng # 惺 +U+6100: qiǎo # 愀 +U+6101: chóu # 愁 +U+6103: xuān # 愃 +U+6106: qiān # 愆 +U+6108: yù # 愈 +U+6109: yú # 愉 +U+610D: mǐn # 愍 +U+610E: bì # 愎 +U+610F: yì # 意 +U+6110: miǎn # 愐 +U+6114: yīn # 愔 +U+6115: è # 愕 +U+611A: yú # 愚 +U+611F: gǎn # 感 +U+6120: yùn # 愠 +U+6123: lèng # 愣 +U+6124: fèn # 愤 +U+6126: kuì # 愦 +U+6127: kuì # 愧 +U+612B: sù # 愫 +U+612D: qí # 愭 +U+613F: yuàn # 愿 +U+6146: tāo # 慆 +U+6148: cí # 慈 +U+614A: qiàn # 慊 +U+614C: huāng # 慌 +U+614E: shèn # 慎 +U+6151: shè # 慑 +U+6155: mù # 慕 +U+615D: tè # 慝 +U+6162: màn # 慢 +U+6165: zào # 慥 +U+6167: huì # 慧 +U+6168: kǎi # 慨 +U+616C: qín # 慬 +U+616D: yìn # 慭 +U+6170: wèi # 慰 +U+6175: yōng # 慵 +U+6177: kāng # 慷 +U+618B: biē # 憋 +U+618E: zēng # 憎 +U+6194: qiáo # 憔 +U+6195: chéng # 憕 +U+6199: xī # 憙 -> xǐ +U+61A7: chōng # 憧 +U+61A8: hān # 憨 +U+61A9: qì # 憩 +U+61AC: jǐng # 憬 +U+61AD: liǎo # 憭 +U+61B7: chù # 憷 +U+61BA: dàn # 憺 +U+61BE: hàn # 憾 +U+61C2: dǒng # 懂 +U+61C8: xiè # 懈 +U+61CA: ào # 懊 +U+61CB: mào # 懋 +U+61D1: mèn # 懑 +U+61D2: lǎn # 懒 +U+61D4: lǐn # 懔 +U+61E6: nuò # 懦 +U+61F5: měng # 懵 +U+61FF: yì # 懿 +U+6206: gàng # 戆 +U+6208: gē # 戈 +U+620A: wù # 戊 +U+620B: jiān # 戋 +U+620C: xū # 戌 +U+620D: shù # 戍 +U+620E: róng # 戎 +U+620F: xì # 戏 +U+6210: chéng # 成 +U+6211: wǒ # 我 +U+6212: jiè # 戒 +U+6215: qiāng # 戕 +U+6216: huò # 或 +U+6217: qiāng # 戗 +U+6218: zhàn # 战 +U+621A: qī # 戚 +U+621B: jiá # 戛 +U+621F: jǐ # 戟 +U+6221: kān # 戡 +U+6222: jí # 戢 +U+6223: kuí # 戣 +U+6224: gài # 戤 +U+6225: děng # 戥 +U+622A: jié # 截 +U+622C: jiǎn # 戬 +U+622D: yǎn # 戭 +U+622E: lù # 戮 +U+6233: chuō # 戳 +U+6234: dài # 戴 +U+6237: hù # 户 +U+623D: hù # 戽 +U+623E: lì # 戾 +U+623F: fáng # 房 +U+6240: suǒ # 所 +U+6241: biǎn # 扁 +U+6242: diàn # 扂 +U+6243: jiōng # 扃 +U+6245: yí # 扅 +U+6246: yǐ # 扆 +U+6247: shàn # 扇 +U+6248: hù # 扈 +U+6249: fēi # 扉 +U+624A: yǎn # 扊 +U+624B: shǒu # 手 +U+624D: cái # 才 +U+624E: zhā # 扎 -> zā +U+6251: pū # 扑 +U+6252: bā # 扒 +U+6253: dǎ # 打 +U+6254: rēng # 扔 +U+6258: tuō # 托 +U+625B: káng # 扛 +U+625E: gǎn # 扞 -> hàn +U+6263: kòu # 扣 +U+6266: qiān # 扦 +U+6267: zhí # 执 +U+6269: kuò # 扩 +U+626A: mén # 扪 +U+626B: sǎo # 扫 +U+626C: yáng # 扬 +U+626D: niǔ # 扭 +U+626E: bàn # 扮 +U+626F: chě # 扯 +U+6270: rǎo # 扰 +U+6273: bān # 扳 +U+6276: fú # 扶 +U+6279: pī # 批 +U+627A: zhǐ # 扺 +U+627C: è # 扼 +U+627D: dèn # 扽 +U+627E: zhǎo # 找 +U+627F: chéng # 承 +U+6280: jì # 技 +U+6283: biàn # 抃 +U+6284: chāo # 抄 +U+6289: jué # 抉 +U+628A: bǎ # 把 +U+6291: yì # 抑 +U+6292: shū # 抒 +U+6293: zhuā # 抓 +U+6294: póu # 抔 +U+6295: tóu # 投 +U+6296: dǒu # 抖 +U+6297: kàng # 抗 +U+6298: zhé # 折 -> zhē +U+629A: fǔ # 抚 +U+629B: pāo # 抛 +U+629F: tuán # 抟 +U+62A0: kōu # 抠 +U+62A1: lūn # 抡 +U+62A2: qiǎng # 抢 -> qiāng +U+62A4: hù # 护 +U+62A5: bào # 报 +U+62A8: pēng # 抨 +U+62AB: pī # 披 +U+62AC: tái # 抬 +U+62B1: bào # 抱 +U+62B5: dǐ # 抵 +U+62B9: mǒ # 抹 +U+62BB: chēn # 抻 +U+62BC: yā # 押 +U+62BD: chōu # 抽 +U+62BF: mǐn # 抿 +U+62C2: fú # 拂 +U+62C3: zhǎ # 拃 +U+62C4: zhǔ # 拄 +U+62C5: dān # 担 +U+62C6: chāi # 拆 +U+62C7: mǔ # 拇 +U+62C8: niān # 拈 +U+62C9: lā # 拉 +U+62CA: fǔ # 拊 +U+62CC: bàn # 拌 +U+62CD: pāi # 拍 +U+62CE: līn # 拎 +U+62D0: guǎi # 拐 +U+62D2: jù # 拒 +U+62D3: tuò # 拓 +U+62D4: bá # 拔 +U+62D6: tuō # 拖 +U+62D7: ǎo # 拗 +U+62D8: jū # 拘 +U+62D9: zhuō # 拙 +U+62DB: zhāo # 招 +U+62DC: bài # 拜 +U+62DF: nǐ # 拟 +U+62E2: lǒng # 拢 +U+62E3: jiǎn # 拣 +U+62E4: qiá # 拤 -> qiǎ +U+62E5: yōng # 拥 +U+62E6: lán # 拦 +U+62E7: níng # 拧 +U+62E8: bō # 拨 +U+62E9: zé # 择 +U+62EC: kuò # 括 +U+62ED: shì # 拭 +U+62EE: jié # 拮 +U+62EF: zhěng # 拯 +U+62F1: gǒng # 拱 +U+62F3: quán # 拳 +U+62F4: shuān # 拴 +U+62F6: zā # 拶 +U+62F7: kǎo # 拷 +U+62FC: pīn # 拼 +U+62FD: zhuāi # 拽 -> zhuài +U+62FE: shí # 拾 +U+62FF: ná # 拿 +U+6301: chí # 持 +U+6302: guà # 挂 +U+6307: zhǐ # 指 +U+6308: qiè # 挈 +U+6309: àn # 按 +U+630E: kuà # 挎 +U+6311: tiāo # 挑 +U+6313: zhā # 挓 +U+6316: wā # 挖 +U+631A: zhì # 挚 +U+631B: luán # 挛 +U+631D: wō # 挝 -> zhuā +U+631E: tà # 挞 +U+631F: xié # 挟 +U+6320: náo # 挠 +U+6321: dǎng # 挡 +U+6323: zhēng # 挣 -> zhèng +U+6324: jǐ # 挤 +U+6325: huī # 挥 +U+6326: xián # 挦 +U+6328: āi # 挨 +U+632A: nuó # 挪 +U+632B: cuò # 挫 +U+632F: zhèn # 振 +U+6332: sā # 挲 -> suō +U+6339: yì # 挹 +U+633A: tǐng # 挺 +U+633D: wǎn # 挽 +U+6342: wǔ # 捂 +U+6343: jùn # 捃 +U+6345: tǒng # 捅 +U+6346: kǔn # 捆 +U+6349: zhuō # 捉 +U+634B: lǚ # 捋 -> luō +U+634C: bā # 捌 +U+634D: hàn # 捍 +U+634E: shāo # 捎 +U+634F: niē # 捏 +U+6350: juān # 捐 +U+6355: bǔ # 捕 +U+635E: lāo # 捞 +U+635F: sǔn # 损 +U+6361: jiǎn # 捡 +U+6362: huàn # 换 +U+6363: dǎo # 捣 +U+6367: pěng # 捧 +U+6369: liè # 捩 +U+636D: bǎi # 捭 +U+636E: jù # 据 +U+636F: dáo # 捯 +U+6376: chuí # 捶 +U+6377: jié # 捷 +U+637A: nà # 捺 +U+637B: niǎn # 捻 +U+637D: zuó # 捽 +U+6380: xiān # 掀 +U+6382: diān # 掂 +U+6387: duō # 掇 +U+6388: shòu # 授 +U+6389: diào # 掉 +U+638A: póu # 掊 -> pǒu +U+638C: zhǎng # 掌 +U+638E: jǐ # 掎 +U+638F: tāo # 掏 +U+6390: qiā # 掐 +U+6392: pái # 排 +U+6396: yē # 掖 -> yè +U+6398: jué # 掘 +U+639E: shàn # 掞 +U+63A0: lüè # 掠 +U+63A2: tàn # 探 +U+63A3: chè # 掣 +U+63A5: jiē # 接 +U+63A7: kòng # 控 +U+63A8: tuī # 推 +U+63A9: yǎn # 掩 +U+63AA: cuò # 措 +U+63AC: jū # 掬 +U+63AD: tiàn # 掭 +U+63AE: qián # 掮 +U+63B0: bāi # 掰 +U+63B3: lǔ # 掳 +U+63B4: guāi # 掴 -> guó +U+63B7: zhì # 掷 +U+63B8: dǎn # 掸 +U+63BA: càn # 掺 -> chān +U+63BC: guàn # 掼 +U+63BE: yuàn # 掾 +U+63C4: yú # 揄 +U+63C6: kuí # 揆 +U+63C9: róu # 揉 +U+63CD: zòu # 揍 +U+63CF: miáo # 描 +U+63D0: tí # 提 +U+63D2: chā # 插 +U+63D5: zhèn # 揕 +U+63D6: yī # 揖 +U+63E0: yà # 揠 +U+63E1: wò # 握 +U+63E3: chuāi # 揣 -> chuǎi +U+63E9: kāi # 揩 +U+63EA: jiū # 揪 +U+63ED: jiē # 揭 +U+63F3: xiē # 揳 +U+63F4: yuán # 援 +U+63F6: yé # 揶 +U+63F8: zhā # 揸 +U+63FD: lǎn # 揽 +U+63FF: qìn # 揿 +U+6400: chān # 搀 +U+6401: gē # 搁 +U+6402: lǒu # 搂 +U+6405: jiǎo # 搅 +U+640B: chuāi # 搋 +U+640C: zhǎn # 搌 +U+640F: bó # 搏 +U+6410: chù # 搐 +U+6412: bàng # 搒 +U+6413: cuō # 搓 +U+6414: sāo # 搔 +U+641B: jiān # 搛 +U+641C: sōu # 搜 +U+641E: gǎo # 搞 +U+6420: shuò # 搠 +U+6421: sǎng # 搡 +U+6426: nuò # 搦 +U+642A: táng # 搪 +U+642C: bān # 搬 +U+642D: dā # 搭 +U+6434: qiān # 搴 +U+643A: xié # 携 +U+643D: chá # 搽 +U+6441: èn # 摁 +U+6444: shè # 摄 +U+6445: shū # 摅 +U+6446: bǎi # 摆 +U+6447: yáo # 摇 +U+6448: bìn # 摈 +U+644A: tān # 摊 +U+644F: chōng # 摏 +U+6452: bǐng # 摒 -> bìng +U+6454: shuāi # 摔 +U+6458: zhāi # 摘 +U+645B: chī # 摛 +U+645E: luò # 摞 +U+6467: cuī # 摧 +U+6469: mó # 摩 +U+646D: zhí # 摭 +U+6474: chū # 摴 +U+6478: mō # 摸 +U+6479: mó # 摹 +U+647D: biāo # 摽 -> biào +U+6482: liào # 撂 +U+6484: yīng # 撄 +U+6485: juē # 撅 +U+6487: piē # 撇 +U+6491: chēng # 撑 +U+6492: sā # 撒 +U+6495: sī # 撕 +U+6496: hàn # 撖 +U+6499: zǔn # 撙 +U+649E: zhuàng # 撞 +U+64A4: chè # 撤 +U+64A9: liāo # 撩 +U+64AC: qiào # 撬 +U+64AD: bō # 播 +U+64AE: cuō # 撮 +U+64B0: zhuàn # 撰 +U+64B5: niǎn # 撵 +U+64B7: xié # 撷 +U+64B8: lū # 撸 +U+64BA: cuān # 撺 +U+64BC: hàn # 撼 +U+64C0: gǎn # 擀 +U+64C2: léi # 擂 +U+64C5: shàn # 擅 +U+64CD: cāo # 操 +U+64CE: qíng # 擎 +U+64D0: huàn # 擐 +U+64D2: qín # 擒 +U+64D8: bāi # 擘 -> bò +U+64DE: sǒu # 擞 -> sòu +U+64E2: zhuó # 擢 +U+64E4: xǐng # 擤 +U+64E6: cā # 擦 +U+64FF: tī # 擿 +U+6500: pān # 攀 +U+6509: huō # 攉 +U+6512: zǎn # 攒 +U+6518: rǎng # 攘 +U+6525: zuàn # 攥 +U+652B: jué # 攫 +U+652E: nǎng # 攮 +U+652F: zhī # 支 +U+6536: shōu # 收 +U+6538: yōu # 攸 +U+6539: gǎi # 改 +U+653B: gōng # 攻 +U+653D: bān # 攽 +U+653E: fàng # 放 +U+653F: zhèng # 政 +U+6545: gù # 故 +U+6548: xiào # 效 +U+6549: mǐ # 敉 +U+654C: dí # 敌 +U+654F: mǐn # 敏 +U+6551: jiù # 救 +U+6554: yǔ # 敔 +U+6555: chì # 敕 +U+6556: áo # 敖 +U+6559: jiào # 教 +U+655B: liǎn # 敛 +U+655D: bì # 敝 +U+655E: chǎng # 敞 +U+6562: gǎn # 敢 +U+6563: sàn # 散 +U+6566: dūn # 敦 +U+6569: xiào # 敩 +U+656B: jiǎo # 敫 +U+656C: jìng # 敬 +U+6570: shù # 数 +U+6572: qiāo # 敲 +U+6574: zhěng # 整 +U+6577: fū # 敷 +U+6587: wén # 文 +U+658B: zhāi # 斋 +U+658C: bīn # 斌 +U+6590: fěi # 斐 +U+6591: bān # 斑 +U+6593: lán # 斓 +U+6597: dòu # 斗 -> dǒu +U+6599: liào # 料 +U+659B: hú # 斛 +U+659C: xié # 斜 +U+659D: jiǎ # 斝 +U+659F: zhēn # 斟 +U+65A0: jiào # 斠 +U+65A1: wò # 斡 +U+65A4: jīn # 斤 +U+65A5: chì # 斥 +U+65A7: fǔ # 斧 +U+65A9: zhǎn # 斩 +U+65AB: zhuó # 斫 +U+65AD: duàn # 断 +U+65AF: sī # 斯 +U+65B0: xīn # 新 +U+65B6: chù # 斶 +U+65B9: fāng # 方 +U+65BC: yú # 於 +U+65BD: shī # 施 +U+65C1: páng # 旁 +U+65C3: zhān # 旃 +U+65C4: máo # 旄 +U+65C5: lǚ # 旅 +U+65C6: pèi # 旆 +U+65CB: xuán # 旋 +U+65CC: jīng # 旌 +U+65CE: nǐ # 旎 +U+65CF: zú # 族 +U+65D0: zhào # 旐 +U+65D2: liú # 旒 +U+65D6: yǐ # 旖 +U+65D7: qí # 旗 +U+65DE: suì # 旞 +U+65E0: wú # 无 +U+65E2: jì # 既 +U+65E5: rì # 日 +U+65E6: dàn # 旦 +U+65E7: jiù # 旧 +U+65E8: zhǐ # 旨 +U+65E9: zǎo # 早 +U+65EC: xún # 旬 +U+65ED: xù # 旭 +U+65EE: gā # 旮 +U+65EF: lá # 旯 +U+65F0: gàn # 旰 +U+65F1: hàn # 旱 +U+65F4: xū # 旴 -> xù +U+65F5: chǎn # 旵 +U+65F6: shí # 时 +U+65F7: kuàng # 旷 +U+65F8: yáng # 旸 +U+65FA: wàng # 旺 +U+65FB: mín # 旻 +U+65FF: wǔ # 旿 -> wù +U+6600: yún # 昀 +U+6602: áng # 昂 +U+6603: zè # 昃 +U+6604: bǎn # 昄 +U+6606: kūn # 昆 +U+6607: shēng # 昇 +U+6608: hù # 昈 +U+6609: fǎng # 昉 +U+660A: hào # 昊 +U+660C: chāng # 昌 +U+660E: míng # 明 +U+660F: hūn # 昏 +U+6612: hū # 昒 +U+6613: yì # 易 +U+6614: xī # 昔 +U+6615: xīn # 昕 +U+6619: tán # 昙 +U+661D: zǎn # 昝 +U+661F: xīng # 星 +U+6620: yìng # 映 +U+6621: xuàn # 昡 +U+6623: zhěn # 昣 +U+6624: líng # 昤 +U+6625: chūn # 春 +U+6627: mèi # 昧 +U+6628: zuó # 昨 +U+662A: biàn # 昪 +U+662B: xù # 昫 +U+662D: zhāo # 昭 +U+662F: shì # 是 +U+6631: yù # 昱 +U+6633: dié # 昳 +U+6634: mǎo # 昴 +U+6635: nì # 昵 +U+6636: chǎng # 昶 +U+663A: bǐng # 昺 +U+663C: zhòu # 昼 +U+663D: lóng # 昽 +U+663E: xiǎn # 显 +U+6641: cháo # 晁 +U+6643: huǎng # 晃 +U+6645: xuǎn # 晅 -> xuān +U+664A: zhì # 晊 +U+664B: jìn # 晋 +U+664C: shǎng # 晌 +U+664F: yàn # 晏 +U+6650: gāi # 晐 +U+6652: shài # 晒 +U+6653: xiǎo # 晓 +U+6654: yè # 晔 +U+6655: yūn # 晕 -> yùn +U+6656: huī # 晖 +U+6657: hán # 晗 +U+6659: jùn # 晙 +U+665A: wǎn # 晚 +U+665E: xī # 晞 +U+665F: chéng # 晟 -> shèng +U+6661: bū # 晡 +U+6662: zhé # 晢 +U+6664: wù # 晤 +U+6666: huì # 晦 +U+6668: chén # 晨 +U+666A: tiǎn # 晪 +U+666B: zhuó # 晫 +U+666E: pǔ # 普 +U+666F: jǐng # 景 +U+6670: xī # 晰 +U+6671: shǎn # 晱 +U+6674: qíng # 晴 +U+6676: jīng # 晶 +U+6677: guǐ # 晷 +U+667A: zhì # 智 +U+667E: liàng # 晾 +U+6682: zàn # 暂 +U+6684: xuān # 暄 +U+6685: gèng # 暅 -> xuǎn +U+6687: xiá # 暇 +U+668C: kuí # 暌 +U+6691: shǔ # 暑 +U+6695: jiǎn # 暕 +U+6696: nuǎn # 暖 +U+6697: àn # 暗 +U+669D: míng # 暝 +U+66A7: ài # 暧 +U+66A8: jì # 暨 +U+66AE: mù # 暮 +U+66B2: zhāng # 暲 +U+66B4: bào # 暴 +U+66B5: hàn # 暵 +U+66B6: xuán # 暶 +U+66B9: xiān # 暹 +U+66BE: tūn # 暾 +U+66BF: xǐ # 暿 -> xī +U+66C8: tóng # 曈 +U+66CC: zhào # 曌 +U+66D9: shǔ # 曙 +U+66DB: xūn # 曛 +U+66DC: yào # 曜 +U+66DD: pù # 曝 +U+66E6: xī # 曦 +U+66E9: nǎng # 曩 +U+66F0: yuē # 曰 +U+66F2: qū # 曲 +U+66F3: yè # 曳 +U+66F4: gèng # 更 -> gēng +U+66F7: hé # 曷 +U+66F9: cáo # 曹 +U+66FC: màn # 曼 +U+66FE: céng # 曾 -> zēng +U+66FF: tì # 替 +U+6700: zuì # 最 +U+6708: yuè # 月 +U+6709: yǒu # 有 +U+670B: péng # 朋 +U+670D: fú # 服 +U+670F: fěi # 朏 +U+6710: qú # 朐 +U+6713: tiǎo # 朓 +U+6714: shuò # 朔 +U+6715: zhèn # 朕 +U+6717: lǎng # 朗 +U+671B: wàng # 望 +U+671D: cháo # 朝 -> zhāo +U+671F: qī # 期 +U+6726: méng # 朦 +U+6728: mù # 木 +U+672A: wèi # 未 +U+672B: mò # 末 +U+672C: běn # 本 +U+672D: zhá # 札 +U+672F: shù # 术 +U+6731: zhū # 朱 +U+6733: bā # 朳 +U+6734: pǔ # 朴 +U+6735: duǒ # 朵 +U+6738: lì # 朸 +U+673A: jī # 机 +U+673D: xiǔ # 朽 +U+6740: shā # 杀 +U+6742: zá # 杂 +U+6743: quán # 权 +U+6744: qiān # 杄 +U+6746: gān # 杆 +U+6748: chā # 杈 +U+6749: shān # 杉 +U+674C: wù # 杌 +U+674E: lǐ # 李 +U+674F: xìng # 杏 +U+6750: cái # 材 +U+6751: cūn # 村 +U+6753: biāo # 杓 -> sháo +U+6755: dì # 杕 +U+6756: zhàng # 杖 +U+6759: yì # 杙 +U+675C: dù # 杜 +U+675E: qǐ # 杞 +U+675F: shù # 束 +U+6760: gāng # 杠 -> gàng +U+6761: tiáo # 条 +U+6765: lái # 来 +U+6767: máng # 杧 +U+6768: yáng # 杨 +U+6769: mà # 杩 +U+676A: miǎo # 杪 +U+676D: háng # 杭 +U+676F: bēi # 杯 +U+6770: jié # 杰 +U+6772: gǎo # 杲 +U+6773: yǎo # 杳 +U+6775: chǔ # 杵 +U+6777: pá # 杷 +U+677B: chǒu # 杻 -> niǔ +U+677C: zhù # 杼 +U+677E: sōng # 松 +U+677F: bǎn # 板 +U+6781: jí # 极 +U+6784: gòu # 构 +U+6785: jī # 枅 +U+6787: pí # 枇 +U+6789: wǎng # 枉 +U+678B: fāng # 枋 +U+678D: yì # 枍 +U+6790: xī # 析 +U+6795: zhěn # 枕 +U+6797: lín # 林 +U+6798: ruì # 枘 +U+679A: méi # 枚 +U+679C: guǒ # 果 +U+679D: zhī # 枝 +U+679E: cōng # 枞 +U+67A2: shū # 枢 +U+67A3: zǎo # 枣 +U+67A5: lì # 枥 +U+67A7: jiǎn # 枧 +U+67A8: chéng # 枨 +U+67AA: qiāng # 枪 +U+67AB: fēng # 枫 +U+67AD: xiāo # 枭 +U+67AF: kū # 枯 +U+67B0: píng # 枰 +U+67B2: xǐ # 枲 +U+67B3: zhǐ # 枳 +U+67B5: xiāo # 枵 +U+67B6: jià # 架 +U+67B7: jiā # 枷 +U+67B8: gǒu # 枸 -> jǔ +U+67B9: bāo # 枹 +U+67C1: duò # 柁 -> tuó +U+67C3: líng # 柃 +U+67C4: bǐng # 柄 +U+67C8: bàn # 柈 -> pán +U+67CA: zhōng # 柊 +U+67CF: bǎi # 柏 +U+67D0: mǒu # 某 +U+67D1: gān # 柑 +U+67D2: qī # 柒 +U+67D3: rǎn # 染 +U+67D4: róu # 柔 +U+67D6: sháo # 柖 +U+67D8: zhè # 柘 +U+67D9: xiá # 柙 +U+67DA: yòu # 柚 +U+67DC: guì # 柜 +U+67DD: tuò # 柝 +U+67DE: zhà # 柞 -> zuò +U+67E0: níng # 柠 +U+67E2: dǐ # 柢 +U+67E5: chá # 查 +U+67E9: jiù # 柩 +U+67EC: jiǎn # 柬 +U+67EF: kē # 柯 +U+67F0: nài # 柰 +U+67F1: zhù # 柱 +U+67F3: liǔ # 柳 +U+67F4: chái # 柴 +U+67F7: chù # 柷 -> zhù +U+67FD: chēng # 柽 +U+67FF: shì # 柿 +U+6800: zhī # 栀 +U+6805: zhà # 栅 +U+6807: biāo # 标 +U+6808: zhàn # 栈 +U+6809: zhì # 栉 +U+680A: lóng # 栊 +U+680B: dòng # 栋 +U+680C: lú # 栌 +U+680E: lì # 栎 +U+680F: lán # 栏 +U+6810: yǒng # 栐 +U+6811: shù # 树 +U+6812: xún # 栒 +U+6813: shuān # 栓 +U+6816: qī # 栖 +U+6817: lì # 栗 +U+681D: guā # 栝 +U+681F: bēn # 栟 +U+6821: xiào # 校 +U+6829: xǔ # 栩 +U+682A: zhū # 株 +U+6832: kǎo # 栲 +U+6833: lǎo # 栳 +U+6834: zhān # 栴 +U+6837: yàng # 样 +U+6838: hé # 核 +U+6839: gēn # 根 +U+683B: shì # 栻 +U+683C: gé # 格 +U+683D: zāi # 栽 +U+683E: luán # 栾 +U+6840: jié # 桀 +U+6841: héng # 桁 +U+6842: guì # 桂 +U+6843: táo # 桃 +U+6844: guāng # 桄 +U+6845: wéi # 桅 +U+6846: kuāng # 框 -> kuàng +U+6848: àn # 案 +U+6849: ān # 桉 +U+684A: juàn # 桊 +U+684C: zhuō # 桌 +U+684E: zhì # 桎 +U+6850: tóng # 桐 +U+6851: sāng # 桑 +U+6853: huán # 桓 +U+6854: jú # 桔 -> jié +U+6855: jiù # 桕 +U+6860: yā # 桠 +U+6861: ráo # 桡 +U+6862: zhēn # 桢 +U+6863: dàng # 档 +U+6864: qī # 桤 +U+6865: qiáo # 桥 +U+6866: huà # 桦 +U+6867: guì # 桧 +U+6868: jiǎng # 桨 +U+6869: zhuāng # 桩 +U+686B: suō # 桫 +U+686F: tīng # 桯 +U+6872: po # 桲 -> bó +U+6874: fú # 桴 +U+6876: tǒng # 桶 +U+6877: jué # 桷 +U+6879: láng # 桹 +U+6881: liáng # 梁 +U+6883: tǐng # 梃 +U+6885: méi # 梅 +U+6886: bāng # 梆 +U+688C: tú # 梌 +U+688F: gù # 梏 +U+6893: zǐ # 梓 +U+6897: gěng # 梗 +U+68A0: lǚ # 梠 +U+68A2: shāo # 梢 +U+68A3: cén # 梣 -> chén +U+68A6: mèng # 梦 +U+68A7: wú # 梧 +U+68A8: lí # 梨 +U+68AD: suō # 梭 +U+68AF: tī # 梯 +U+68B0: xiè # 械 +U+68B3: shū # 梳 +U+68B4: chān # 梴 +U+68B5: fàn # 梵 +U+68BC: táo # 梼 -> chóu +U+68BD: zhì # 梽 +U+68BE: lái # 梾 +U+68BF: lián # 梿 +U+68C0: jiǎn # 检 +U+68C1: zhuō # 棁 +U+68C2: líng # 棂 +U+68C9: mián # 棉 +U+68CB: qí # 棋 +U+68CD: gùn # 棍 +U+68D0: fěi # 棐 +U+68D2: bàng # 棒 +U+68D3: bàng # 棓 +U+68D5: zōng # 棕 +U+68D8: jí # 棘 +U+68DA: péng # 棚 +U+68E0: táng # 棠 +U+68E3: dì # 棣 +U+68E4: cuò # 棤 -> què +U+68E8: qǐ # 棨 +U+68EA: yǎn # 棪 +U+68EB: yù # 棫 +U+68EC: quān # 棬 +U+68EE: sēn # 森 +U+68F0: chuí # 棰 +U+68F1: léng # 棱 +U+68F5: kē # 棵 +U+68F9: zhào # 棹 +U+68FA: guān # 棺 +U+68FB: fēn # 棻 +U+68FC: fén # 棼 +U+68FD: shēn # 棽 -> chēn +U+6900: wǎn # 椀 +U+6901: guǒ # 椁 +U+6905: yǐ # 椅 +U+6906: chóu # 椆 +U+690B: liáng # 椋 +U+690D: zhí # 植 +U+690E: chuí # 椎 -> zhuī +U+6910: jū # 椐 +U+6911: bēi # 椑 +U+6912: jiāo # 椒 +U+6913: zhuó # 椓 +U+691F: dú # 椟 +U+6920: qiàn # 椠 +U+6924: luó # 椤 +U+692A: pèng # 椪 +U+692D: tuǒ # 椭 +U+6930: yē # 椰 +U+6934: duàn # 椴 +U+6938: yí # 椸 +U+6939: shèn # 椹 -> zhēn +U+693D: chuán # 椽 +U+693F: chūn # 椿 +U+6942: zhā # 楂 +U+6952: sī # 楒 +U+6954: xiē # 楔 +U+6957: jiàn # 楗 +U+6959: mào # 楙 -> máo +U+695A: chǔ # 楚 +U+695D: liàn # 楝 +U+695E: léng # 楞 +U+6960: nán # 楠 +U+6963: méi # 楣 +U+6966: xuàn # 楦 +U+6969: pián # 楩 +U+696A: yè # 楪 -> dié +U+696B: jí # 楫 +U+696E: chǔ # 楮 +U+696F: dùn # 楯 -> shǔn +U+6977: kǎi # 楷 +U+6978: qiū # 楸 +U+6979: yíng # 楹 +U+697C: lóu # 楼 +U+6982: gài # 概 +U+6983: tán # 榃 +U+6984: lǎn # 榄 +U+6985: wēn # 榅 +U+6986: yú # 榆 +U+6987: chèn # 榇 +U+6988: lǘ # 榈 +U+6989: jǔ # 榉 +U+698D: xiè # 榍 +U+6991: fú # 榑 +U+6994: láng # 榔 +U+6995: róng # 榕 +U+6996: gǔ # 榖 +U+699B: zhēn # 榛 +U+699C: bǎng # 榜 +U+69A7: fěi # 榧 +U+69A8: zhà # 榨 +U+69AB: sǔn # 榫 +U+69AD: xiè # 榭 +U+69B0: zhī # 榰 +U+69B1: cuī # 榱 +U+69B4: liú # 榴 +U+69B7: què # 榷 +U+69BB: tà # 榻 +U+69C1: gǎo # 槁 +U+69C3: pán # 槃 +U+69CA: shuò # 槊 +U+69CC: chuí # 槌 +U+69CE: chá # 槎 +U+69D0: huái # 槐 +U+69D4: gāo # 槔 +U+69DA: jiǎ # 槚 +U+69DB: kǎn # 槛 -> jiàn +U+69DC: zuì # 槜 +U+69DF: bīn # 槟 +U+69E0: zhū # 槠 +U+69ED: qī # 槭 -> qì +U+69F1: yǒu # 槱 +U+69F2: hú # 槲 +U+69FD: cáo # 槽 +U+69FF: jǐn # 槿 +U+6A0A: fán # 樊 +U+6A17: chū # 樗 +U+6A18: táng # 樘 +U+6A1F: zhāng # 樟 +U+6A21: mó # 模 +U+6A28: xī # 樨 +U+6A2A: héng # 横 +U+6A2F: qiáng # 樯 +U+6A31: yīng # 樱 +U+6A35: qiáo # 樵 +U+6A3D: zūn # 樽 +U+6A3E: yuè # 樾 +U+6A44: gǎn # 橄 +U+6A47: qiāo # 橇 +U+6A50: tuó # 橐 +U+6A51: lǎo # 橑 -> liáo +U+6A58: jú # 橘 +U+6A59: chéng # 橙 +U+6A5B: jué # 橛 +U+6A5E: huì # 橞 +U+6A61: xiàng # 橡 +U+6A65: zhū # 橥 +U+6A66: tóng # 橦 +U+6A71: chú # 橱 +U+6A79: lǔ # 橹 +U+6A7C: yuán # 橼 +U+6A80: tán # 檀 +U+6A84: xí # 檄 +U+6A8E: qín # 檎 +U+6A90: yán # 檐 +U+6A91: léi # 檑 +U+6A97: bò # 檗 +U+6A9E: jiě # 檞 +U+6AA0: qíng # 檠 +U+6AA9: lǐn # 檩 +U+6AAB: chá # 檫 +U+6AAC: méng # 檬 +U+6AC6: kuí # 櫆 +U+6B02: bó # 欂 +U+6B20: qiàn # 欠 +U+6B21: cì # 次 +U+6B22: huān # 欢 +U+6B23: xīn # 欣 +U+6B24: yú # 欤 +U+6B27: ōu # 欧 +U+6B32: yù # 欲 +U+6B38: āi # 欸 -> èi +U+6B39: yī # 欹 -> qī +U+6B3A: qī # 欺 +U+6B3B: chuā # 欻 -> xū +U+6B3E: kuǎn # 款 +U+6B43: shà # 歃 +U+6B45: yīn # 歅 -> yān +U+6B46: xīn # 歆 +U+6B47: xiē # 歇 +U+6B49: qiàn # 歉 +U+6B4C: gē # 歌 +U+6B59: shè # 歙 -> xī +U+6B62: zhǐ # 止 +U+6B63: zhèng # 正 +U+6B64: cǐ # 此 +U+6B65: bù # 步 +U+6B66: wǔ # 武 +U+6B67: qí # 歧 +U+6B6A: wāi # 歪 +U+6B79: dǎi # 歹 +U+6B7B: sǐ # 死 +U+6B7C: jiān # 歼 +U+6B81: mò # 殁 +U+6B82: cú # 殂 +U+6B83: yāng # 殃 +U+6B84: tiǎn # 殄 +U+6B86: dài # 殆 +U+6B87: shāng # 殇 +U+6B89: xùn # 殉 +U+6B8A: shū # 殊 +U+6B8B: cán # 残 +U+6B8D: piǎo # 殍 +U+6B92: yǔn # 殒 +U+6B93: liàn # 殓 +U+6B96: zhí # 殖 +U+6B9A: dān # 殚 +U+6B9B: jí # 殛 +U+6BA1: bìn # 殡 +U+6BA3: jìn # 殣 +U+6BAA: yì # 殪 +U+6BB3: shū # 殳 +U+6BB4: ōu # 殴 +U+6BB5: duàn # 段 +U+6BB7: yīn # 殷 +U+6BBF: diàn # 殿 +U+6BC1: huǐ # 毁 +U+6BC2: gǔ # 毂 +U+6BC5: yì # 毅 +U+6BCB: wú # 毋 +U+6BCC: guàn # 毌 +U+6BCD: mǔ # 母 +U+6BCF: měi # 每 +U+6BD0: ǎi # 毐 +U+6BD2: dú # 毒 +U+6BD3: yù # 毓 +U+6BD4: bǐ # 比 +U+6BD5: bì # 毕 +U+6BD6: bì # 毖 +U+6BD7: pí # 毗 +U+6BD9: bì # 毙 +U+6BDB: máo # 毛 +U+6BE1: zhān # 毡 +U+6BEA: mú # 毪 +U+6BEB: háo # 毫 +U+6BEF: tǎn # 毯 +U+6BF3: cuì # 毳 +U+6BF5: sān # 毵 +U+6BF9: shū # 毹 +U+6BFD: jiàn # 毽 +U+6C05: chǎng # 氅 +U+6C06: pǔ # 氆 +U+6C07: lu # 氇 -> lǔ +U+6C0D: qú # 氍 +U+6C0F: shì # 氏 +U+6C10: dī # 氐 +U+6C11: mín # 民 +U+6C13: máng # 氓 -> méng +U+6C14: qì # 气 +U+6C15: piē # 氕 +U+6C16: nǎi # 氖 +U+6C18: dāo # 氘 +U+6C19: xiān # 氙 +U+6C1A: chuān # 氚 +U+6C1B: fēn # 氛 +U+6C1F: fú # 氟 +U+6C21: dōng # 氡 +U+6C22: qīng # 氢 +U+6C24: yīn # 氤 +U+6C26: hài # 氦 +U+6C27: yǎng # 氧 +U+6C28: ān # 氨 +U+6C29: yà # 氩 +U+6C2A: kè # 氪 +U+6C2E: dàn # 氮 +U+6C2F: lǜ # 氯 +U+6C30: qíng # 氰 +U+6C32: yūn # 氲 +U+6C34: shuǐ # 水 +U+6C38: yǒng # 永 +U+6C3E: fàn # 氾 +U+6C3F: guǐ # 氿 +U+6C40: tīng # 汀 +U+6C41: zhī # 汁 +U+6C42: qiú # 求 +U+6C46: cuān # 汆 +U+6C47: huì # 汇 +U+6C48: diāo # 汈 +U+6C49: hàn # 汉 +U+6C4A: chà # 汊 +U+6C4B: zhuó # 汋 +U+6C50: xī # 汐 +U+6C54: qì # 汔 +U+6C55: shàn # 汕 +U+6C57: hàn # 汗 +U+6C5B: xùn # 汛 +U+6C5C: sì # 汜 +U+6C5D: rǔ # 汝 +U+6C5E: gǒng # 汞 +U+6C5F: jiāng # 江 +U+6C60: chí # 池 +U+6C61: wū # 污 +U+6C64: tāng # 汤 +U+6C67: qiān # 汧 +U+6C68: mì # 汨 +U+6C69: gǔ # 汩 +U+6C6A: wāng # 汪 +U+6C6B: jǐng # 汫 +U+6C6D: ruì # 汭 +U+6C70: tài # 汰 +U+6C72: jí # 汲 +U+6C74: biàn # 汴 +U+6C76: wèn # 汶 +U+6C79: xiōng # 汹 +U+6C7D: qì # 汽 +U+6C7E: fén # 汾 +U+6C81: qìn # 沁 +U+6C82: yí # 沂 +U+6C83: wò # 沃 +U+6C84: yún # 沄 +U+6C85: yuán # 沅 +U+6C86: hàng # 沆 +U+6C87: yǎn # 沇 +U+6C88: shěn # 沈 +U+6C89: chén # 沉 +U+6C8C: dùn # 沌 +U+6C8F: qī # 沏 +U+6C90: mù # 沐 +U+6C93: dá # 沓 -> tà +U+6C94: miǎn # 沔 +U+6C98: bǐ # 沘 +U+6C99: shā # 沙 +U+6C9A: zhǐ # 沚 +U+6C9B: pèi # 沛 +U+6C9F: gōu # 沟 +U+6CA1: méi # 没 +U+6CA3: fēng # 沣 +U+6CA4: ōu # 沤 -> òu +U+6CA5: lì # 沥 +U+6CA6: lún # 沦 +U+6CA7: cāng # 沧 +U+6CA8: fēng # 沨 +U+6CA9: wéi # 沩 +U+6CAA: hù # 沪 +U+6CAB: mò # 沫 +U+6CAD: shù # 沭 +U+6CAE: jǔ # 沮 +U+6CB1: tuó # 沱 +U+6CB3: hé # 河 +U+6CB8: fèi # 沸 +U+6CB9: yóu # 油 +U+6CBA: tián # 沺 +U+6CBB: zhì # 治 +U+6CBC: zhǎo # 沼 +U+6CBD: gū # 沽 +U+6CBE: zhān # 沾 +U+6CBF: yán # 沿 +U+6CC2: jiǒng # 泂 +U+6CC3: jū # 泃 +U+6CC4: xiè # 泄 +U+6CC5: qiú # 泅 +U+6CC7: jiā # 泇 +U+6CC9: quán # 泉 +U+6CCA: pō # 泊 -> bó +U+6CCC: mì # 泌 +U+6CD0: lè # 泐 +U+6CD3: hóng # 泓 +U+6CD4: gān # 泔 +U+6CD5: fǎ # 法 +U+6CD6: mǎo # 泖 +U+6CD7: sì # 泗 +U+6CD9: píng # 泙 -> pēng +U+6CDA: cǐ # 泚 +U+6CDB: fàn # 泛 +U+6CDC: zhī # 泜 +U+6CDE: nìng # 泞 +U+6CE0: líng # 泠 +U+6CE1: pào # 泡 +U+6CE2: bō # 波 +U+6CE3: qì # 泣 +U+6CE5: ní # 泥 +U+6CE8: zhù # 注 +U+6CEA: lèi # 泪 +U+6CEB: xuàn # 泫 +U+6CEE: pàn # 泮 +U+6CEF: mǐn # 泯 +U+6CF0: tài # 泰 +U+6CF1: yāng # 泱 +U+6CF3: yǒng # 泳 +U+6CF5: bèng # 泵 +U+6CF7: lóng # 泷 +U+6CF8: lú # 泸 +U+6CFA: luò # 泺 +U+6CFB: xiè # 泻 +U+6CFC: pō # 泼 +U+6CFD: zé # 泽 +U+6CFE: jīng # 泾 +U+6D01: jié # 洁 +U+6D04: huí # 洄 +U+6D07: yīn # 洇 +U+6D08: wéi # 洈 +U+6D0B: yáng # 洋 +U+6D0C: liè # 洌 +U+6D0E: jì # 洎 +U+6D11: fú # 洑 +U+6D12: sǎ # 洒 +U+6D13: sè # 洓 +U+6D17: xǐ # 洗 +U+6D18: kǎo # 洘 +U+6D19: zhū # 洙 +U+6D1A: jiàng # 洚 +U+6D1B: luò # 洛 +U+6D1E: dòng # 洞 +U+6D22: yī # 洢 +U+6D23: mǐ # 洣 +U+6D25: jīn # 津 +U+6D27: wěi # 洧 +U+6D28: xiáo # 洨 +U+6D2A: hóng # 洪 +U+6D2B: xù # 洫 +U+6D2D: kuāng # 洭 +U+6D2E: táo # 洮 +U+6D31: ěr # 洱 +U+6D32: zhōu # 洲 +U+6D33: rù # 洳 +U+6D34: píng # 洴 +U+6D35: xún # 洵 +U+6D38: guāng # 洸 +U+6D39: huán # 洹 +U+6D3A: míng # 洺 +U+6D3B: huó # 活 +U+6D3C: wā # 洼 +U+6D3D: qià # 洽 +U+6D3E: pài # 派 +U+6D3F: wū # 洿 +U+6D41: liú # 流 +U+6D43: jiā # 浃 +U+6D45: qiǎn # 浅 +U+6D46: jiāng # 浆 +U+6D47: jiāo # 浇 +U+6D48: zhēn # 浈 +U+6D49: shī # 浉 +U+6D4A: zhuó # 浊 +U+6D4B: cè # 测 +U+6D4D: huì # 浍 -> kuài +U+6D4E: jì # 济 +U+6D4F: liú # 浏 +U+6D50: chǎn # 浐 +U+6D51: hún # 浑 +U+6D52: hǔ # 浒 +U+6D53: nóng # 浓 +U+6D54: xún # 浔 +U+6D55: jìn # 浕 +U+6D59: zhè # 浙 +U+6D5A: jùn # 浚 +U+6D5B: hán # 浛 +U+6D5C: bāng # 浜 +U+6D5E: zhuó # 浞 +U+6D5F: yóu # 浟 -> yōu +U+6D60: xī # 浠 +U+6D61: bó # 浡 +U+6D63: huàn # 浣 +U+6D65: yì # 浥 +U+6D66: pǔ # 浦 +U+6D69: hào # 浩 +U+6D6A: làng # 浪 +U+6D6C: lǐ # 浬 +U+6D6D: gēng # 浭 +U+6D6E: fú # 浮 +U+6D6F: wú # 浯 +U+6D70: liàn # 浰 -> lì +U+6D72: féng # 浲 +U+6D74: yù # 浴 +U+6D77: hǎi # 海 +U+6D78: jìn # 浸 +U+6D7C: měi # 浼 +U+6D82: tú # 涂 +U+6D84: pīng # 涄 +U+6D85: niè # 涅 +U+6D88: xiāo # 消 +U+6D89: shè # 涉 +U+6D8C: yǒng # 涌 +U+6D8D: xiào # 涍 +U+6D8E: xián # 涎 +U+6D90: é # 涐 +U+6D91: sù # 涑 +U+6D93: juān # 涓 +U+6D94: cén # 涔 +U+6D95: tì # 涕 +U+6D98: sì # 涘 +U+6D9B: tāo # 涛 +U+6D9D: lào # 涝 +U+6D9E: lái # 涞 +U+6D9F: lián # 涟 +U+6DA0: wéi # 涠 +U+6DA1: wō # 涡 +U+6DA2: yún # 涢 +U+6DA3: huàn # 涣 +U+6DA4: dí # 涤 +U+6DA6: rùn # 润 +U+6DA7: jiàn # 涧 +U+6DA8: zhǎng # 涨 +U+6DA9: sè # 涩 +U+6DAA: fú # 涪 +U+6DAB: guàn # 涫 -> guān +U+6DAE: shuàn # 涮 +U+6DAF: yá # 涯 +U+6DB2: yè # 液 +U+6DB4: wò # 涴 -> wǎn +U+6DB5: hán # 涵 +U+6DB8: hé # 涸 +U+6DBF: zhuō # 涿 +U+6DC0: diàn # 淀 +U+6DC4: zī # 淄 +U+6DC5: xī # 淅 +U+6DC6: xiáo # 淆 +U+6DC7: qí # 淇 +U+6DCB: lín # 淋 +U+6DCC: tǎng # 淌 +U+6DCF: hào # 淏 +U+6DD1: shū # 淑 +U+6DD6: nào # 淖 +U+6DD8: táo # 淘 +U+6DD9: cóng # 淙 +U+6DDC: píng # 淜 +U+6DDD: féi # 淝 +U+6DDE: sōng # 淞 +U+6DDF: tiǎn # 淟 +U+6DE0: pì # 淠 +U+6DE1: dàn # 淡 +U+6DE4: yū # 淤 +U+6DE6: gàn # 淦 +U+6DEB: yín # 淫 +U+6DEC: cuì # 淬 +U+6DEE: huái # 淮 +U+6DEF: yù # 淯 +U+6DF1: shēn # 深 +U+6DF3: chún # 淳 +U+6DF4: hū # 淴 +U+6DF7: hùn # 混 +U+6DF9: yān # 淹 +U+6DFB: tiān # 添 +U+6DFC: miǎo # 淼 +U+6E05: qīng # 清 +U+6E0A: yuān # 渊 +U+6E0C: lù # 渌 +U+6E0D: zì # 渍 +U+6E0E: dú # 渎 +U+6E10: jiàn # 渐 +U+6E11: miǎn # 渑 +U+6E14: yú # 渔 +U+6E17: shèn # 渗 +U+6E1A: zhǔ # 渚 +U+6E1D: yú # 渝 +U+6E1F: tíng # 渟 +U+6E20: qú # 渠 +U+6E21: dù # 渡 +U+6E23: zhā # 渣 +U+6E24: bó # 渤 +U+6E25: wò # 渥 +U+6E29: wēn # 温 +U+6E2B: xiè # 渫 +U+6E2D: wèi # 渭 +U+6E2F: gǎng # 港 +U+6E30: yǎn # 渰 -> yān +U+6E32: xuàn # 渲 +U+6E34: kě # 渴 +U+6E38: yóu # 游 +U+6E3A: miǎo # 渺 +U+6E3C: měi # 渼 +U+6E43: pài # 湃 +U+6E44: méi # 湄 +U+6E49: tián # 湉 +U+6E4D: tuān # 湍 +U+6E4E: miǎn # 湎 +U+6E51: xū # 湑 -> xǔ +U+6E53: pén # 湓 +U+6E54: jiān # 湔 +U+6E56: hú # 湖 +U+6E58: xiāng # 湘 +U+6E5B: zhàn # 湛 +U+6E5C: shí # 湜 +U+6E5D: jiē # 湝 +U+6E5F: huáng # 湟 +U+6E63: mǐn # 湣 +U+6E6B: jiǎo # 湫 -> qiū +U+6E6E: yān # 湮 +U+6E72: yuán # 湲 +U+6E74: bàn # 湴 +U+6E7E: wān # 湾 +U+6E7F: shī # 湿 +U+6E81: yíng # 溁 +U+6E83: kuì # 溃 +U+6E85: jiàn # 溅 +U+6E86: xù # 溆 +U+6E87: lóu # 溇 +U+6E89: gài # 溉 +U+6E8D: jìn # 溍 +U+6E8F: táng # 溏 +U+6E90: yuán # 源 +U+6E98: kè # 溘 +U+6E9A: tǎ # 溚 -> dá +U+6E9C: liū # 溜 +U+6E9E: sāo # 溞 +U+6E9F: míng # 溟 +U+6EA0: zhà # 溠 +U+6EA2: yì # 溢 +U+6EA5: pǔ # 溥 +U+6EA6: wēi # 溦 +U+6EA7: lì # 溧 +U+6EAA: xī # 溪 +U+6EAF: sù # 溯 +U+6EB1: qín # 溱 -> zhēn +U+6EB2: sōu # 溲 +U+6EB4: xiù # 溴 +U+6EB5: yīn # 溵 +U+6EB6: róng # 溶 +U+6EB7: hùn # 溷 +U+6EB9: suò # 溹 +U+6EBA: nì # 溺 +U+6EBB: tā # 溻 +U+6EBD: rù # 溽 +U+6EC1: chú # 滁 +U+6EC2: pāng # 滂 +U+6EC3: wēng # 滃 -> wěng +U+6EC6: gé # 滆 +U+6EC7: diān # 滇 +U+6EC9: huàng # 滉 +U+6ECB: zī # 滋 +U+6ECD: zhì # 滍 +U+6ECF: fǔ # 滏 +U+6ED1: huá # 滑 +U+6ED3: zǐ # 滓 +U+6ED4: tāo # 滔 +U+6ED5: téng # 滕 +U+6ED7: bì # 滗 +U+6ED8: jiào # 滘 +U+6EDA: gǔn # 滚 +U+6EDE: zhì # 滞 +U+6EDF: yàn # 滟 +U+6EE0: shè # 滠 +U+6EE1: mǎn # 满 +U+6EE2: yíng # 滢 +U+6EE4: lǜ # 滤 +U+6EE5: làn # 滥 +U+6EE6: luán # 滦 +U+6EE7: yáo # 滧 +U+6EE8: bīn # 滨 +U+6EE9: tān # 滩 +U+6EEA: yù # 滪 +U+6EEB: xiǔ # 滫 +U+6EF4: dī # 滴 +U+6EF9: hū # 滹 +U+6F02: piāo # 漂 +U+6F06: qī # 漆 +U+6F08: jì # 漈 +U+6F09: lù # 漉 +U+6F0B: lóng # 漋 +U+6F0F: lòu # 漏 +U+6F13: lí # 漓 +U+6F14: yǎn # 演 +U+6F15: cáo # 漕 +U+6F16: jiào # 漖 +U+6F20: mò # 漠 +U+6F24: lǎn # 漤 +U+6F26: chí # 漦 +U+6F29: xuán # 漩 +U+6F2A: yī # 漪 +U+6F2B: màn # 漫 +U+6F2D: mǎng # 漭 +U+6F2F: luò # 漯 +U+6F31: shù # 漱 +U+6F33: zhāng # 漳 +U+6F34: zhuàng # 漴 -> chóng +U+6F36: huàn # 漶 +U+6F37: huǒ # 漷 +U+6F39: yān # 漹 +U+6F3B: liáo # 漻 +U+6F3C: cuǐ # 漼 +U+6F3E: yàng # 漾 +U+6F46: yíng # 潆 +U+6F47: xiāo # 潇 +U+6F4B: liàn # 潋 +U+6F4D: wéi # 潍 +U+6F4F: yù # 潏 +U+6F56: pá # 潖 +U+6F58: pān # 潘 +U+6F5C: qián # 潜 +U+6F5E: lù # 潞 +U+6F5F: xì # 潟 +U+6F62: huáng # 潢 +U+6F66: lǎo # 潦 +U+6F69: yì # 潩 +U+6F6D: tán # 潭 +U+6F6E: cháo # 潮 +U+6F72: shào # 潲 +U+6F74: zhū # 潴 +U+6F75: sǎ # 潵 -> sàn +U+6F78: shān # 潸 +U+6F7A: chán # 潺 +U+6F7C: tóng # 潼 +U+6F7D: pū # 潽 +U+6F7E: lín # 潾 +U+6F82: chéng # 澂 +U+6F84: chéng # 澄 +U+6F88: chè # 澈 +U+6F89: gǎn # 澉 +U+6F8C: sī # 澌 +U+6F8D: shù # 澍 +U+6F8E: pēng # 澎 -> péng +U+6F9B: lǔ # 澛 +U+6F9C: lán # 澜 +U+6FA1: zǎo # 澡 +U+6FA5: xiè # 澥 +U+6FA7: lǐ # 澧 +U+6FAA: líng # 澪 +U+6FAD: yōng # 澭 +U+6FB3: ào # 澳 +U+6FB4: huán # 澴 +U+6FB6: chán # 澶 +U+6FB9: dàn # 澹 +U+6FBC: pì # 澼 +U+6FBD: jù # 澽 +U+6FC0: jī # 激 +U+6FC2: lián # 濂 +U+6FC9: suī # 濉 +U+6FCB: chǔ # 濋 +U+6FD1: lài # 濑 +U+6FD2: bīn # 濒 +U+6FDE: bì # 濞 +U+6FE0: háo # 濠 +U+6FE1: rú # 濡 +U+6FE9: huò # 濩 +U+6FEE: pú # 濮 +U+6FEF: zhuó # 濯 +U+700C: biāo # 瀌 +U+700D: chán # 瀍 +U+7011: pù # 瀑 +U+7014: gǔ # 瀔 +U+701A: hàn # 瀚 +U+701B: yíng # 瀛 +U+7023: xiè # 瀣 +U+7031: jì # 瀱 +U+7035: fèn # 瀵 +U+7039: yuè # 瀹 +U+703C: ráng # 瀼 +U+7048: qú # 灈 +U+704C: guàn # 灌 +U+704F: hào # 灏 +U+705E: bà # 灞 +U+706B: huǒ # 火 +U+706D: miè # 灭 +U+706F: dēng # 灯 +U+7070: huī # 灰 +U+7075: líng # 灵 +U+7076: zào # 灶 +U+7078: jiǔ # 灸 +U+707C: zhuó # 灼 +U+707E: zāi # 灾 +U+707F: càn # 灿 +U+7080: yáng # 炀 +U+7085: jiǒng # 炅 +U+7086: wén # 炆 +U+7089: lú # 炉 +U+708A: chuī # 炊 +U+708C: kài # 炌 +U+708E: yán # 炎 +U+7092: chǎo # 炒 +U+7094: guì # 炔 -> quē +U+7095: kàng # 炕 +U+7096: dùn # 炖 +U+7098: xīn # 炘 -> xìn +U+7099: zhì # 炙 +U+709C: wěi # 炜 +U+709D: qiàng # 炝 +U+709F: dá # 炟 +U+70A3: kě # 炣 +U+70AB: xuàn # 炫 +U+70AC: jù # 炬 +U+70AD: tàn # 炭 +U+70AE: pào # 炮 -> páo +U+70AF: jiǒng # 炯 +U+70B1: tái # 炱 +U+70B3: bǐng # 炳 +U+70B7: zhù # 炷 +U+70B8: zhà # 炸 +U+70B9: diǎn # 点 +U+70BB: shí # 炻 +U+70BC: liàn # 炼 +U+70BD: chì # 炽 +U+70C0: hū # 烀 +U+70C1: shuò # 烁 +U+70C2: làn # 烂 +U+70C3: tīng # 烃 +U+70C8: liè # 烈 +U+70CA: yáng # 烊 +U+70D4: tóng # 烔 +U+70D8: hōng # 烘 +U+70D9: lào # 烙 +U+70DB: zhú # 烛 +U+70DC: xuǎn # 烜 +U+70DD: zhēng # 烝 +U+70DF: yān # 烟 +U+70E0: huí # 烠 +U+70E4: kǎo # 烤 +U+70E6: fán # 烦 +U+70E7: shāo # 烧 +U+70E8: yè # 烨 +U+70E9: huì # 烩 +U+70EB: tàng # 烫 +U+70EC: jìn # 烬 +U+70ED: rè # 热 +U+70EF: xī # 烯 +U+70F6: tǐng # 烶 +U+70F7: wán # 烷 +U+70F9: pēng # 烹 +U+70FA: lǎng # 烺 +U+70FB: yàn # 烻 +U+70FD: fēng # 烽 +U+7106: juān # 焆 +U+7109: yān # 焉 +U+710A: hàn # 焊 +U+710C: jùn # 焌 -> qū +U+7110: wù # 焐 +U+7113: hán # 焓 +U+7115: huàn # 焕 +U+7116: mèn # 焖 +U+7117: jú # 焗 +U+7118: dào # 焘 +U+7119: bèi # 焙 +U+711A: fén # 焚 +U+711C: kūn # 焜 +U+711E: tūn # 焞 +U+7126: jiāo # 焦 +U+712F: chāo # 焯 -> zhuō +U+7130: yàn # 焰 +U+7131: yàn # 焱 +U+7136: rán # 然 +U+7141: chén # 煁 +U+7143: kuǐ # 煃 +U+7145: duàn # 煅 +U+714A: xuān # 煊 +U+714B: xīng # 煋 +U+714C: huáng # 煌 +U+714E: jiān # 煎 +U+7153: tuān # 煓 +U+715C: yù # 煜 +U+715E: shā # 煞 +U+715F: wèi # 煟 +U+7164: méi # 煤 +U+7166: xù # 煦 +U+7167: zhào # 照 +U+7168: wēi # 煨 +U+716E: zhǔ # 煮 +U+7172: bāo # 煲 +U+7173: hú # 煳 +U+7174: yūn # 煴 +U+7178: biān # 煸 +U+717A: tuì # 煺 +U+717D: shān # 煽 +U+7184: xī # 熄 +U+7187: hè # 熇 +U+718A: xióng # 熊 +U+718F: xūn # 熏 +U+7194: róng # 熔 +U+7198: liū # 熘 +U+7199: xī # 熙 +U+719B: biāo # 熛 +U+719C: cōng # 熜 +U+719F: shú # 熟 +U+71A0: yì # 熠 +U+71A5: tēng # 熥 +U+71A8: yùn # 熨 +U+71AC: áo # 熬 -> āo +U+71B5: shāng # 熵 +U+71B9: xī # 熹 +U+71BB: xī # 熻 +U+71C3: rán # 燃 +U+71CA: shēn # 燊 +U+71CB: jiāo # 燋 +U+71CE: liáo # 燎 +U+71CF: yù # 燏 +U+71D4: fán # 燔 +U+71D5: yàn # 燕 +U+71DA: yì # 燚 +U+71E0: yù # 燠 +U+71E5: zào # 燥 +U+71E7: suì # 燧 +U+71EE: xiè # 燮 +U+71F9: xiǎn # 燹 +U+7206: bào # 爆 +U+7207: ruò # 爇 +U+7214: xī # 爔 +U+721A: yuè # 爚 +U+721D: jué # 爝 +U+721F: guàn # 爟 +U+7228: cuàn # 爨 +U+722A: zhǎo # 爪 +U+722C: pá # 爬 +U+7230: yuán # 爰 +U+7231: ài # 爱 +U+7235: jué # 爵 +U+7236: fù # 父 +U+7237: yé # 爷 +U+7238: bà # 爸 +U+7239: diē # 爹 +U+723B: yáo # 爻 +U+723D: shuǎng # 爽 +U+723F: pán # 爿 +U+7241: kē # 牁 +U+7242: zāng # 牂 +U+7247: piàn # 片 +U+7248: bǎn # 版 +U+724C: pái # 牌 +U+724D: dú # 牍 +U+7252: dié # 牒 +U+7256: yǒu # 牖 +U+7259: yá # 牙 +U+725A: chēng # 牚 +U+725B: niú # 牛 +U+725D: pìn # 牝 +U+725F: móu # 牟 +U+7261: mǔ # 牡 +U+7262: láo # 牢 +U+7264: māng # 牤 +U+7265: fāng # 牥 +U+7266: máo # 牦 +U+7267: mù # 牧 +U+7269: wù # 物 +U+726E: jiàn # 牮 +U+726F: gǔ # 牯 +U+7272: shēng # 牲 +U+7275: qiān # 牵 +U+7279: tè # 特 +U+727A: xī # 牺 +U+727B: máng # 牻 +U+727E: wǔ # 牾 +U+727F: gù # 牿 +U+7280: xī # 犀 +U+7281: lí # 犁 +U+7284: jī # 犄 +U+7287: bēn # 犇 +U+728A: dú # 犊 +U+728B: jù # 犋 +U+728D: jiān # 犍 +U+728F: piān # 犏 +U+7292: kào # 犒 +U+729F: jiàng # 犟 +U+72A8: chōu # 犨 +U+72AC: quǎn # 犬 +U+72AF: fàn # 犯 +U+72B0: qiú # 犰 +U+72B4: àn # 犴 -> hān +U+72B6: zhuàng # 状 +U+72B7: guǎng # 犷 +U+72B8: mà # 犸 -> mǎ +U+72B9: yóu # 犹 +U+72C1: yǔn # 狁 +U+72C2: kuáng # 狂 +U+72C3: niǔ # 狃 +U+72C4: dí # 狄 +U+72C8: bèi # 狈 +U+72C9: pī # 狉 +U+72CD: páo # 狍 +U+72CE: xiá # 狎 +U+72D0: hú # 狐 +U+72D2: fèi # 狒 +U+72D7: gǒu # 狗 +U+72D9: jū # 狙 +U+72DD: xiǎn # 狝 +U+72DE: níng # 狞 +U+72E0: hěn # 狠 +U+72E1: jiǎo # 狡 +U+72E8: róng # 狨 +U+72E9: shòu # 狩 +U+72EC: dú # 独 +U+72ED: xiá # 狭 +U+72EE: shī # 狮 +U+72EF: kuài # 狯 +U+72F0: zhēng # 狰 +U+72F1: yù # 狱 +U+72F2: sūn # 狲 +U+72F3: yú # 狳 +U+72F4: bì # 狴 +U+72F7: juàn # 狷 +U+72F8: lí # 狸 +U+72FA: yín # 狺 +U+72FB: suān # 狻 +U+72FC: láng # 狼 +U+7301: lì # 猁 +U+7303: xiǎn # 猃 +U+7304: jīng # 猄 +U+7307: xiāo # 猇 +U+730A: ní # 猊 +U+730E: liè # 猎 +U+7315: mí # 猕 +U+7316: chāng # 猖 +U+7317: yī # 猗 +U+731B: měng # 猛 +U+731C: cāi # 猜 +U+731D: cù # 猝 +U+731E: shē # 猞 +U+7321: luó # 猡 +U+7322: hú # 猢 +U+7325: wěi # 猥 +U+7329: xīng # 猩 +U+732A: zhū # 猪 +U+732B: māo # 猫 +U+732C: wèi # 猬 +U+732E: xiàn # 献 +U+732F: tuān # 猯 +U+7330: yà # 猰 +U+7331: náo # 猱 +U+7334: hóu # 猴 +U+7337: yóu # 猷 +U+7339: chá # 猹 +U+733A: yáo # 猺 +U+733E: huá # 猾 +U+733F: yuán # 猿 +U+734D: jìng # 獍 +U+7350: zhāng # 獐 +U+7352: áo # 獒 +U+7357: jué # 獗 +U+7360: liáo # 獠 +U+736C: xiè # 獬 +U+736D: tǎ # 獭 +U+736F: xūn # 獯 +U+7374: měng # 獴 -> méng +U+737E: huān # 獾 +U+7383: jué # 玃 +U+7384: xuán # 玄 +U+7387: lǜ # 率 -> shuài +U+7389: yù # 玉 +U+738B: wáng # 王 +U+738E: dīng # 玎 +U+7391: jī # 玑 +U+7392: hóng # 玒 +U+7393: dì # 玓 +U+7395: gān # 玕 +U+7396: jiǔ # 玖 +U+7398: qǐ # 玘 +U+7399: yú # 玙 +U+739A: chàng # 玚 +U+739B: mǎ # 玛 +U+739E: fū # 玞 +U+739F: wén # 玟 -> mín +U+73A0: jiè # 玠 +U+73A1: yá # 玡 -> yà +U+73A2: bīn # 玢 +U+73A4: bàng # 玤 +U+73A5: yuè # 玥 +U+73A6: jué # 玦 +U+73A9: wán # 玩 +U+73AB: méi # 玫 +U+73AD: pín # 玭 +U+73AE: wěi # 玮 +U+73AF: huán # 环 +U+73B0: xiàn # 现 +U+73B1: qiāng # 玱 +U+73B2: líng # 玲 +U+73B3: dài # 玳 +U+73B6: píng # 玶 +U+73B7: diàn # 玷 +U+73B9: xuán # 玹 +U+73BA: xǐ # 玺 +U+73BB: bō # 玻 +U+73BC: cǐ # 玼 -> cī +U+73BF: sháo # 玿 +U+73C0: pò # 珀 +U+73C2: kē # 珂 +U+73C5: shēn # 珅 +U+73C7: zǔ # 珇 +U+73C8: jiā # 珈 +U+73C9: mín # 珉 +U+73CA: shān # 珊 +U+73CB: liǔ # 珋 +U+73CC: bì # 珌 +U+73CD: zhēn # 珍 +U+73CF: jué # 珏 +U+73D0: fà # 珐 +U+73D1: lóng # 珑 +U+73D2: jīn # 珒 +U+73D5: lì # 珕 +U+73D6: guāng # 珖 +U+73D9: gǒng # 珙 +U+73DB: xiù # 珛 +U+73DD: xǔ # 珝 +U+73DE: luò # 珞 +U+73E0: zhū # 珠 +U+73E2: yín # 珢 +U+73E3: xún # 珣 +U+73E5: ěr # 珥 +U+73E6: xiàng # 珦 +U+73E7: yáo # 珧 +U+73E9: háng # 珩 -> héng +U+73EA: guī # 珪 +U+73EB: chōng # 珫 +U+73ED: bān # 班 +U+73F0: dāng # 珰 +U+73F2: huī # 珲 -> hún +U+73F5: chéng # 珵 +U+73F7: wǔ # 珷 +U+73F8: wú # 珸 +U+73F9: chéng # 珹 +U+73FA: jùn # 珺 +U+73FD: tǐng # 珽 +U+7400: hán # 琀 +U+7403: qiú # 球 +U+7404: xuàn # 琄 +U+7405: láng # 琅 +U+7406: lǐ # 理 +U+7407: xiù # 琇 +U+7408: fú # 琈 +U+7409: liú # 琉 +U+740A: yá # 琊 +U+740E: jìn # 琎 -> jīn +U+740F: liǎn # 琏 +U+7410: suǒ # 琐 +U+7414: diàn # 琔 +U+741A: jū # 琚 +U+741B: chēn # 琛 +U+741F: wéi # 琟 +U+7421: chù # 琡 -> shū +U+7422: zuó # 琢 -> zhuó +U+7424: chēng # 琤 +U+7425: hǔ # 琥 +U+7426: qí # 琦 +U+7428: kūn # 琨 +U+742A: qí # 琪 +U+742B: běng # 琫 +U+742C: wǎn # 琬 +U+742D: lù # 琭 +U+742E: cóng # 琮 +U+742F: guǎn # 琯 +U+7430: yǎn # 琰 +U+7432: bèi # 琲 +U+7433: lín # 琳 +U+7434: qín # 琴 +U+7435: pí # 琵 +U+7436: pá # 琶 +U+743C: qióng # 琼 +U+7440: yǔ # 瑀 +U+7441: mào # 瑁 +U+7442: méi # 瑂 +U+7443: chūn # 瑃 +U+7444: xuān # 瑄 +U+7445: tí # 瑅 +U+7446: xīng # 瑆 +U+7451: zhuàn # 瑑 +U+7453: liàn # 瑓 +U+7454: quán # 瑔 +U+7455: xiá # 瑕 +U+7456: duàn # 瑖 +U+7457: yuàn # 瑗 +U+7459: nǎo # 瑙 +U+745A: hú # 瑚 +U+745B: yīng # 瑛 +U+745C: yú # 瑜 +U+745D: huáng # 瑝 +U+745E: ruì # 瑞 +U+745F: sè # 瑟 +U+7462: róng # 瑢 +U+7467: zhēn # 瑧 +U+7468: jìn # 瑨 +U+746C: liú # 瑬 +U+746D: táng # 瑭 +U+7470: guī # 瑰 +U+7471: zhèn # 瑱 -> tiàn +U+7473: cuō # 瑳 +U+7476: yáo # 瑶 +U+7477: ài # 瑷 +U+747E: jǐn # 瑾 +U+7480: cuǐ # 璀 +U+7481: cōng # 璁 +U+7483: lí # 璃 +U+7486: qiú # 璆 +U+7487: xuán # 璇 +U+7488: áo # 璈 +U+748B: zhāng # 璋 +U+748E: yīng # 璎 +U+7490: lù # 璐 +U+7492: dēng # 璒 +U+7498: lín # 璘 +U+749C: huáng # 璜 +U+749E: pú # 璞 +U+749F: jǐng # 璟 +U+74A0: fán # 璠 +U+74A5: jǐng # 璥 +U+74A7: bì # 璧 +U+74A8: càn # 璨 +U+74A9: qú # 璩 +U+74AA: zǎo # 璪 +U+74AC: jiǎo # 璬 +U+74AE: tǎn # 璮 +U+74B1: sè # 璱 +U+74B2: suì # 璲 +U+74BA: wèn # 璺 +U+74C0: ruǎn # 瓀 +U+74D2: zàn # 瓒 +U+74D6: xiāng # 瓖 +U+74D8: guàn # 瓘 +U+74DC: guā # 瓜 +U+74DE: dié # 瓞 +U+74E0: hù # 瓠 +U+74E2: piáo # 瓢 +U+74E3: bàn # 瓣 +U+74E4: ráng # 瓤 +U+74E6: wǎ # 瓦 +U+74EE: wèng # 瓮 +U+74EF: ōu # 瓯 +U+74F4: líng # 瓴 +U+74F6: píng # 瓶 +U+74F7: cí # 瓷 +U+74FB: chī # 瓻 +U+74FF: bù # 瓿 +U+7504: zhēn # 甄 +U+750D: méng # 甍 +U+750F: bèng # 甏 +U+7511: zèng # 甑 +U+7513: pì # 甓 +U+7517: yǎn # 甗 +U+7518: gān # 甘 +U+751A: shèn # 甚 +U+751C: tián # 甜 +U+751F: shēng # 生 +U+7521: shēn # 甡 +U+7525: shēng # 甥 +U+7526: sū # 甦 +U+7528: yòng # 用 +U+7529: shuǎi # 甩 +U+752A: lù # 甪 +U+752B: fǔ # 甫 +U+752C: yǒng # 甬 +U+752D: béng # 甭 +U+752F: níng # 甯 +U+7530: tián # 田 +U+7531: yóu # 由 +U+7532: jiǎ # 甲 +U+7533: shēn # 申 +U+7535: diàn # 电 +U+7537: nán # 男 +U+7538: diān # 甸 -> diàn +U+753A: tīng # 町 -> tǐng +U+753B: huà # 画 +U+753E: zāi # 甾 +U+7540: bì # 畀 +U+7545: chàng # 畅 +U+7548: fàn # 畈 +U+754B: tián # 畋 +U+754C: jiè # 界 +U+754E: quǎn # 畎 +U+754F: wèi # 畏 +U+7554: pàn # 畔 +U+7556: wā # 畖 +U+7559: liú # 留 +U+755A: běn # 畚 +U+755B: zhěn # 畛 +U+755C: chù # 畜 -> xù +U+7564: zhì # 畤 +U+7565: lüè # 略 +U+7566: qí # 畦 +U+756A: fān # 番 +U+756C: shē # 畬 +U+756F: jùn # 畯 +U+7572: shē # 畲 +U+7574: chóu # 畴 +U+7578: jī # 畸 +U+7579: wǎn # 畹 +U+757F: jī # 畿 +U+7581: liú # 疁 +U+7583: tuǎn # 疃 +U+7586: jiāng # 疆 +U+758D: dàn # 疍 +U+758F: shū # 疏 +U+7590: zhì # 疐 +U+7591: yí # 疑 +U+7594: dīng # 疔 +U+7596: jiē # 疖 +U+7597: liáo # 疗 +U+7599: gē # 疙 +U+759A: jiù # 疚 +U+759D: shàn # 疝 +U+759F: nüè # 疟 +U+75A0: lì # 疠 +U+75A1: yáng # 疡 +U+75A2: chèn # 疢 +U+75A3: yóu # 疣 +U+75A4: bā # 疤 +U+75A5: jiè # 疥 +U+75AB: yì # 疫 +U+75AC: lì # 疬 +U+75AD: zòng # 疭 +U+75AE: chuāng # 疮 +U+75AF: fēng # 疯 +U+75B0: zhù # 疰 +U+75B1: pào # 疱 +U+75B2: pí # 疲 +U+75B3: gān # 疳 +U+75B4: kē # 疴 +U+75B5: cī # 疵 +U+75B8: dǎn # 疸 +U+75B9: zhěn # 疹 +U+75BC: téng # 疼 +U+75BD: jū # 疽 +U+75BE: jí # 疾 +U+75C2: jiā # 痂 +U+75C3: xuán # 痃 +U+75C4: zhà # 痄 +U+75C5: bìng # 病 +U+75C7: zhèng # 症 +U+75C8: yōng # 痈 +U+75C9: jìng # 痉 +U+75CA: quán # 痊 +U+75CD: yí # 痍 +U+75D2: yǎng # 痒 +U+75D3: chì # 痓 -> zhì +U+75D4: zhì # 痔 +U+75D5: hén # 痕 +U+75D8: dòu # 痘 +U+75DB: tòng # 痛 +U+75DE: pǐ # 痞 +U+75E2: lì # 痢 +U+75E3: zhì # 痣 +U+75E4: cuó # 痤 +U+75E6: wù # 痦 +U+75E7: shā # 痧 +U+75E8: láo # 痨 +U+75EA: huàn # 痪 +U+75EB: xián # 痫 +U+75F0: tán # 痰 +U+75F1: fèi # 痱 +U+75F4: chī # 痴 +U+75F9: bì # 痹 +U+75FC: gù # 痼 +U+75FF: wěi # 痿 +U+7600: yū # 瘀 +U+7601: cuì # 瘁 +U+7603: zhú # 瘃 +U+7605: dān # 瘅 -> dàn +U+7606: shèn # 瘆 +U+760A: hóu # 瘊 +U+760C: là # 瘌 +U+7610: yǔ # 瘐 +U+7615: jiǎ # 瘕 +U+7617: yì # 瘗 +U+7618: lòu # 瘘 +U+7619: sào # 瘙 +U+761B: chì # 瘛 +U+761F: wēn # 瘟 +U+7620: jí # 瘠 +U+7622: bān # 瘢 +U+7624: liú # 瘤 +U+7625: chài # 瘥 +U+7626: shòu # 瘦 +U+7629: dā # 瘩 -> dá +U+762A: biě # 瘪 -> biē +U+762B: tān # 瘫 +U+762D: biāo # 瘭 +U+7630: luǒ # 瘰 +U+7633: chōu # 瘳 +U+7634: zhàng # 瘴 +U+7635: zhài # 瘵 +U+7638: qué # 瘸 +U+763C: mò # 瘼 +U+763E: yǐn # 瘾 +U+763F: yǐng # 瘿 +U+7640: huáng # 癀 +U+7643: lóng # 癃 +U+764C: ái # 癌 +U+764D: bān # 癍 +U+7654: yì # 癔 +U+7656: pǐ # 癖 +U+7657: lěi # 癗 +U+765C: diàn # 癜 +U+765E: lài # 癞 +U+7663: xuǎn # 癣 +U+766B: diān # 癫 +U+766F: qú # 癯 +U+7678: guǐ # 癸 +U+767B: dēng # 登 +U+767D: bái # 白 +U+767E: bǎi # 百 +U+767F: qié # 癿 +U+7682: zào # 皂 +U+7684: de # 的 +U+7686: jiē # 皆 +U+7687: huáng # 皇 +U+7688: guī # 皈 +U+768B: gāo # 皋 +U+768E: jiǎo # 皎 +U+7691: ái # 皑 +U+7693: hào # 皓 +U+7695: bì # 皕 +U+7696: wǎn # 皖 +U+7699: xī # 皙 +U+769B: xiǎo # 皛 +U+769E: hào # 皞 +U+76A4: pó # 皤 +U+76A6: jiǎo # 皦 +U+76AD: jiào # 皭 +U+76AE: pí # 皮 +U+76B1: zhòu # 皱 +U+76B2: jūn # 皲 +U+76B4: cūn # 皴 +U+76BF: mǐn # 皿 +U+76C2: yú # 盂 +U+76C5: zhōng # 盅 +U+76C6: pén # 盆 +U+76C8: yíng # 盈 +U+76C9: hé # 盉 +U+76CA: yì # 益 +U+76CD: hé # 盍 +U+76CE: àng # 盎 +U+76CF: zhǎn # 盏 +U+76D0: yán # 盐 +U+76D1: jiān # 监 +U+76D2: hé # 盒 +U+76D4: kuī # 盔 +U+76D6: gài # 盖 +U+76D7: dào # 盗 +U+76D8: pán # 盘 +U+76DB: shèng # 盛 +U+76DF: méng # 盟 +U+76E5: guàn # 盥 +U+76E6: ān # 盦 +U+76EE: mù # 目 +U+76EF: dīng # 盯 +U+76F1: xū # 盱 +U+76F2: máng # 盲 +U+76F4: zhí # 直 +U+76F7: tián # 盷 -> xián +U+76F8: xiāng # 相 +U+76F9: dǔn # 盹 +U+76FC: pàn # 盼 +U+76FE: dùn # 盾 +U+7701: shěng # 省 +U+7704: miǎn # 眄 +U+7707: miǎo # 眇 +U+7708: dān # 眈 +U+7709: méi # 眉 +U+770A: mào # 眊 +U+770B: kàn # 看 +U+770D: kōu # 眍 +U+7719: yí # 眙 +U+771A: shěng # 眚 +U+771F: zhēn # 真 +U+7720: mián # 眠 +U+7722: yuān # 眢 +U+7726: zì # 眦 +U+7728: zhǎ # 眨 +U+7729: xuàn # 眩 +U+772C: lóng # 眬 +U+772D: suī # 眭 -> guì +U+772F: mī # 眯 +U+7735: chī # 眵 +U+7736: kuàng # 眶 +U+7737: juàn # 眷 +U+7738: móu # 眸 +U+773A: tiào # 眺 +U+773C: yǎn # 眼 +U+7740: zhe # 着 +U+7741: zhēng # 睁 +U+7743: suō # 睃 +U+7744: shào # 睄 -> qiáo +U+7747: dì # 睇 +U+774E: xī # 睎 +U+7750: lài # 睐 +U+7751: jiǎn # 睑 +U+775A: yá # 睚 +U+775B: jīng # 睛 +U+7761: shuì # 睡 +U+7762: suī # 睢 -> huī +U+7763: dū # 督 +U+7765: pì # 睥 -> bì +U+7766: mù # 睦 +U+7768: nì # 睨 +U+776B: jié # 睫 +U+776C: cǎi # 睬 +U+7779: dǔ # 睹 +U+777D: kuí # 睽 +U+777E: gāo # 睾 +U+777F: ruì # 睿 +U+7780: mào # 瞀 +U+7784: miáo # 瞄 +U+7785: chǒu # 瞅 +U+778B: chēn # 瞋 +U+778C: kē # 瞌 +U+778D: sǒu # 瞍 +U+778E: xiā # 瞎 +U+7791: míng # 瞑 +U+7792: mán # 瞒 +U+779F: piǎo # 瞟 +U+77A0: chēng # 瞠 +U+77A2: méng # 瞢 +U+77A5: piē # 瞥 +U+77A7: qiáo # 瞧 +U+77A9: zhǔ # 瞩 +U+77AA: dèng # 瞪 +U+77AB: shěn # 瞫 +U+77AC: shùn # 瞬 +U+77AD: liǎo # 瞭 +U+77B0: kàn # 瞰 +U+77B3: tóng # 瞳 +U+77B5: lín # 瞵 +U+77BB: zhān # 瞻 +U+77BD: gǔ # 瞽 +U+77BF: qú # 瞿 +U+77CD: jué # 矍 +U+77D7: chù # 矗 +U+77DB: máo # 矛 +U+77DC: jīn # 矜 +U+77DE: yù # 矞 +U+77E2: shǐ # 矢 +U+77E3: yǐ # 矣 +U+77E5: zhī # 知 +U+77E7: shěn # 矧 +U+77E9: jǔ # 矩 +U+77EB: jiǎo # 矫 +U+77EC: cuó # 矬 +U+77ED: duǎn # 短 +U+77EE: ǎi # 矮 +U+77F0: zēng # 矰 +U+77F3: shí # 石 +U+77F6: jī # 矶 +U+77F8: gān # 矸 +U+77FB: kū # 矻 +U+77FC: gāng # 矼 +U+77FE: fán # 矾 +U+77FF: kuàng # 矿 +U+7800: dàng # 砀 +U+7801: mǎ # 码 +U+7802: shā # 砂 +U+7804: jué # 砄 +U+7806: fū # 砆 +U+7809: huò # 砉 -> xū +U+780C: qì # 砌 +U+780D: kǎn # 砍 +U+7811: yà # 砑 +U+7812: pī # 砒 +U+7814: yán # 研 +U+7816: zhuān # 砖 +U+7817: chē # 砗 +U+7818: dùn # 砘 +U+781A: yàn # 砚 +U+781C: fēng # 砜 +U+781D: fá # 砝 -> fǎ +U+781F: zhǎ # 砟 +U+7820: jū # 砠 +U+7823: tuó # 砣 +U+7825: dǐ # 砥 +U+7827: zhēn # 砧 +U+782B: zhù # 砫 +U+782C: lá # 砬 -> lì +U+782D: biān # 砭 +U+782E: nǔ # 砮 +U+7830: pēng # 砰 +U+7834: pò # 破 +U+7835: bō # 砵 +U+7837: shēn # 砷 +U+7838: zá # 砸 +U+7839: ài # 砹 +U+783A: lì # 砺 +U+783B: lóng # 砻 +U+783C: tóng # 砼 +U+783E: lì # 砾 +U+7840: chǔ # 础 +U+7841: kēng # 硁 +U+7845: guī # 硅 +U+7847: náo # 硇 +U+784A: wěi # 硊 +U+784C: gè # 硌 -> luò +U+784D: xiàn # 硍 -> kèn +U+784E: xíng # 硎 +U+7850: dòng # 硐 +U+7852: xī # 硒 +U+7854: hóng # 硔 +U+7855: shuò # 硕 +U+7856: xiá # 硖 +U+7857: qiāo # 硗 +U+7859: wéi # 硙 +U+785A: qiáo # 硚 +U+785D: xiāo # 硝 +U+786A: wò # 硪 +U+786B: liú # 硫 +U+786C: yìng # 硬 +U+786D: máng # 硭 +U+786E: què # 确 +U+787C: péng # 硼 +U+787F: kōng # 硿 +U+7883: qìng # 碃 +U+7887: dìng # 碇 +U+7888: mín # 碈 +U+7889: diāo # 碉 +U+788C: lù # 碌 +U+788D: ài # 碍 +U+788E: suì # 碎 +U+788F: què # 碏 +U+7891: bēi # 碑 +U+7893: duì # 碓 +U+7897: wǎn # 碗 +U+7898: diǎn # 碘 +U+789A: bèi # 碚 +U+789B: qì # 碛 +U+789C: chěn # 碜 +U+789F: dié # 碟 +U+78A1: dú # 碡 -> zhóu +U+78A3: jié # 碣 +U+78A5: biǎn # 碥 +U+78A7: bì # 碧 +U+78A8: wèi # 碨 -> wěi +U+78B0: pèng # 碰 +U+78B1: jiǎn # 碱 +U+78B2: dì # 碲 +U+78B3: tàn # 碳 +U+78B4: chá # 碴 +U+78B6: qì # 碶 +U+78B9: xuàn # 碹 +U+78BE: niǎn # 碾 +U+78C1: cí # 磁 +U+78C5: bàng # 磅 +U+78C9: sǎng # 磉 +U+78CA: lěi # 磊 +U+78CB: cuō # 磋 +U+78CF: lián # 磏 +U+78D0: pán # 磐 +U+78D4: zhé # 磔 +U+78D5: kē # 磕 +U+78D9: gǔn # 磙 +U+78DC: qì # 磜 +U+78E1: kàn # 磡 +U+78E8: mó # 磨 +U+78EC: qìng # 磬 +U+78F2: qú # 磲 +U+78F4: dèng # 磴 +U+78F7: lín # 磷 +U+78F9: tán # 磹 -> diàn +U+78FB: pán # 磻 +U+7901: jiāo # 礁 +U+7905: dūn # 礅 +U+790C: léi # 礌 +U+7913: jiāng # 礓 +U+791E: méng # 礞 +U+7934: bó # 礴 +U+7935: shuāng # 礵 +U+793A: shì # 示 +U+793C: lǐ # 礼 +U+793E: shè # 社 +U+7940: sì # 祀 +U+7941: qí # 祁 +U+7943: mà # 祃 +U+7946: xiān # 祆 +U+7947: qí # 祇 -> zhǐ +U+7948: qí # 祈 +U+7949: zhǐ # 祉 +U+794A: bēng # 祊 +U+794B: duì # 祋 +U+794E: yī # 祎 +U+794F: shí # 祏 +U+7950: yòu # 祐 +U+7953: fú # 祓 +U+7955: mì # 祕 +U+7956: zǔ # 祖 +U+7957: zhī # 祗 +U+795A: zuò # 祚 +U+795B: qū # 祛 +U+795C: hù # 祜 +U+795D: zhù # 祝 +U+795E: shén # 神 +U+795F: suì # 祟 +U+7960: cí # 祠 +U+7962: mí # 祢 +U+7965: xiáng # 祥 +U+7967: tiāo # 祧 +U+7968: piào # 票 +U+796D: jì # 祭 +U+796F: zhēn # 祯 +U+7972: jìn # 祲 +U+7977: dǎo # 祷 +U+7978: huò # 祸 +U+797A: qí # 祺 +U+797C: guàn # 祼 +U+797E: líng # 祾 +U+7980: bǐng # 禀 +U+7981: jìn # 禁 -> jīn +U+7984: lù # 禄 +U+7985: chán # 禅 +U+798A: xì # 禊 +U+798B: yīn # 禋 +U+798F: fú # 福 +U+7992: xiǎn # 禒 +U+7994: zhī # 禔 -> tí +U+7998: dì # 禘 +U+799A: zhuó # 禚 +U+799B: zhēn # 禛 +U+79A4: xuān # 禤 +U+79A7: xǐ # 禧 +U+79B3: ráng # 禳 +U+79B9: yǔ # 禹 +U+79BA: yú # 禺 +U+79BB: lí # 离 +U+79BD: qín # 禽 +U+79BE: hé # 禾 +U+79C0: xiù # 秀 +U+79C1: sī # 私 +U+79C3: tū # 秃 +U+79C6: gǎn # 秆 +U+79C9: bǐng # 秉 +U+79CB: qiū # 秋 +U+79CD: zhǒng # 种 +U+79D1: kē # 科 +U+79D2: miǎo # 秒 +U+79D5: bǐ # 秕 +U+79D8: mì # 秘 +U+79DF: zū # 租 +U+79E3: mò # 秣 +U+79E4: chèng # 秤 +U+79E6: qín # 秦 +U+79E7: yāng # 秧 +U+79E9: zhì # 秩 +U+79EB: shú # 秫 +U+79EC: jù # 秬 +U+79ED: zǐ # 秭 +U+79EF: jī # 积 +U+79F0: chēng # 称 +U+79F8: jiē # 秸 +U+79FB: yí # 移 +U+79FD: huì # 秽 +U+79FE: nóng # 秾 +U+7A00: xī # 稀 +U+7A02: láng # 稂 +U+7A03: fū # 稃 +U+7A06: lǚ # 稆 +U+7A0B: chéng # 程 +U+7A0C: tú # 稌 +U+7A0D: shāo # 稍 +U+7A0E: shuì # 税 +U+7A11: lù # 稑 +U+7A14: rěn # 稔 +U+7A17: bài # 稗 +U+7A19: zhī # 稙 +U+7A1A: zhì # 稚 +U+7A1E: kē # 稞 +U+7A20: chóu # 稠 +U+7A23: sū # 稣 +U+7A33: wěn # 稳 +U+7A37: jì # 稷 +U+7A39: zhěn # 稹 +U+7A3B: dào # 稻 +U+7A3C: jià # 稼 +U+7A3D: jī # 稽 +U+7A3F: gǎo # 稿 +U+7A44: jì # 穄 +U+7A46: mù # 穆 +U+7A51: sè # 穑 +U+7A57: suì # 穗 +U+7A59: pú # 穙 +U+7A5C: zhǒng # 穜 -> tóng +U+7A5F: suì # 穟 +U+7A70: ráng # 穰 +U+7A74: xué # 穴 +U+7A76: jiū # 究 +U+7A77: qióng # 穷 +U+7A78: xī # 穸 +U+7A79: qióng # 穹 +U+7A7A: kōng # 空 +U+7A7F: chuān # 穿 +U+7A80: zhūn # 窀 +U+7A81: tū # 突 +U+7A83: qiè # 窃 +U+7A84: zhǎi # 窄 +U+7A85: yǎo # 窅 +U+7A88: yǎo # 窈 +U+7A8A: wā # 窊 +U+7A8D: qiào # 窍 +U+7A8E: diào # 窎 +U+7A91: yáo # 窑 +U+7A92: zhì # 窒 +U+7A95: tiǎo # 窕 +U+7A96: jiào # 窖 +U+7A97: chuāng # 窗 +U+7A98: jiǒng # 窘 +U+7A9C: cuàn # 窜 +U+7A9D: wō # 窝 +U+7A9F: kū # 窟 +U+7AA0: kē # 窠 +U+7AA3: sū # 窣 +U+7AA5: kuī # 窥 +U+7AA6: dòu # 窦 +U+7AA8: xūn # 窨 -> yìn +U+7AAC: yú # 窬 +U+7AAD: jù # 窭 +U+7AB3: yǔ # 窳 +U+7AB8: xī # 窸 +U+7ABF: lóng # 窿 +U+7ACB: lì # 立 +U+7AD1: hóng # 竑 +U+7AD6: shù # 竖 +U+7AD8: qǔ # 竘 +U+7AD9: zhàn # 站 +U+7ADE: jìng # 竞 +U+7ADF: jìng # 竟 +U+7AE0: zhāng # 章 +U+7AE3: jùn # 竣 +U+7AE5: tóng # 童 +U+7AE6: sǒng # 竦 +U+7AEB: jìng # 竫 +U+7AED: jié # 竭 +U+7AEF: duān # 端 +U+7AF9: zhú # 竹 +U+7AFA: zhú # 竺 +U+7AFD: yú # 竽 +U+7AFF: gān # 竿 +U+7B03: dǔ # 笃 +U+7B04: jī # 笄 +U+7B06: bā # 笆 +U+7B08: jí # 笈 +U+7B0A: zhào # 笊 +U+7B0B: sǔn # 笋 +U+7B0F: hù # 笏 +U+7B11: xiào # 笑 +U+7B14: bǐ # 笔 +U+7B15: jiǎn # 笕 +U+7B19: shēng # 笙 +U+7B1B: dí # 笛 +U+7B1E: chī # 笞 +U+7B20: lì # 笠 +U+7B24: tiáo # 笤 +U+7B25: sì # 笥 +U+7B26: fú # 符 +U+7B28: bèn # 笨 +U+7B2A: dá # 笪 +U+7B2B: zǐ # 笫 +U+7B2C: dì # 第 +U+7B2E: zé # 笮 -> zuó +U+7B2F: nú # 笯 +U+7B31: gǒu # 笱 +U+7B33: jiā # 笳 +U+7B38: pǒ # 笸 +U+7B3A: jiān # 笺 +U+7B3C: lóng # 笼 +U+7B3E: biān # 笾 +U+7B40: guì # 筀 +U+7B45: xiǎn # 筅 +U+7B47: qióng # 筇 +U+7B49: děng # 等 +U+7B4B: jīn # 筋 +U+7B4C: quán # 筌 +U+7B4F: fá # 筏 +U+7B50: kuāng # 筐 +U+7B51: zhù # 筑 +U+7B52: tǒng # 筒 +U+7B54: dá # 答 +U+7B56: cè # 策 +U+7B58: kòu # 筘 +U+7B5A: bì # 筚 +U+7B5B: shāi # 筛 +U+7B5C: dāng # 筜 +U+7B5D: zhēng # 筝 +U+7B60: yún # 筠 +U+7B62: pá # 筢 +U+7B64: láng # 筤 +U+7B65: jǔ # 筥 +U+7B66: guǎn # 筦 +U+7B6E: shì # 筮 +U+7B71: xiǎo # 筱 +U+7B72: shāo # 筲 +U+7B75: yán # 筵 +U+7B76: gào # 筶 +U+7B77: kuài # 筷 +U+7B79: chóu # 筹 +U+7B7B: gàng # 筻 +U+7B7C: yún # 筼 +U+7B7E: qiān # 签 +U+7B80: jiǎn # 简 +U+7B85: bì # 箅 +U+7B8D: gū # 箍 +U+7B90: qìng # 箐 +U+7B93: lù # 箓 +U+7B94: bó # 箔 +U+7B95: jī # 箕 +U+7B96: lín # 箖 +U+7B97: suàn # 算 +U+7B9C: kōng # 箜 +U+7BA1: guǎn # 管 +U+7BA2: yuān # 箢 -> wǎn +U+7BA6: zé # 箦 +U+7BA7: qiè # 箧 +U+7BA8: tuò # 箨 +U+7BA9: luó # 箩 +U+7BAA: dān # 箪 +U+7BAB: xiāo # 箫 +U+7BAC: ruò # 箬 +U+7BAD: jiàn # 箭 +U+7BB1: xiāng # 箱 +U+7BB4: zhēn # 箴 +U+7BB8: zhù # 箸 +U+7BC1: huáng # 篁 +U+7BC6: zhuàn # 篆 +U+7BC7: piān # 篇 +U+7BCC: hóu # 篌 +U+7BD1: kuì # 篑 +U+7BD3: lǒu # 篓 +U+7BD9: gāo # 篙 +U+7BDA: fěi # 篚 +U+7BDD: gōu # 篝 +U+7BE1: cuàn # 篡 +U+7BE5: lì # 篥 +U+7BE6: bì # 篦 +U+7BEA: chí # 篪 +U+7BEE: lán # 篮 +U+7BEF: jiān # 篯 -> jiǎn +U+7BF1: lí # 篱 +U+7BF7: péng # 篷 +U+7BFC: dōu # 篼 +U+7BFE: miè # 篾 +U+7C03: yí # 簃 +U+7C07: cù # 簇 +U+7C09: zào # 簉 +U+7C0B: guǐ # 簋 +U+7C0C: sù # 簌 +U+7C0F: lù # 簏 +U+7C15: lè # 簕 +U+7C16: duàn # 簖 +U+7C1D: liáo # 簝 +U+7C1F: diàn # 簟 +U+7C20: fǔ # 簠 +U+7C27: huáng # 簧 +U+7C2A: zān # 簪 +U+7C30: pái # 簰 +U+7C38: bǒ # 簸 -> bò +U+7C3F: bù # 簿 +U+7C40: zhòu # 籀 +U+7C41: lài # 籁 +U+7C4D: jí # 籍 +U+7C65: yuè # 籥 +U+7C73: mǐ # 米 +U+7C74: dí # 籴 +U+7C7B: lèi # 类 +U+7C7C: xiān # 籼 +U+7C7D: zǐ # 籽 +U+7C89: fěn # 粉 +U+7C91: bā # 粑 +U+7C92: lì # 粒 +U+7C95: pò # 粕 +U+7C97: cū # 粗 +U+7C98: zhān # 粘 -> nián +U+7C9C: tiào # 粜 +U+7C9D: lì # 粝 +U+7C9E: xī # 粞 +U+7C9F: sù # 粟 +U+7CA2: zī # 粢 +U+7CA4: yuè # 粤 +U+7CA5: zhōu # 粥 +U+7CAA: fèn # 粪 +U+7CAE: liáng # 粮 +U+7CB1: liáng # 粱 +U+7CB2: càn # 粲 +U+7CB3: jīng # 粳 +U+7CB9: cuì # 粹 +U+7CBC: lín # 粼 +U+7CBD: zòng # 粽 +U+7CBE: jīng # 精 +U+7CBF: guǒ # 粿 +U+7CC1: sǎn # 糁 +U+7CC5: róu # 糅 +U+7CC7: hóu # 糇 +U+7CC8: xǔ # 糈 +U+7CCA: hú # 糊 -> hū +U+7CCC: zān # 糌 +U+7CCD: cí # 糍 +U+7CD2: bèi # 糒 +U+7CD5: gāo # 糕 +U+7CD6: táng # 糖 +U+7CD7: qiǔ # 糗 +U+7CD9: cāo # 糙 +U+7CDC: mí # 糜 +U+7CDF: zāo # 糟 +U+7CE0: kāng # 糠 +U+7CE8: jiàng # 糨 +U+7CEF: nuò # 糯 +U+7CF5: niè # 糵 +U+7CFB: xì # 系 +U+7D0A: wěn # 紊 +U+7D20: sù # 素 +U+7D22: suǒ # 索 +U+7D27: jǐn # 紧 +U+7D2B: zǐ # 紫 +U+7D2F: lèi # 累 -> léi +U+7D5C: jié # 絜 -> xié +U+7D6E: xù # 絮 +U+7D77: zhí # 絷 +U+7DA6: qí # 綦 +U+7DAE: qǐ # 綮 -> qìng +U+7E20: hú # 縠 +U+7E22: téng # 縢 +U+7E3B: mí # 縻 +U+7E41: fán # 繁 +U+7E44: yī # 繄 +U+7E47: yáo # 繇 +U+7E82: zuǎn # 纂 +U+7E9B: dào # 纛 +U+7EA0: jiū # 纠 +U+7EA1: yū # 纡 +U+7EA2: hóng # 红 +U+7EA3: zhòu # 纣 +U+7EA4: xiān # 纤 +U+7EA5: gē # 纥 -> hé +U+7EA6: yuē # 约 +U+7EA7: jí # 级 +U+7EA8: wán # 纨 +U+7EA9: kuàng # 纩 +U+7EAA: jì # 纪 +U+7EAB: rèn # 纫 +U+7EAC: wěi # 纬 +U+7EAD: yún # 纭 +U+7EAE: hóng # 纮 +U+7EAF: chún # 纯 +U+7EB0: pī # 纰 +U+7EB1: shā # 纱 +U+7EB2: gāng # 纲 +U+7EB3: nà # 纳 +U+7EB4: rèn # 纴 +U+7EB5: zòng # 纵 +U+7EB6: lún # 纶 +U+7EB7: fēn # 纷 +U+7EB8: zhǐ # 纸 +U+7EB9: wén # 纹 +U+7EBA: fǎng # 纺 +U+7EBB: zhù # 纻 +U+7EBC: zhèn # 纼 +U+7EBD: niǔ # 纽 +U+7EBE: shū # 纾 +U+7EBF: xiàn # 线 +U+7EC0: gàn # 绀 +U+7EC1: xiè # 绁 +U+7EC2: fú # 绂 +U+7EC3: liàn # 练 +U+7EC4: zǔ # 组 +U+7EC5: shēn # 绅 +U+7EC6: xì # 细 +U+7EC7: zhī # 织 +U+7EC8: zhōng # 终 +U+7EC9: zhòu # 绉 +U+7ECA: bàn # 绊 +U+7ECB: fú # 绋 +U+7ECC: chù # 绌 +U+7ECD: shào # 绍 +U+7ECE: yì # 绎 +U+7ECF: jīng # 经 +U+7ED0: dài # 绐 +U+7ED1: bǎng # 绑 +U+7ED2: róng # 绒 +U+7ED3: jié # 结 +U+7ED4: kù # 绔 +U+7ED5: rào # 绕 +U+7ED6: dié # 绖 +U+7ED7: háng # 绗 +U+7ED8: huì # 绘 +U+7ED9: gěi # 给 +U+7EDA: xuàn # 绚 +U+7EDB: jiàng # 绛 +U+7EDC: luò # 络 +U+7EDD: jué # 绝 +U+7EDE: jiǎo # 绞 +U+7EDF: tǒng # 统 +U+7EE0: gěng # 绠 +U+7EE1: xiāo # 绡 +U+7EE2: juàn # 绢 +U+7EE3: xiù # 绣 +U+7EE4: xì # 绤 +U+7EE5: suí # 绥 +U+7EE6: tāo # 绦 +U+7EE7: jì # 继 +U+7EE8: tí # 绨 +U+7EE9: jì # 绩 +U+7EEA: xù # 绪 +U+7EEB: líng # 绫 +U+7EED: xù # 续 +U+7EEE: qǐ # 绮 +U+7EEF: fēi # 绯 +U+7EF0: chuò # 绰 +U+7EF1: shàng # 绱 +U+7EF2: gǔn # 绲 +U+7EF3: shéng # 绳 +U+7EF4: wéi # 维 +U+7EF5: mián # 绵 +U+7EF6: shòu # 绶 +U+7EF7: bēng # 绷 +U+7EF8: chóu # 绸 +U+7EF9: táo # 绹 +U+7EFA: liǔ # 绺 +U+7EFB: quǎn # 绻 +U+7EFC: zōng # 综 +U+7EFD: zhàn # 绽 +U+7EFE: wǎn # 绾 +U+7EFF: lǜ # 绿 +U+7F00: zhuì # 缀 +U+7F01: zī # 缁 +U+7F02: kè # 缂 +U+7F03: xiāng # 缃 +U+7F04: jiān # 缄 +U+7F05: miǎn # 缅 +U+7F06: lǎn # 缆 +U+7F07: tí # 缇 +U+7F08: miǎo # 缈 +U+7F09: jī # 缉 +U+7F0A: yūn # 缊 -> yùn +U+7F0C: sī # 缌 +U+7F0E: duàn # 缎 +U+7F10: xiàn # 缐 +U+7F11: gōu # 缑 +U+7F12: zhuì # 缒 +U+7F13: huǎn # 缓 +U+7F14: dì # 缔 +U+7F15: lǚ # 缕 +U+7F16: biān # 编 +U+7F17: mín # 缗 +U+7F18: yuán # 缘 +U+7F19: jìn # 缙 +U+7F1A: fù # 缚 +U+7F1B: rù # 缛 +U+7F1C: zhěn # 缜 +U+7F1D: fèng # 缝 -> féng +U+7F1E: cuī # 缞 +U+7F1F: gǎo # 缟 +U+7F20: chán # 缠 +U+7F21: lí # 缡 +U+7F22: yì # 缢 +U+7F23: jiān # 缣 +U+7F24: bīn # 缤 +U+7F25: piāo # 缥 -> piǎo +U+7F26: màn # 缦 +U+7F27: léi # 缧 +U+7F28: yīng # 缨 +U+7F29: suō # 缩 +U+7F2A: móu # 缪 +U+7F2B: sāo # 缫 +U+7F2C: xié # 缬 +U+7F2D: liáo # 缭 +U+7F2E: shàn # 缮 +U+7F2F: zēng # 缯 +U+7F30: jiāng # 缰 +U+7F31: qiǎn # 缱 +U+7F32: qiāo # 缲 +U+7F33: huán # 缳 +U+7F34: jiǎo # 缴 +U+7F35: zuǎn # 缵 +U+7F36: fǒu # 缶 +U+7F38: gāng # 缸 +U+7F3A: quē # 缺 +U+7F42: yīng # 罂 +U+7F44: qìng # 罄 +U+7F45: xià # 罅 +U+7F4D: léi # 罍 +U+7F50: guàn # 罐 +U+7F51: wǎng # 网 +U+7F54: wǎng # 罔 +U+7F55: hǎn # 罕 +U+7F57: luó # 罗 +U+7F58: fú # 罘 +U+7F5A: fá # 罚 +U+7F5F: gǔ # 罟 +U+7F61: gāng # 罡 +U+7F62: bà # 罢 +U+7F68: yǎn # 罨 +U+7F69: zhào # 罩 +U+7F6A: zuì # 罪 +U+7F6E: zhì # 置 +U+7F71: lǎn # 罱 +U+7F72: shǔ # 署 +U+7F74: pí # 罴 +U+7F76: liǔ # 罶 +U+7F79: lí # 罹 +U+7F7D: jì # 罽 +U+7F7E: zēng # 罾 +U+7F81: jī # 羁 +U+7F8A: yáng # 羊 +U+7F8C: qiāng # 羌 +U+7F8E: měi # 美 +U+7F91: yǒu # 羑 +U+7F93: bā # 羓 +U+7F94: gāo # 羔 +U+7F95: yàng # 羕 +U+7F96: gǔ # 羖 +U+7F9A: líng # 羚 +U+7F9D: dī # 羝 +U+7F9E: xiū # 羞 +U+7F9F: qiǎng # 羟 +U+7FA1: xiàn # 羡 +U+7FA4: qún # 群 +U+7FA7: suō # 羧 +U+7FAF: jié # 羯 +U+7FB0: tāng # 羰 +U+7FB1: yuán # 羱 +U+7FB2: xī # 羲 +U+7FB8: léi # 羸 +U+7FB9: gēng # 羹 +U+7FBC: chàn # 羼 +U+7FBD: yǔ # 羽 +U+7FBF: yì # 羿 +U+7FC0: chōng # 翀 +U+7FC1: wēng # 翁 +U+7FC2: fēn # 翂 +U+7FC3: hóng # 翃 +U+7FC5: chì # 翅 +U+7FC8: xiá # 翈 +U+7FCA: yì # 翊 +U+7FCC: yì # 翌 +U+7FCE: líng # 翎 +U+7FD4: xiáng # 翔 +U+7FD5: xī # 翕 +U+7FD8: qiào # 翘 -> qiáo +U+7FD9: huì # 翙 +U+7FDA: huī # 翚 +U+7FDB: xiāo # 翛 +U+7FDF: dí # 翟 +U+7FE0: cuì # 翠 +U+7FE1: fěi # 翡 +U+7FE5: zhù # 翥 +U+7FE6: jiǎn # 翦 +U+7FE9: piān # 翩 +U+7FEE: hé # 翮 +U+7FEF: hè # 翯 +U+7FF0: hàn # 翰 +U+7FF1: áo # 翱 +U+7FF3: yì # 翳 +U+7FF7: lín # 翷 +U+7FFB: fān # 翻 +U+7FFC: yì # 翼 +U+7FFE: xuān # 翾 +U+8000: yào # 耀 +U+8001: lǎo # 老 +U+8003: kǎo # 考 +U+8004: mào # 耄 +U+8005: zhě # 者 +U+8006: qí # 耆 +U+8007: gǒu # 耇 +U+800B: dié # 耋 +U+800C: ér # 而 +U+800D: shuǎ # 耍 +U+800F: nài # 耏 -> ér +U+8010: nài # 耐 +U+8011: duān # 耑 +U+8012: lěi # 耒 +U+8014: zǐ # 耔 +U+8015: gēng # 耕 +U+8016: chào # 耖 +U+8017: hào # 耗 +U+8018: yún # 耘 +U+8019: bà # 耙 +U+801C: sì # 耜 +U+8020: huō # 耠 +U+8022: lào # 耢 +U+8024: jí # 耤 +U+8025: tāng # 耥 -> tǎng +U+8026: ǒu # 耦 +U+8027: lóu # 耧 +U+8028: nòu # 耨 +U+8029: jiǎng # 耩 +U+802A: pǎng # 耪 +U+8030: yōu # 耰 +U+8031: mò # 耱 +U+8033: ěr # 耳 +U+8035: dīng # 耵 +U+8036: yé # 耶 +U+8037: dā # 耷 +U+8038: sǒng # 耸 +U+803B: chǐ # 耻 +U+803D: dān # 耽 +U+803F: gěng # 耿 +U+8042: niè # 聂 +U+8043: dān # 聃 +U+8046: líng # 聆 +U+804A: liáo # 聊 +U+804B: lóng # 聋 +U+804C: zhí # 职 +U+804D: níng # 聍 +U+8052: guā # 聒 -> guō +U+8054: lián # 联 +U+8058: pìn # 聘 +U+805A: jù # 聚 +U+8069: kuì # 聩 +U+806A: cōng # 聪 +U+8071: áo # 聱 +U+807F: yù # 聿 +U+8083: sù # 肃 +U+8084: yì # 肄 +U+8086: sì # 肆 +U+8087: zhào # 肇 +U+8089: ròu # 肉 +U+808B: lē # 肋 -> lèi +U+808C: jī # 肌 +U+8093: huāng # 肓 +U+8096: xiào # 肖 -> xiāo +U+8098: zhǒu # 肘 +U+809A: dù # 肚 +U+809B: gāng # 肛 +U+809D: gān # 肝 +U+809F: wò # 肟 +U+80A0: cháng # 肠 +U+80A1: gǔ # 股 +U+80A2: zhī # 肢 +U+80A4: fū # 肤 +U+80A5: féi # 肥 +U+80A9: jiān # 肩 +U+80AA: fáng # 肪 +U+80AB: zhūn # 肫 +U+80AD: nà # 肭 +U+80AE: āng # 肮 +U+80AF: kěn # 肯 +U+80B1: gōng # 肱 +U+80B2: yù # 育 +U+80B4: yáo # 肴 +U+80B7: qiǎn # 肷 +U+80B8: xī # 肸 +U+80BA: fèi # 肺 +U+80BC: jǐng # 肼 +U+80BD: tài # 肽 +U+80BE: shèn # 肾 +U+80BF: zhǒng # 肿 +U+80C0: zhàng # 胀 +U+80C1: xié # 胁 +U+80C2: shèn # 胂 +U+80C3: wèi # 胃 +U+80C4: zhòu # 胄 +U+80C6: dǎn # 胆 +U+80C8: bá # 胈 +U+80CC: bèi # 背 +U+80CD: guā # 胍 +U+80CE: tāi # 胎 +U+80D6: pàng # 胖 +U+80D7: zhēn # 胗 +U+80D9: zuò # 胙 +U+80DA: pēi # 胚 +U+80DB: jiǎ # 胛 +U+80DC: shèng # 胜 +U+80DD: zhī # 胝 +U+80DE: bāo # 胞 +U+80E0: qū # 胠 +U+80E1: hú # 胡 +U+80E3: chǐ # 胣 +U+80E4: yìn # 胤 +U+80E5: xū # 胥 +U+80E7: lóng # 胧 +U+80E8: dòng # 胨 +U+80E9: kǎ # 胩 +U+80EA: lú # 胪 +U+80EB: jìng # 胫 +U+80EC: nǔ # 胬 +U+80ED: yān # 胭 +U+80EF: kuà # 胯 +U+80F0: yí # 胰 +U+80F1: guāng # 胱 +U+80F2: hǎi # 胲 +U+80F3: gē # 胳 +U+80F4: dòng # 胴 +U+80F6: jiāo # 胶 +U+80F8: xiōng # 胸 +U+80FA: àn # 胺 +U+80FC: pián # 胼 +U+80FD: néng # 能 +U+8102: zhī # 脂 +U+8106: cuì # 脆 +U+8109: mài # 脉 +U+810A: jí # 脊 -> jǐ +U+810D: kuài # 脍 +U+810E: sà # 脎 +U+810F: zàng # 脏 +U+8110: qí # 脐 +U+8111: nǎo # 脑 +U+8112: mǐ # 脒 +U+8113: nóng # 脓 +U+8114: luán # 脔 +U+8116: bó # 脖 +U+8118: wǎn # 脘 +U+811A: jiǎo # 脚 +U+811E: cuǒ # 脞 +U+811F: liè # 脟 +U+8129: xiū # 脩 +U+812C: pāo # 脬 +U+812F: pú # 脯 -> fǔ +U+8131: tuō # 脱 +U+8132: niào # 脲 +U+8136: luó # 脶 +U+8138: liǎn # 脸 +U+813E: pí # 脾 +U+813F: biāo # 脿 +U+8146: tiǎn # 腆 +U+8148: jīng # 腈 +U+814A: là # 腊 +U+814B: yè # 腋 +U+814C: yān # 腌 -> ā +U+8150: fǔ # 腐 +U+8151: fǔ # 腑 +U+8152: jū # 腒 +U+8153: féi # 腓 +U+8154: qiāng # 腔 +U+8155: wàn # 腕 +U+8158: guó # 腘 +U+8159: zōng # 腙 +U+815A: dìng # 腚 +U+8160: còu # 腠 +U+8165: xīng # 腥 +U+8167: shù # 腧 +U+8168: shuàn # 腨 +U+8169: nǎn # 腩 +U+816D: è # 腭 +U+816E: sāi # 腮 +U+816F: tú # 腯 +U+8170: yāo # 腰 +U+8171: jiàn # 腱 +U+8174: yú # 腴 +U+8179: fù # 腹 +U+817A: xiàn # 腺 +U+817B: nì # 腻 +U+817C: miǎn # 腼 +U+817D: wà # 腽 +U+817E: téng # 腾 +U+817F: tuǐ # 腿 +U+8180: bǎng # 膀 +U+8182: lǚ # 膂 +U+8188: gé # 膈 +U+818A: bó # 膊 +U+818F: gāo # 膏 +U+8191: bìn # 膑 +U+8198: biāo # 膘 +U+8199: jiǎng # 膙 +U+819B: táng # 膛 +U+819C: mó # 膜 +U+819D: xī # 膝 +U+81A6: lìn # 膦 +U+81A8: péng # 膨 +U+81B3: shàn # 膳 +U+81BA: yīng # 膺 +U+81BB: shān # 膻 +U+81C0: tún # 臀 +U+81C2: bì # 臂 +U+81C3: yōng # 臃 +U+81C6: yì # 臆 +U+81CA: sāo # 臊 +U+81CC: gǔ # 臌 +U+81D1: nào # 臑 +U+81DC: zā # 臜 +U+81E3: chén # 臣 +U+81E7: zāng # 臧 +U+81EA: zì # 自 +U+81EC: niè # 臬 +U+81ED: chòu # 臭 +U+81F3: zhì # 至 +U+81F4: zhì # 致 +U+81FB: zhēn # 臻 +U+81FC: jiù # 臼 +U+81FE: yú # 臾 +U+8200: yǎo # 舀 +U+8201: yú # 舁 +U+8202: chōng # 舂 +U+8204: xì # 舄 +U+8205: jiù # 舅 +U+8206: yú # 舆 +U+820C: shé # 舌 +U+820D: shě # 舍 +U+8210: shì # 舐 +U+8212: shū # 舒 +U+8214: tiǎn # 舔 +U+821B: chuǎn # 舛 +U+821C: shùn # 舜 +U+821E: wǔ # 舞 +U+821F: zhōu # 舟 +U+8220: dāo # 舠 +U+8222: shān # 舢 +U+8223: yǐ # 舣 +U+8225: pā # 舥 +U+822A: háng # 航 +U+822B: fǎng # 舫 +U+822C: bān # 般 +U+822D: bǐ # 舭 +U+822F: zhōng # 舯 +U+8230: jiàn # 舰 +U+8231: cāng # 舱 +U+8232: líng # 舲 +U+8233: zhú # 舳 +U+8234: zé # 舴 +U+8235: duò # 舵 +U+8236: bó # 舶 +U+8237: xián # 舷 +U+8238: gě # 舸 +U+8239: chuán # 船 +U+823B: lú # 舻 +U+823E: xī # 舾 +U+8244: shāo # 艄 +U+8245: yú # 艅 +U+8247: tǐng # 艇 +U+8249: wěi # 艉 +U+824B: měng # 艋 +U+824E: huáng # 艎 +U+824F: shǒu # 艏 +U+8258: sōu # 艘 +U+825A: cáo # 艚 +U+825F: chōng # 艟 +U+8268: méng # 艨 +U+826E: gěn # 艮 -> gèn +U+826F: liáng # 良 +U+8270: jiān # 艰 +U+8272: sè # 色 +U+8273: yàn # 艳 +U+8274: fú # 艴 +U+827A: yì # 艺 +U+827D: jiāo # 艽 +U+827E: ài # 艾 +U+827F: nǎi # 艿 +U+8282: jié # 节 +U+8283: péng # 芃 +U+8284: wán # 芄 +U+8288: mǐ # 芈 +U+828A: qiān # 芊 +U+828B: yù # 芋 +U+828D: sháo # 芍 +U+828E: qiōng # 芎 -> xiōng +U+828F: dù # 芏 +U+8291: qǐ # 芑 +U+8292: máng # 芒 +U+8297: xiāng # 芗 +U+8298: pí # 芘 -> bì +U+8299: fú # 芙 +U+829C: wú # 芜 +U+829D: zhī # 芝 +U+829F: shān # 芟 +U+82A0: wén # 芠 +U+82A1: qiàn # 芡 +U+82A3: fú # 芣 +U+82A4: kōu # 芤 +U+82A5: jiè # 芥 +U+82A6: lú # 芦 +U+82A8: jī # 芨 +U+82A9: qín # 芩 +U+82AA: qí # 芪 +U+82AB: yán # 芫 -> yuán +U+82AC: fēn # 芬 +U+82AD: bā # 芭 +U+82AE: ruì # 芮 +U+82AF: xīn # 芯 +U+82B0: jì # 芰 +U+82B1: huā # 花 +U+82B3: fāng # 芳 +U+82B4: wù # 芴 +U+82B7: zhǐ # 芷 +U+82B8: yún # 芸 +U+82B9: qín # 芹 +U+82BC: mào # 芼 -> máo +U+82BD: yá # 芽 +U+82BE: fèi # 芾 +U+82C1: cōng # 苁 +U+82C4: biàn # 苄 +U+82C7: wěi # 苇 +U+82C8: lì # 苈 +U+82C9: pǐ # 苉 +U+82CA: è # 苊 +U+82CB: xiàn # 苋 +U+82CC: cháng # 苌 +U+82CD: cāng # 苍 +U+82CE: zhù # 苎 +U+82CF: sū # 苏 +U+82D1: yuàn # 苑 +U+82D2: rǎn # 苒 +U+82D3: líng # 苓 +U+82D4: tái # 苔 +U+82D5: sháo # 苕 -> tiáo +U+82D7: miáo # 苗 +U+82D8: qǐng # 苘 +U+82DB: kē # 苛 +U+82DC: mù # 苜 +U+82DE: bāo # 苞 +U+82DF: gǒu # 苟 +U+82E0: mín # 苠 +U+82E1: yǐ # 苡 +U+82E3: jù # 苣 +U+82E4: piě # 苤 +U+82E5: ruò # 若 +U+82E6: kǔ # 苦 +U+82E7: níng # 苧 -> zhù +U+82EB: shān # 苫 +U+82EF: běn # 苯 +U+82F1: yīng # 英 +U+82F4: jū # 苴 +U+82F7: gān # 苷 +U+82F9: píng # 苹 +U+82FB: fú # 苻 +U+82FE: bì # 苾 +U+8300: fú # 茀 +U+8301: zhuó # 茁 +U+8302: mào # 茂 +U+8303: fàn # 范 +U+8304: jiā # 茄 -> qié +U+8305: máo # 茅 +U+8306: máo # 茆 +U+8308: cí # 茈 -> zǐ +U+8309: mò # 茉 +U+830B: zhǐ # 茋 +U+830C: chí # 茌 +U+830E: jīng # 茎 +U+830F: lóng # 茏 +U+8311: niǎo # 茑 +U+8313: xué # 茓 +U+8314: yíng # 茔 +U+8315: qióng # 茕 +U+8317: míng # 茗 +U+831A: yìn # 茚 +U+831B: gèn # 茛 +U+831C: qiàn # 茜 +U+831D: chǎi # 茝 +U+8327: jiǎn # 茧 +U+8328: cí # 茨 +U+832B: máng # 茫 +U+832C: chá # 茬 +U+832D: jiāo # 茭 +U+832F: fú # 茯 +U+8331: zhū # 茱 +U+8333: jiāng # 茳 +U+8334: huí # 茴 +U+8335: yīn # 茵 +U+8336: chá # 茶 +U+8338: rōng # 茸 -> róng +U+8339: rú # 茹 +U+833A: chōng # 茺 +U+833C: tóng # 茼 +U+833D: zhòng # 茽 +U+8340: xún # 荀 +U+8341: huán # 荁 +U+8343: quán # 荃 +U+8344: gāi # 荄 +U+8346: jīng # 荆 +U+8347: xìng # 荇 +U+8349: cǎo # 草 +U+834F: rěn # 荏 +U+8350: jiàn # 荐 +U+8351: tí # 荑 -> yí +U+8352: huāng # 荒 +U+8353: píng # 荓 +U+8354: lì # 荔 +U+8356: lǎo # 荖 +U+8359: dá # 荙 +U+835A: jiá # 荚 +U+835B: ráo # 荛 +U+835C: bì # 荜 +U+835E: qiáo # 荞 +U+835F: huì # 荟 +U+8360: jì # 荠 +U+8361: dàng # 荡 +U+8363: róng # 荣 +U+8364: hūn # 荤 +U+8365: xíng # 荥 +U+8366: luò # 荦 +U+8367: yíng # 荧 +U+8368: xún # 荨 -> qián +U+8369: jìn # 荩 +U+836A: sūn # 荪 +U+836B: yīn # 荫 +U+836C: mǎi # 荬 +U+836D: hóng # 荭 +U+836E: zhòu # 荮 +U+836F: yào # 药 +U+8377: hé # 荷 +U+8378: bí # 荸 +U+837B: dí # 荻 +U+837C: tú # 荼 +U+837D: suī # 荽 +U+8385: lì # 莅 +U+8386: pú # 莆 +U+8389: lì # 莉 +U+838E: shā # 莎 -> suō +U+8392: jǔ # 莒 +U+8393: méi # 莓 +U+8398: shēn # 莘 +U+8399: jūn # 莙 +U+839B: tíng # 莛 +U+839C: yóu # 莜 +U+839D: cuò # 莝 +U+839E: guǎn # 莞 -> guān +U+83A0: yǒu # 莠 +U+83A8: làng # 莨 +U+83A9: fú # 莩 +U+83AA: é # 莪 +U+83AB: mò # 莫 +U+83B0: kǎn # 莰 +U+83B1: lái # 莱 +U+83B2: lián # 莲 +U+83B3: shí # 莳 -> shì +U+83B4: wō # 莴 +U+83B6: xiān # 莶 +U+83B7: huò # 获 +U+83B8: yóu # 莸 +U+83B9: yíng # 莹 +U+83BA: yīng # 莺 +U+83BC: chún # 莼 +U+83BD: mǎng # 莽 +U+83BF: cì # 莿 +U+83C0: wǎn # 菀 +U+83C1: jīng # 菁 +U+83C2: dì # 菂 +U+83C5: jiān # 菅 +U+83C7: gū # 菇 +U+83C9: lù # 菉 +U+83CA: jú # 菊 +U+83CC: jūn # 菌 +U+83CD: niè # 菍 +U+83CF: hé # 菏 +U+83D4: fú # 菔 +U+83D6: chāng # 菖 +U+83D8: sōng # 菘 +U+83DC: cài # 菜 +U+83DD: bá # 菝 +U+83DF: tú # 菟 -> tù +U+83E0: bō # 菠 +U+83E1: hàn # 菡 +U+83E5: xī # 菥 +U+83E9: pú # 菩 +U+83EA: dàng # 菪 +U+83F0: gū # 菰 +U+83F1: líng # 菱 +U+83F2: fēi # 菲 +U+83F9: jū # 菹 -> zū +U+83FC: tǎn # 菼 +U+83FD: shū # 菽 +U+8401: qí # 萁 +U+8403: cuì # 萃 +U+8404: táo # 萄 +U+8406: bì # 萆 +U+840B: qī # 萋 +U+840C: méng # 萌 +U+840D: píng # 萍 +U+840E: wēi # 萎 -> wěi +U+840F: dàn # 萏 +U+8411: huán # 萑 +U+8418: nài # 萘 +U+841A: tuò # 萚 +U+841C: tiē # 萜 +U+841D: luó # 萝 +U+8423: dìng # 萣 +U+8424: yíng # 萤 +U+8425: yíng # 营 +U+8426: yíng # 萦 +U+8427: xiāo # 萧 +U+8428: sà # 萨 +U+8429: qiū # 萩 +U+8431: xuān # 萱 +U+8433: nǎn # 萳 +U+8438: yú # 萸 +U+8439: biǎn # 萹 -> biān +U+843C: è # 萼 +U+843D: luò # 落 -> là +U+8446: bǎo # 葆 +U+844E: lǜ # 葎 +U+8451: fēng # 葑 +U+8456: tū # 葖 +U+8457: zhù # 著 +U+8459: xiāng # 葙 +U+845A: rèn # 葚 -> shèn +U+845B: gé # 葛 +U+845C: qiā # 葜 +U+8461: pú # 葡 +U+8463: dǒng # 董 +U+8469: pā # 葩 +U+846B: hú # 葫 +U+846C: zàng # 葬 +U+846D: jiā # 葭 +U+8470: suī # 葰 -> jùn +U+8471: cōng # 葱 +U+8473: wēi # 葳 +U+8474: zhēn # 葴 +U+8475: kuí # 葵 +U+8476: tíng # 葶 +U+8478: xǐ # 葸 +U+847A: qì # 葺 +U+8482: dì # 蒂 +U+8484: guān # 蒄 +U+8487: chǎn # 蒇 +U+8488: kǎi # 蒈 +U+8489: kuì # 蒉 +U+848B: jiǎng # 蒋 +U+848C: lóu # 蒌 +U+848E: pài # 蒎 +U+8490: sōu # 蒐 +U+8497: làng # 蒗 +U+8499: méng # 蒙 -> mēng +U+849C: suàn # 蒜 +U+849F: jǔ # 蒟 +U+84A1: bàng # 蒡 +U+84A8: qiàn # 蒨 +U+84AF: kuǎi # 蒯 +U+84B1: pú # 蒱 +U+84B2: pú # 蒲 +U+84B4: shuò # 蒴 +U+84B8: zhēng # 蒸 +U+84B9: jiān # 蒹 +U+84BA: jí # 蒺 +U+84BB: ruò # 蒻 +U+84BD: ēn # 蒽 +U+84BF: hāo # 蒿 +U+84C1: zhēn # 蓁 +U+84C2: míng # 蓂 +U+84C4: xù # 蓄 +U+84C7: gǔ # 蓇 -> gū +U+84C9: róng # 蓉 +U+84CA: wěng # 蓊 +U+84CD: shī # 蓍 +U+84CF: luǒ # 蓏 +U+84D0: rù # 蓐 +U+84D1: suō # 蓑 +U+84D3: bèi # 蓓 +U+84D6: bì # 蓖 +U+84DD: lán # 蓝 +U+84DF: jì # 蓟 +U+84E0: lí # 蓠 +U+84E2: lǎng # 蓢 +U+84E3: yù # 蓣 +U+84E5: yíng # 蓥 +U+84E6: mò # 蓦 +U+84EC: péng # 蓬 +U+84F0: xǐ # 蓰 +U+84FC: liǎo # 蓼 +U+84FF: xu # 蓿 -> xù +U+8500: bù # 蔀 +U+8503: qiáng # 蔃 +U+8508: biāo # 蔈 +U+850A: hǎn # 蔊 -> hàn +U+850C: sù # 蔌 +U+8511: miè # 蔑 +U+8513: màn # 蔓 +U+8517: zhè # 蔗 +U+851A: wèi # 蔚 +U+851F: cù # 蔟 +U+8521: cài # 蔡 +U+852B: niān # 蔫 +U+852C: shū # 蔬 +U+8537: qiáng # 蔷 +U+8538: dōu # 蔸 +U+8539: liǎn # 蔹 +U+853A: lìn # 蔺 +U+853B: kòu # 蔻 +U+853C: ǎi # 蔼 +U+853D: bì # 蔽 +U+8543: fān # 蕃 +U+8548: xùn # 蕈 +U+8549: jiāo # 蕉 +U+854A: ruǐ # 蕊 +U+8556: qú # 蕖 +U+8557: lù # 蕗 +U+8559: huì # 蕙 +U+855E: zuì # 蕞 +U+8564: ruí # 蕤 +U+8568: jué # 蕨 +U+8570: wēn # 蕰 -> yùn +U+8572: qí # 蕲 +U+8574: yùn # 蕴 +U+8579: wèng # 蕹 +U+857A: jí # 蕺 +U+857B: hóng # 蕻 -> hòng +U+857E: lěi # 蕾 +U+8581: yù # 薁 +U+8584: báo # 薄 +U+8585: hāo # 薅 +U+8587: wēi # 薇 +U+858F: yì # 薏 +U+859B: xuē # 薛 +U+859C: bì # 薜 +U+85A2: xiè # 薢 +U+85A4: xiè # 薤 +U+85A8: hōng # 薨 +U+85AA: xīn # 薪 +U+85AE: sǒu # 薮 +U+85AF: shǔ # 薯 +U+85B0: xūn # 薰 +U+85B3: wěi # 薳 -> yuǎn +U+85B7: rú # 薷 +U+85B8: piáo # 薸 +U+85B9: tái # 薹 +U+85BF: nǐ # 薿 +U+85C1: gǎo # 藁 +U+85C9: jí # 藉 -> jiè +U+85CF: cáng # 藏 +U+85D0: miǎo # 藐 +U+85D3: xiǎn # 藓 +U+85D5: ǒu # 藕 +U+85DC: lí # 藜 +U+85DF: lěi # 藟 +U+85E0: jiào # 藠 +U+85E4: téng # 藤 +U+85E6: mò # 藦 +U+85E8: biāo # 藨 +U+85E9: fān # 藩 +U+85FB: zǎo # 藻 +U+85FF: huò # 藿 +U+8605: héng # 蘅 +U+8611: mó # 蘑 +U+8616: niè # 蘖 +U+8618: ráng # 蘘 +U+8627: qú # 蘧 +U+8629: fán # 蘩 +U+8638: zhàn # 蘸 +U+863C: mí # 蘼 +U+864E: hǔ # 虎 +U+864F: lǔ # 虏 +U+8650: nüè # 虐 +U+8651: lǜ # 虑 +U+8652: sī # 虒 +U+8653: xiāo # 虓 +U+8654: qián # 虔 +U+865A: xū # 虚 +U+865E: yú # 虞 +U+8662: guó # 虢 +U+8664: yán # 虤 +U+866B: chóng # 虫 +U+866C: qiú # 虬 +U+866E: jǐ # 虮 +U+8671: shī # 虱 +U+8677: hán # 虷 +U+8678: zǐ # 虸 +U+8679: hóng # 虹 +U+867A: huī # 虺 -> huǐ +U+867B: méng # 虻 +U+867C: gè # 虼 +U+867D: suī # 虽 +U+867E: xiā # 虾 +U+867F: chài # 虿 +U+8680: shí # 蚀 +U+8681: yǐ # 蚁 +U+8682: mǎ # 蚂 +U+8684: fāng # 蚄 +U+8686: bā # 蚆 +U+868A: wén # 蚊 +U+868B: ruì # 蚋 +U+868C: bàng # 蚌 +U+868D: pí # 蚍 +U+8693: yǐn # 蚓 +U+8695: cán # 蚕 +U+869C: yá # 蚜 +U+869D: háo # 蚝 +U+86A3: gōng # 蚣 +U+86A4: zǎo # 蚤 +U+86A7: jiè # 蚧 +U+86A8: fú # 蚨 +U+86A9: chī # 蚩 +U+86AA: dǒu # 蚪 +U+86AC: xiǎn # 蚬 +U+86AF: qiū # 蚯 +U+86B0: yóu # 蚰 +U+86B1: zhà # 蚱 +U+86B2: píng # 蚲 +U+86B4: yòu # 蚴 +U+86B6: hān # 蚶 +U+86BA: rán # 蚺 +U+86C0: zhù # 蛀 +U+86C3: bǐng # 蛃 -> bīng +U+86C4: gū # 蛄 +U+86C6: qū # 蛆 +U+86C7: shé # 蛇 +U+86C9: líng # 蛉 +U+86CA: gǔ # 蛊 +U+86CB: dàn # 蛋 +U+86CE: lì # 蛎 +U+86CF: chēng # 蛏 +U+86D0: qū # 蛐 +U+86D1: móu # 蛑 +U+86D4: huí # 蛔 +U+86D8: yáng # 蛘 +U+86D9: wā # 蛙 +U+86DB: zhū # 蛛 +U+86DE: kuò # 蛞 +U+86DF: jiāo # 蛟 +U+86E4: há # 蛤 -> gé +U+86E9: qióng # 蛩 +U+86ED: zhì # 蛭 +U+86EE: mán # 蛮 +U+86F0: zhé # 蛰 +U+86F1: jiá # 蛱 +U+86F2: náo # 蛲 +U+86F3: sī # 蛳 +U+86F4: qí # 蛴 +U+86F8: shāo # 蛸 -> xiāo +U+86F9: yǒng # 蛹 +U+86FE: é # 蛾 +U+8700: shǔ # 蜀 +U+8702: fēng # 蜂 +U+8703: shèn # 蜃 +U+8707: zhē # 蜇 -> zhé +U+8708: wú # 蜈 +U+8709: fú # 蜉 +U+870A: lí # 蜊 -> lì +U+870D: chú # 蜍 +U+870E: yuān # 蜎 +U+8710: jié # 蜐 +U+8712: yán # 蜒 +U+8713: tíng # 蜓 +U+8715: tuì # 蜕 +U+8717: wō # 蜗 +U+8718: zhī # 蜘 +U+871A: fēi # 蜚 +U+871C: mì # 蜜 +U+871E: qí # 蜞 +U+8721: là # 蜡 +U+8722: měng # 蜢 +U+8723: qiāng # 蜣 +U+8725: xī # 蜥 +U+8729: tiáo # 蜩 +U+872E: yù # 蜮 +U+8731: pí # 蜱 +U+8734: yì # 蜴 +U+8737: quán # 蜷 +U+873B: qīng # 蜻 +U+873E: guǒ # 蜾 +U+873F: wān # 蜿 +U+8747: yíng # 蝇 +U+8748: guō # 蝈 +U+8749: chán # 蝉 +U+874C: kē # 蝌 +U+874E: xiē # 蝎 +U+8753: yú # 蝓 +U+8757: huáng # 蝗 +U+8758: yǎn # 蝘 +U+8759: biān # 蝙 +U+8760: fú # 蝠 +U+8763: yóu # 蝣 +U+8764: qiú # 蝤 +U+8765: máo # 蝥 +U+876E: fù # 蝮 +U+8770: kuí # 蝰 +U+8772: là # 蝲 +U+8774: hú # 蝴 +U+8776: dié # 蝶 +U+877B: nǎn # 蝻 +U+877C: lóu # 蝼 +U+877D: chūn # 蝽 +U+877E: róng # 蝾 +U+8782: láng # 螂 +U+8783: páng # 螃 +U+8785: xī # 螅 +U+8788: yuán # 螈 +U+878B: sōu # 螋 +U+878D: róng # 融 +U+8797: táng # 螗 +U+879F: míng # 螟 +U+87A0: yì # 螠 +U+87A3: tè # 螣 -> téng +U+87A8: mǎn # 螨 +U+87AB: shì # 螫 +U+87AC: cáo # 螬 +U+87AD: chī # 螭 +U+87AF: áo # 螯 +U+87B1: wèi # 螱 +U+87B3: táng # 螳 +U+87B5: piāo # 螵 +U+87BA: luó # 螺 +U+87BD: zhōng # 螽 +U+87C0: shuài # 蟀 +U+87C6: má # 蟆 +U+87CA: máo # 蟊 +U+87CB: xī # 蟋 +U+87CF: xiāo # 蟏 +U+87D1: zhāng # 蟑 +U+87D2: mǎng # 蟒 +U+87DB: péng # 蟛 +U+87E0: pán # 蟠 +U+87E5: huáng # 蟥 +U+87EA: huì # 蟪 +U+87EB: yín # 蟫 +U+87EE: shàn # 蟮 +U+87F9: xiè # 蟹 +U+87FE: chán # 蟾 +U+8803: luǒ # 蠃 +U+880A: lián # 蠊 +U+880B: zhú # 蠋 +U+8813: měng # 蠓 +U+8815: rú # 蠕 +U+8816: huò # 蠖 +U+8821: lí # 蠡 -> lǐ +U+8822: chǔn # 蠢 +U+8832: juān # 蠲 +U+8839: dù # 蠹 +U+883C: qú # 蠼 +U+8840: xuè # 血 -> xiě +U+8843: pēi # 衃 +U+8844: nǜ # 衄 +U+8845: xìn # 衅 +U+884C: xíng # 行 -> háng +U+884D: yǎn # 衍 +U+884E: kàn # 衎 +U+8852: xuàn # 衒 +U+8854: xián # 衔 +U+8857: jiē # 街 +U+8859: yá # 衙 +U+8860: zhūn # 衠 +U+8861: héng # 衡 +U+8862: qú # 衢 +U+8863: yī # 衣 +U+8865: bǔ # 补 +U+8868: biǎo # 表 +U+8869: chǎ # 衩 -> chà +U+886B: shān # 衫 +U+886C: chèn # 衬 +U+886E: gǔn # 衮 +U+8870: shuāi # 衰 +U+8872: nà # 衲 +U+8877: zhōng # 衷 +U+887D: rèn # 衽 +U+887E: qīn # 衾 +U+887F: jīn # 衿 +U+8881: yuán # 袁 +U+8882: mèi # 袂 +U+8884: ǎo # 袄 +U+8885: niǎo # 袅 +U+8886: huī # 袆 +U+8888: jiā # 袈 +U+888B: dài # 袋 +U+888D: páo # 袍 +U+8892: tǎn # 袒 +U+8896: xiù # 袖 +U+8897: zhěn # 袗 +U+889C: wà # 袜 +U+88A2: pàn # 袢 +U+88A4: mào # 袤 +U+88AA: qū # 袪 +U+88AB: bèi # 被 +U+88AD: xí # 袭 +U+88AF: bó # 袯 +U+88B1: fú # 袱 +U+88B7: jiá # 袷 +U+88BC: gē # 袼 +U+88C1: cái # 裁 +U+88C2: liè # 裂 +U+88C5: zhuāng # 装 +U+88C6: dāng # 裆 +U+88C8: kūn # 裈 +U+88C9: kèn # 裉 +U+88CE: chéng # 裎 +U+88D2: póu # 裒 +U+88D4: yì # 裔 +U+88D5: yù # 裕 +U+88D8: qiú # 裘 +U+88D9: qún # 裙 +U+88DB: yì # 裛 +U+88DF: shā # 裟 +U+88E2: lián # 裢 +U+88E3: liǎn # 裣 +U+88E4: kù # 裤 +U+88E5: jiǎn # 裥 +U+88E8: bì # 裨 +U+88F0: duō # 裰 +U+88F1: biǎo # 裱 +U+88F3: shang # 裳 -> cháng +U+88F4: péi # 裴 +U+88F8: luǒ # 裸 +U+88F9: guǒ # 裹 +U+88FC: tì # 裼 -> xī +U+88FE: jū # 裾 +U+8902: guà # 褂 +U+890A: biǎn # 褊 +U+8910: hè # 褐 +U+8912: bāo # 褒 +U+8913: bǎo # 褓 +U+8915: yú # 褕 +U+8919: bèi # 褙 +U+891A: chǔ # 褚 -> zhǔ +U+891B: lǚ # 褛 +U+891F: tā # 褟 +U+8921: dā # 褡 +U+8925: rù # 褥 +U+892A: tuì # 褪 +U+892B: chǐ # 褫 +U+892F: jiè # 褯 +U+8930: qiān # 褰 +U+8934: lán # 褴 +U+8936: zhě # 褶 +U+8941: qiǎng # 襁 +U+8944: xiāng # 襄 +U+8955: lán # 襕 +U+895A: suì # 襚 +U+895C: chān # 襜 +U+895E: bì # 襞 +U+895F: jīn # 襟 +U+8966: rú # 襦 +U+896B: shì # 襫 +U+897B: pàn # 襻 +U+897F: xī # 西 +U+8981: yào # 要 +U+8983: tán # 覃 +U+8986: fù # 覆 +U+89C1: jiàn # 见 +U+89C2: guān # 观 +U+89C3: yàn # 觃 +U+89C4: guī # 规 +U+89C5: mì # 觅 +U+89C6: shì # 视 +U+89C7: chān # 觇 +U+89C8: lǎn # 览 +U+89C9: jué # 觉 +U+89CA: jì # 觊 +U+89CB: xí # 觋 +U+89CC: dí # 觌 +U+89CE: yú # 觎 +U+89CF: gòu # 觏 +U+89D0: jìn # 觐 +U+89D1: qù # 觑 +U+89D2: jiǎo # 角 +U+89D6: jué # 觖 +U+89DA: gū # 觚 +U+89DC: zī # 觜 +U+89DE: shāng # 觞 +U+89DF: huà # 觟 +U+89E3: jiě # 解 +U+89E5: gōng # 觥 +U+89E6: chù # 触 +U+89EB: sù # 觫 +U+89ED: jī # 觭 +U+89EF: zhì # 觯 +U+89F1: bì # 觱 +U+89F3: hú # 觳 +U+89FF: xī # 觿 +U+8A00: yán # 言 +U+8A04: qiú # 訄 +U+8A07: hōng # 訇 +U+8A1A: yín # 訚 +U+8A3E: zī # 訾 +U+8A48: lì # 詈 +U+8A5F: zhé # 詟 +U+8A79: zhān # 詹 +U+8A89: yù # 誉 +U+8A8A: téng # 誊 +U+8A93: shì # 誓 +U+8B07: jiǎn # 謇 +U+8B66: jǐng # 警 +U+8B6C: pì # 譬 +U+8BA1: jì # 计 +U+8BA2: dìng # 订 +U+8BA3: fù # 讣 +U+8BA4: rèn # 认 +U+8BA5: jī # 讥 +U+8BA6: jié # 讦 +U+8BA7: hòng # 讧 +U+8BA8: tǎo # 讨 +U+8BA9: ràng # 让 +U+8BAA: shàn # 讪 +U+8BAB: qì # 讫 +U+8BAD: xùn # 训 +U+8BAE: yì # 议 +U+8BAF: xùn # 讯 +U+8BB0: jì # 记 +U+8BB1: rèn # 讱 +U+8BB2: jiǎng # 讲 +U+8BB3: huì # 讳 +U+8BB4: ōu # 讴 +U+8BB5: jù # 讵 +U+8BB6: yà # 讶 +U+8BB7: nè # 讷 +U+8BB8: xǔ # 许 +U+8BB9: é # 讹 +U+8BBA: lùn # 论 +U+8BBB: xiōng # 讻 +U+8BBC: sòng # 讼 +U+8BBD: fěng # 讽 +U+8BBE: shè # 设 +U+8BBF: fǎng # 访 +U+8BC0: jué # 诀 +U+8BC1: zhèng # 证 +U+8BC2: gǔ # 诂 +U+8BC3: hē # 诃 +U+8BC4: píng # 评 +U+8BC5: zǔ # 诅 +U+8BC6: shí # 识 +U+8BC7: xiòng # 诇 +U+8BC8: zhà # 诈 +U+8BC9: sù # 诉 +U+8BCA: zhěn # 诊 +U+8BCB: dǐ # 诋 +U+8BCC: zhōu # 诌 +U+8BCD: cí # 词 +U+8BCE: qū # 诎 +U+8BCF: zhào # 诏 +U+8BD0: bì # 诐 +U+8BD1: yì # 译 +U+8BD2: yí # 诒 +U+8BD3: kuāng # 诓 +U+8BD4: lěi # 诔 +U+8BD5: shì # 试 +U+8BD6: guà # 诖 +U+8BD7: shī # 诗 +U+8BD8: jí # 诘 -> jié +U+8BD9: huī # 诙 +U+8BDA: chéng # 诚 +U+8BDB: zhū # 诛 +U+8BDC: shēn # 诜 +U+8BDD: huà # 话 +U+8BDE: dàn # 诞 +U+8BDF: gòu # 诟 +U+8BE0: quán # 诠 +U+8BE1: guǐ # 诡 +U+8BE2: xún # 询 +U+8BE3: yì # 诣 +U+8BE4: zhèng # 诤 +U+8BE5: gāi # 该 +U+8BE6: xiáng # 详 +U+8BE7: chà # 诧 +U+8BE8: hùn # 诨 +U+8BE9: xǔ # 诩 +U+8BEB: jiè # 诫 +U+8BEC: wū # 诬 +U+8BED: yǔ # 语 +U+8BEE: qiào # 诮 +U+8BEF: wù # 误 +U+8BF0: gào # 诰 +U+8BF1: yòu # 诱 +U+8BF2: huì # 诲 +U+8BF3: kuáng # 诳 +U+8BF4: shuō # 说 +U+8BF5: sòng # 诵 +U+8BF7: qǐng # 请 +U+8BF8: zhū # 诸 +U+8BF9: zōu # 诹 +U+8BFA: nuò # 诺 +U+8BFB: dú # 读 +U+8BFC: zhuó # 诼 +U+8BFD: fěi # 诽 +U+8BFE: kè # 课 +U+8BFF: wěi # 诿 +U+8C00: yú # 谀 +U+8C01: shuí # 谁 +U+8C02: shěn # 谂 +U+8C03: diào # 调 -> tiáo +U+8C04: chǎn # 谄 +U+8C05: liàng # 谅 +U+8C06: zhūn # 谆 +U+8C07: suì # 谇 +U+8C08: tán # 谈 +U+8C0A: yì # 谊 +U+8C0B: móu # 谋 +U+8C0C: chén # 谌 +U+8C0D: dié # 谍 +U+8C0E: huǎng # 谎 +U+8C0F: jiàn # 谏 +U+8C10: xié # 谐 +U+8C11: xuè # 谑 +U+8C12: yè # 谒 +U+8C13: wèi # 谓 +U+8C14: è # 谔 +U+8C15: yù # 谕 +U+8C16: xuān # 谖 +U+8C17: chán # 谗 +U+8C19: ān # 谙 +U+8C1A: yàn # 谚 +U+8C1B: dì # 谛 +U+8C1C: mí # 谜 +U+8C1D: pián # 谝 -> piǎn +U+8C1E: xū # 谞 +U+8C1F: mó # 谟 +U+8C20: dǎng # 谠 +U+8C21: sù # 谡 +U+8C22: xiè # 谢 +U+8C23: yáo # 谣 +U+8C24: bàng # 谤 +U+8C25: shì # 谥 +U+8C26: qiān # 谦 +U+8C27: mì # 谧 +U+8C28: jǐn # 谨 +U+8C29: mán # 谩 +U+8C2A: zhé # 谪 +U+8C2B: jiǎn # 谫 +U+8C2C: miù # 谬 +U+8C2D: tán # 谭 +U+8C2E: zèn # 谮 +U+8C2F: qiáo # 谯 +U+8C30: lán # 谰 +U+8C31: pǔ # 谱 +U+8C32: jué # 谲 +U+8C33: yàn # 谳 +U+8C34: qiǎn # 谴 +U+8C35: zhān # 谵 +U+8C36: chèn # 谶 +U+8C37: gǔ # 谷 +U+8C3C: hóng # 谼 +U+8C3F: xī # 谿 +U+8C41: huō # 豁 +U+8C46: dòu # 豆 +U+8C47: jiāng # 豇 +U+8C49: shì # 豉 -> chǐ +U+8C4C: wān # 豌 +U+8C55: shǐ # 豕 +U+8C5A: tún # 豚 +U+8C61: xiàng # 象 +U+8C62: huàn # 豢 +U+8C68: xī # 豨 +U+8C6A: háo # 豪 +U+8C6B: yù # 豫 +U+8C6E: fén # 豮 +U+8C73: bīn # 豳 +U+8C78: zhì # 豸 +U+8C79: bào # 豹 +U+8C7A: chái # 豺 +U+8C82: diāo # 貂 +U+8C85: xiū # 貅 +U+8C86: huán # 貆 +U+8C89: háo # 貉 -> hé +U+8C8A: mò # 貊 +U+8C8C: mào # 貌 +U+8C94: pí # 貔 +U+8C98: mò # 貘 +U+8D1D: bèi # 贝 +U+8D1E: zhēn # 贞 +U+8D1F: fù # 负 +U+8D21: gòng # 贡 +U+8D22: cái # 财 +U+8D23: zé # 责 +U+8D24: xián # 贤 +U+8D25: bài # 败 +U+8D26: zhàng # 账 +U+8D27: huò # 货 +U+8D28: zhì # 质 +U+8D29: fàn # 贩 +U+8D2A: tān # 贪 +U+8D2B: pín # 贫 +U+8D2C: biǎn # 贬 +U+8D2D: gòu # 购 +U+8D2E: zhù # 贮 +U+8D2F: guàn # 贯 +U+8D30: èr # 贰 +U+8D31: jiàn # 贱 +U+8D32: bēn # 贲 -> bì +U+8D33: shì # 贳 +U+8D34: tiē # 贴 +U+8D35: guì # 贵 +U+8D36: kuàng # 贶 +U+8D37: dài # 贷 +U+8D38: mào # 贸 +U+8D39: fèi # 费 +U+8D3A: hè # 贺 +U+8D3B: yí # 贻 +U+8D3C: zéi # 贼 +U+8D3D: zhì # 贽 +U+8D3E: jiǎ # 贾 -> gǔ +U+8D3F: huì # 贿 +U+8D40: zī # 赀 +U+8D41: lìn # 赁 +U+8D42: lù # 赂 +U+8D43: zāng # 赃 +U+8D44: zī # 资 +U+8D45: gāi # 赅 +U+8D46: jìn # 赆 +U+8D47: qiú # 赇 +U+8D48: zhèn # 赈 +U+8D49: lài # 赉 +U+8D4A: shē # 赊 +U+8D4B: fù # 赋 +U+8D4C: dǔ # 赌 +U+8D4D: jī # 赍 +U+8D4E: shú # 赎 +U+8D4F: shǎng # 赏 +U+8D50: cì # 赐 +U+8D51: bì # 赑 +U+8D52: zhōu # 赒 +U+8D53: gēng # 赓 +U+8D54: péi # 赔 +U+8D55: dǎn # 赕 +U+8D56: lài # 赖 +U+8D57: fèng # 赗 +U+8D58: zhuì # 赘 +U+8D59: fù # 赙 +U+8D5A: zhuàn # 赚 +U+8D5B: sài # 赛 +U+8D5C: zé # 赜 +U+8D5D: yàn # 赝 +U+8D5E: zàn # 赞 +U+8D5F: yūn # 赟 +U+8D60: zèng # 赠 +U+8D61: shàn # 赡 +U+8D62: yíng # 赢 +U+8D63: gàn # 赣 +U+8D64: chì # 赤 +U+8D66: shè # 赦 +U+8D67: nǎn # 赧 +U+8D6A: chēng # 赪 +U+8D6B: hè # 赫 +U+8D6D: zhě # 赭 +U+8D70: zǒu # 走 +U+8D73: jiū # 赳 +U+8D74: fù # 赴 +U+8D75: zhào # 赵 +U+8D76: gǎn # 赶 +U+8D77: qǐ # 起 +U+8D81: chèn # 趁 +U+8D84: jū # 趄 -> qiè +U+8D85: chāo # 超 +U+8D8A: yuè # 越 +U+8D8B: qū # 趋 +U+8D91: zī # 趑 +U+8D94: liè # 趔 +U+8D9F: tàng # 趟 +U+8DA3: qù # 趣 +U+8DAF: tì # 趯 +U+8DB1: zǎn # 趱 +U+8DB3: zú # 足 +U+8DB4: pā # 趴 +U+8DB5: bào # 趵 +U+8DB8: dǔn # 趸 +U+8DBA: fū # 趺 +U+8DBC: jiǎn # 趼 +U+8DBE: zhǐ # 趾 +U+8DBF: tā # 趿 +U+8DC2: qí # 跂 +U+8DC3: yuè # 跃 +U+8DC4: qiāng # 跄 +U+8DC6: tái # 跆 +U+8DCB: bá # 跋 +U+8DCC: diē # 跌 +U+8DCE: tuó # 跎 +U+8DCF: jiā # 跏 +U+8DD0: cī # 跐 +U+8DD1: pǎo # 跑 +U+8DD6: zhí # 跖 +U+8DD7: fū # 跗 +U+8DDA: shān # 跚 +U+8DDB: bǒ # 跛 +U+8DDD: jù # 距 +U+8DDE: lì # 跞 +U+8DDF: gēn # 跟 +U+8DE3: xiǎn # 跣 +U+8DE4: jiāo # 跤 +U+8DE8: kuà # 跨 +U+8DEA: guì # 跪 +U+8DEC: kuǐ # 跬 +U+8DEF: lù # 路 +U+8DF1: zhì # 跱 +U+8DF3: tiào # 跳 +U+8DF5: jiàn # 践 +U+8DF6: dá # 跶 +U+8DF7: qiāo # 跷 +U+8DF8: bì # 跸 +U+8DF9: xiān # 跹 +U+8DFA: duò # 跺 +U+8DFB: jī # 跻 +U+8DFD: jì # 跽 +U+8E05: xué # 踅 +U+8E09: liáng # 踉 +U+8E0A: yǒng # 踊 +U+8E0C: chóu # 踌 +U+8E0F: tà # 踏 +U+8E12: wō # 踒 +U+8E14: chuō # 踔 +U+8E1D: huái # 踝 +U+8E1E: jù # 踞 +U+8E1F: chí # 踟 +U+8E22: tī # 踢 +U+8E23: bó # 踣 +U+8E26: yǐ # 踦 -> qī +U+8E29: cǎi # 踩 +U+8E2A: zōng # 踪 +U+8E2C: zhì # 踬 +U+8E2E: diǎn # 踮 +U+8E2F: zhí # 踯 +U+8E31: duó # 踱 +U+8E35: zhǒng # 踵 +U+8E36: dì # 踶 +U+8E39: chuài # 踹 +U+8E3A: jiàn # 踺 +U+8E3D: jǔ # 踽 +U+8E40: dié # 蹀 +U+8E41: pián # 蹁 +U+8E42: róu # 蹂 +U+8E44: tí # 蹄 +U+8E45: chǎ # 蹅 +U+8E47: jiǎn # 蹇 +U+8E48: dǎo # 蹈 +U+8E49: cuō # 蹉 +U+8E4A: qī # 蹊 +U+8E4B: tà # 蹋 +U+8E50: jí # 蹐 +U+8E51: niè # 蹑 +U+8E52: pán # 蹒 +U+8E59: cù # 蹙 +U+8E5A: tāng # 蹚 +U+8E5C: sù # 蹜 +U+8E62: dí # 蹢 +U+8E66: bèng # 蹦 +U+8E69: bié # 蹩 +U+8E6C: dēng # 蹬 +U+8E6D: cèng # 蹭 +U+8E6F: fán # 蹯 +U+8E70: chú # 蹰 +U+8E72: dūn # 蹲 +U+8E74: cù # 蹴 +U+8E76: jué # 蹶 +U+8E7C: pǔ # 蹼 +U+8E7D: liāo # 蹽 +U+8E7E: dūn # 蹾 +U+8E7F: cuān # 蹿 +U+8E81: zào # 躁 +U+8E85: zhú # 躅 +U+8E87: chú # 躇 +U+8E8F: lìn # 躏 +U+8E90: liè # 躐 +U+8E94: chán # 躔 +U+8E9C: zuān # 躜 +U+8E9E: xiè # 躞 +U+8EAB: shēn # 身 +U+8EAC: gōng # 躬 +U+8EAF: qū # 躯 +U+8EB2: duǒ # 躲 +U+8EBA: tǎng # 躺 +U+8F66: chē # 车 +U+8F67: yà # 轧 +U+8F68: guǐ # 轨 +U+8F69: xuān # 轩 +U+8F6A: dài # 轪 +U+8F6B: rèn # 轫 +U+8F6C: zhuǎn # 转 +U+8F6D: è # 轭 +U+8F6E: lún # 轮 +U+8F6F: ruǎn # 软 +U+8F70: hōng # 轰 +U+8F71: gū # 轱 +U+8F72: kē # 轲 +U+8F73: lú # 轳 +U+8F74: zhóu # 轴 +U+8F75: zhǐ # 轵 +U+8F76: yì # 轶 +U+8F77: hū # 轷 +U+8F78: zhěn # 轸 +U+8F79: lì # 轹 +U+8F7A: yáo # 轺 +U+8F7B: qīng # 轻 +U+8F7C: shì # 轼 +U+8F7D: zài # 载 -> zǎi +U+8F7E: zhì # 轾 +U+8F7F: jiào # 轿 +U+8F80: zhōu # 辀 +U+8F81: quán # 辁 +U+8F82: lù # 辂 +U+8F83: jiào # 较 +U+8F84: zhé # 辄 +U+8F85: fǔ # 辅 +U+8F86: liàng # 辆 +U+8F87: niǎn # 辇 +U+8F88: bèi # 辈 +U+8F89: huī # 辉 +U+8F8A: gǔn # 辊 +U+8F8B: wǎng # 辋 +U+8F8C: liáng # 辌 +U+8F8D: chuò # 辍 +U+8F8E: zī # 辎 +U+8F8F: còu # 辏 +U+8F90: fú # 辐 +U+8F91: jí # 辑 +U+8F92: wēn # 辒 +U+8F93: shū # 输 +U+8F94: pèi # 辔 +U+8F95: yuán # 辕 +U+8F96: xiá # 辖 +U+8F97: niǎn # 辗 -> zhǎn +U+8F98: lù # 辘 +U+8F99: zhé # 辙 +U+8F9A: lín # 辚 +U+8F9B: xīn # 辛 +U+8F9C: gū # 辜 +U+8F9E: cí # 辞 +U+8F9F: pì # 辟 -> bì +U+8FA3: là # 辣 +U+8FA8: biàn # 辨 +U+8FA9: biàn # 辩 +U+8FAB: biàn # 辫 +U+8FB0: chén # 辰 +U+8FB1: rǔ # 辱 +U+8FB9: biān # 边 +U+8FBD: liáo # 辽 +U+8FBE: dá # 达 +U+8FBF: chān # 辿 +U+8FC1: qiān # 迁 +U+8FC2: yū # 迂 +U+8FC4: qì # 迄 +U+8FC5: xùn # 迅 +U+8FC7: guò # 过 +U+8FC8: mài # 迈 +U+8FCE: yíng # 迎 +U+8FD0: yùn # 运 +U+8FD1: jìn # 近 +U+8FD3: yà # 迓 +U+8FD4: fǎn # 返 +U+8FD5: wù # 迕 -> wǔ +U+8FD8: hái # 还 +U+8FD9: zhè # 这 +U+8FDB: jìn # 进 +U+8FDC: yuǎn # 远 +U+8FDD: wéi # 违 +U+8FDE: lián # 连 +U+8FDF: chí # 迟 +U+8FE2: tiáo # 迢 +U+8FE4: yí # 迤 -> yǐ +U+8FE5: jiǒng # 迥 +U+8FE6: jiā # 迦 +U+8FE8: dài # 迨 +U+8FE9: ěr # 迩 +U+8FEA: dí # 迪 +U+8FEB: pò # 迫 +U+8FED: dié # 迭 +U+8FEE: zé # 迮 +U+8FF0: shù # 述 +U+8FF3: jìng # 迳 +U+8FF7: mí # 迷 +U+8FF8: bèng # 迸 +U+8FF9: jì # 迹 +U+8FFA: nǎi # 迺 +U+8FFD: zhuī # 追 +U+9000: tuì # 退 +U+9001: sòng # 送 +U+9002: shì # 适 +U+9003: táo # 逃 +U+9004: páng # 逄 +U+9005: hòu # 逅 +U+9006: nì # 逆 +U+9009: xuǎn # 选 +U+900A: xùn # 逊 +U+900B: bū # 逋 +U+900D: xiāo # 逍 +U+900F: tòu # 透 +U+9010: zhú # 逐 +U+9011: qiú # 逑 +U+9012: dì # 递 +U+9014: tú # 途 +U+9016: tì # 逖 +U+9017: dòu # 逗 +U+901A: tōng # 通 +U+901B: guàng # 逛 +U+901D: shì # 逝 +U+901E: chěng # 逞 +U+901F: sù # 速 +U+9020: zào # 造 +U+9021: qūn # 逡 +U+9022: féng # 逢 +U+9026: lǐ # 逦 +U+902D: huàn # 逭 +U+902E: dǎi # 逮 -> dài +U+902F: lù # 逯 +U+9034: chuō # 逴 +U+9035: kuí # 逵 +U+9036: wēi # 逶 +U+9038: yì # 逸 +U+903B: luó # 逻 +U+903C: bī # 逼 +U+903E: yú # 逾 +U+9041: dùn # 遁 +U+9042: suì # 遂 +U+9044: chuán # 遄 +U+9046: tí # 遆 -> dì +U+9047: yù # 遇 +U+904D: biàn # 遍 +U+904F: è # 遏 +U+9050: xiá # 遐 +U+9051: huáng # 遑 +U+9052: qiú # 遒 +U+9053: dào # 道 +U+9057: yí # 遗 +U+9058: gòu # 遘 +U+905B: liú # 遛 -> liù +U+9062: tà # 遢 +U+9063: qiǎn # 遣 +U+9065: yáo # 遥 +U+9068: áo # 遨 +U+906D: zāo # 遭 +U+906E: zhē # 遮 +U+9074: lín # 遴 +U+9075: zūn # 遵 +U+9079: yù # 遹 +U+907D: jù # 遽 +U+907F: bì # 避 +U+9080: yāo # 邀 +U+9082: xiè # 邂 +U+9083: suì # 邃 +U+9088: miǎo # 邈 +U+908B: lā # 邋 +U+9091: yì # 邑 +U+9093: dèng # 邓 +U+9095: yōng # 邕 +U+9097: hán # 邗 +U+9098: yú # 邘 +U+9099: máng # 邙 +U+909B: qióng # 邛 +U+909D: kuàng # 邝 +U+90A0: bīn # 邠 +U+90A1: fāng # 邡 +U+90A2: xíng # 邢 +U+90A3: nà # 那 +U+90A6: bāng # 邦 +U+90A8: cūn # 邨 +U+90AA: xié # 邪 +U+90AC: wū # 邬 +U+90AE: yóu # 邮 +U+90AF: hán # 邯 +U+90B0: tái # 邰 +U+90B1: qiū # 邱 +U+90B2: bì # 邲 +U+90B3: pī # 邳 +U+90B4: bǐng # 邴 +U+90B5: shào # 邵 +U+90B6: bèi # 邶 +U+90B8: dǐ # 邸 +U+90B9: zōu # 邹 +U+90BA: yè # 邺 +U+90BB: lín # 邻 +U+90BD: guī # 邽 +U+90BE: zhū # 邾 +U+90BF: shī # 邿 +U+90C1: yù # 郁 +U+90C3: hé # 郃 +U+90C4: qiè # 郄 +U+90C5: zhì # 郅 +U+90C7: huán # 郇 -> xún +U+90C8: hòu # 郈 +U+90CA: jiāo # 郊 +U+90CE: láng # 郎 +U+90CF: jiá # 郏 +U+90D0: kuài # 郐 +U+90D1: zhèng # 郑 +U+90D3: yùn # 郓 +U+90D7: xī # 郗 +U+90DA: wú # 郚 +U+90DB: fú # 郛 +U+90DC: gào # 郜 +U+90DD: hǎo # 郝 +U+90E1: jùn # 郡 +U+90E2: yǐng # 郢 +U+90E4: xì # 郤 +U+90E6: lì # 郦 +U+90E7: yún # 郧 +U+90E8: bù # 部 +U+90EA: qī # 郪 +U+90EB: pí # 郫 +U+90ED: guō # 郭 +U+90EF: tán # 郯 +U+90F4: chēn # 郴 +U+90F8: dān # 郸 +U+90FD: dōu # 都 +U+90FE: yǎn # 郾 +U+90FF: méi # 郿 +U+9100: ruò # 鄀 +U+9102: è # 鄂 +U+9103: shū # 鄃 +U+9104: juàn # 鄄 +U+9105: yǔ # 鄅 +U+910C: táng # 鄌 +U+9111: zī # 鄑 +U+9117: hào # 鄗 +U+9118: yōng # 鄘 +U+9119: bǐ # 鄙 +U+911A: mào # 鄚 +U+911C: fū # 鄜 +U+911E: yín # 鄞 +U+9120: hù # 鄠 +U+9122: yān # 鄢 +U+9123: zhāng # 鄣 +U+912B: zēng # 鄫 +U+912F: shàn # 鄯 +U+9131: pó # 鄱 +U+9139: zōu # 鄹 +U+9142: cuó # 酂 +U+9143: líng # 酃 +U+9145: xī # 酅 +U+9146: fēng # 酆 +U+9149: yǒu # 酉 +U+914A: dīng # 酊 +U+914B: qiú # 酋 +U+914C: zhuó # 酌 +U+914D: pèi # 配 +U+914E: zhòu # 酎 +U+914F: yǐ # 酏 +U+9150: gān # 酐 +U+9152: jiǔ # 酒 +U+9157: xù # 酗 +U+915A: fēn # 酚 +U+915D: yùn # 酝 +U+915E: tài # 酞 +U+9161: tuó # 酡 +U+9162: cù # 酢 -> zuò +U+9163: hān # 酣 +U+9164: gū # 酤 +U+9165: sū # 酥 +U+9166: pò # 酦 -> pō +U+9169: mǐng # 酩 +U+916A: lào # 酪 +U+916C: chóu # 酬 +U+916E: tóng # 酮 +U+916F: zhǐ # 酯 +U+9170: xiān # 酰 +U+9171: jiàng # 酱 +U+9172: chéng # 酲 +U+9174: tú # 酴 +U+9175: jiào # 酵 +U+9176: méi # 酶 +U+9177: kù # 酷 +U+9178: suān # 酸 +U+9179: lèi # 酹 +U+917A: pú # 酺 +U+917D: yàn # 酽 +U+917E: shāi # 酾 -> shī +U+917F: niàng # 酿 +U+9185: pēi # 醅 +U+9187: chún # 醇 +U+9189: zuì # 醉 +U+918B: cù # 醋 +U+918C: kūn # 醌 +U+918D: tí # 醍 +U+9190: hú # 醐 +U+9191: xǔ # 醑 +U+9192: xǐng # 醒 +U+919A: mí # 醚 +U+919B: quán # 醛 +U+91A2: hǎi # 醢 +U+91A8: lí # 醨 +U+91AA: láo # 醪 +U+91AD: bú # 醭 +U+91AE: jiào # 醮 +U+91AF: xī # 醯 +U+91B4: lǐ # 醴 +U+91B5: jù # 醵 +U+91BA: xūn # 醺 +U+91BE: mí # 醾 +U+91C7: cǎi # 采 +U+91C9: yòu # 釉 +U+91CA: shì # 释 +U+91CC: lǐ # 里 +U+91CD: zhòng # 重 +U+91CE: yě # 野 +U+91CF: liàng # 量 -> liáng +U+91D0: lí # 釐 +U+91D1: jīn # 金 +U+91DC: fǔ # 釜 +U+9274: jiàn # 鉴 +U+928E: qióng # 銎 +U+92AE: luán # 銮 +U+92C6: yún # 鋆 +U+92C8: wù # 鋈 +U+933E: zàn # 錾 +U+936A: móu # 鍪 +U+938F: liú # 鎏 +U+93CA: ào # 鏊 +U+93D6: áo # 鏖 +U+943E: bèi # 鐾 +U+946B: xīn # 鑫 +U+9486: gá # 钆 +U+9487: yǐ # 钇 +U+9488: zhēn # 针 +U+9489: dīng # 钉 +U+948A: zhāo # 钊 +U+948B: pō # 钋 +U+948C: liǎo # 钌 +U+948D: tǔ # 钍 +U+948E: qiān # 钎 +U+948F: chuàn # 钏 +U+9490: shān # 钐 +U+9492: fán # 钒 +U+9493: diào # 钓 +U+9494: mén # 钔 +U+9495: nǚ # 钕 +U+9496: yáng # 钖 +U+9497: chāi # 钗 +U+9498: xíng # 钘 +U+9499: gài # 钙 +U+949A: bù # 钚 +U+949B: tài # 钛 +U+949C: jù # 钜 +U+949D: dùn # 钝 +U+949E: chāo # 钞 +U+949F: zhōng # 钟 +U+94A0: nà # 钠 +U+94A1: bèi # 钡 +U+94A2: gāng # 钢 +U+94A3: bǎn # 钣 +U+94A4: qián # 钤 +U+94A5: yào # 钥 -> yuè +U+94A6: qīn # 钦 +U+94A7: jūn # 钧 +U+94A8: wū # 钨 +U+94A9: gōu # 钩 +U+94AA: kàng # 钪 +U+94AB: fāng # 钫 +U+94AC: huǒ # 钬 +U+94AD: tǒu # 钭 -> dǒu +U+94AE: niǔ # 钮 +U+94AF: bǎ # 钯 +U+94B0: yù # 钰 +U+94B1: qián # 钱 +U+94B2: zhēng # 钲 +U+94B3: qián # 钳 +U+94B4: gǔ # 钴 +U+94B5: bō # 钵 +U+94B7: pǒ # 钷 +U+94B9: bó # 钹 +U+94BA: yuè # 钺 +U+94BB: zuān # 钻 +U+94BC: mù # 钼 +U+94BD: tǎn # 钽 +U+94BE: jiǎ # 钾 +U+94BF: diàn # 钿 +U+94C0: yóu # 铀 +U+94C1: tiě # 铁 +U+94C2: bó # 铂 +U+94C3: líng # 铃 +U+94C4: shuò # 铄 +U+94C5: qiān # 铅 +U+94C6: mǎo # 铆 +U+94C8: shì # 铈 +U+94C9: xuàn # 铉 +U+94CA: tā # 铊 +U+94CB: bì # 铋 +U+94CC: ní # 铌 +U+94CD: pī # 铍 -> pí +U+94CE: duó # 铎 +U+94CF: xíng # 铏 +U+94D0: kào # 铐 +U+94D1: lǎo # 铑 +U+94D2: ěr # 铒 +U+94D5: yǒu # 铕 +U+94D6: chéng # 铖 +U+94D7: jiá # 铗 +U+94D8: yé # 铘 +U+94D9: náo # 铙 +U+94DA: zhì # 铚 +U+94DB: dāng # 铛 +U+94DC: tóng # 铜 +U+94DD: lǚ # 铝 +U+94DE: diào # 铞 +U+94DF: yīn # 铟 +U+94E0: kǎi # 铠 +U+94E1: zhá # 铡 +U+94E2: zhū # 铢 +U+94E3: xǐ # 铣 -> xiǎn +U+94E4: dìng # 铤 -> tǐng +U+94E5: diū # 铥 +U+94E7: huá # 铧 +U+94E8: quán # 铨 +U+94E9: shā # 铩 +U+94EA: hā # 铪 +U+94EB: diào # 铫 +U+94EC: gè # 铬 +U+94ED: míng # 铭 +U+94EE: zhēng # 铮 +U+94EF: sè # 铯 +U+94F0: jiǎo # 铰 +U+94F1: yī # 铱 +U+94F2: chǎn # 铲 +U+94F3: chòng # 铳 +U+94F4: tāng # 铴 -> tàng +U+94F5: ǎn # 铵 +U+94F6: yín # 银 +U+94F7: rú # 铷 +U+94F8: zhù # 铸 +U+94F9: láo # 铹 +U+94FA: pù # 铺 -> pū +U+94FB: wú # 铻 +U+94FC: lái # 铼 +U+94FD: tè # 铽 +U+94FE: liàn # 链 +U+94FF: kēng # 铿 +U+9500: xiāo # 销 +U+9501: suǒ # 锁 +U+9502: lǐ # 锂 +U+9503: zèng # 锃 +U+9504: chú # 锄 +U+9505: guō # 锅 +U+9506: gào # 锆 +U+9507: é # 锇 +U+9508: xiù # 锈 +U+9509: cuò # 锉 +U+950A: lüè # 锊 +U+950B: fēng # 锋 +U+950C: xīn # 锌 +U+950D: liǔ # 锍 +U+950E: kāi # 锎 +U+950F: jiǎn # 锏 +U+9510: ruì # 锐 +U+9511: tī # 锑 +U+9512: láng # 锒 +U+9513: qǐn # 锓 +U+9514: jū # 锔 +U+9515: ā # 锕 +U+9516: qiāng # 锖 +U+9517: zhě # 锗 +U+9518: nuò # 锘 +U+9519: cuò # 错 +U+951A: máo # 锚 +U+951B: bēn # 锛 +U+951C: qí # 锜 +U+951D: dé # 锝 +U+951E: kè # 锞 +U+951F: kūn # 锟 +U+9521: xī # 锡 +U+9522: gù # 锢 +U+9523: luó # 锣 +U+9524: chuí # 锤 +U+9525: zhuī # 锥 +U+9526: jǐn # 锦 +U+9527: zhì # 锧 +U+9528: xiān # 锨 +U+9529: juǎn # 锩 +U+952A: huō # 锪 -> huò +U+952B: péi # 锫 +U+952C: tán # 锬 +U+952D: dìng # 锭 +U+952E: jiàn # 键 +U+952F: jù # 锯 +U+9530: měng # 锰 +U+9531: zī # 锱 +U+9532: qiè # 锲 +U+9533: yīng # 锳 +U+9534: kǎi # 锴 +U+9535: qiāng # 锵 +U+9536: sī # 锶 +U+9537: è # 锷 +U+9538: chā # 锸 +U+9539: qiāo # 锹 +U+953A: zhōng # 锺 +U+953B: duàn # 锻 +U+953C: sōu # 锼 +U+953D: huáng # 锽 +U+953E: huán # 锾 +U+953F: āi # 锿 +U+9540: dù # 镀 +U+9541: měi # 镁 +U+9542: lòu # 镂 +U+9543: zī # 镃 +U+9544: fèi # 镄 +U+9545: méi # 镅 +U+9546: mò # 镆 +U+9547: zhèn # 镇 +U+9548: bó # 镈 +U+9549: gé # 镉 +U+954A: niè # 镊 +U+954B: tǎng # 镋 +U+954C: juān # 镌 +U+954D: niè # 镍 +U+954E: ná # 镎 +U+954F: liú # 镏 +U+9550: gǎo # 镐 +U+9551: bàng # 镑 +U+9552: yì # 镒 +U+9553: jiā # 镓 +U+9554: bīn # 镔 +U+9555: róng # 镕 +U+9556: biāo # 镖 +U+9557: tāng # 镗 +U+9558: màn # 镘 +U+955A: bèng # 镚 +U+955B: yōng # 镛 +U+955C: jìng # 镜 +U+955D: dī # 镝 -> dí +U+955E: zú # 镞 +U+9560: liú # 镠 +U+9561: chán # 镡 -> xín +U+9562: jué # 镢 +U+9563: liào # 镣 +U+9564: pú # 镤 +U+9565: lǔ # 镥 +U+9566: duì # 镦 -> duī +U+9567: lán # 镧 +U+9568: pǔ # 镨 +U+9569: cuān # 镩 +U+956A: qiāng # 镪 -> qiǎng +U+956B: dèng # 镫 +U+956C: huò # 镬 +U+956D: léi # 镭 +U+956E: huán # 镮 +U+956F: zhuó # 镯 +U+9570: lián # 镰 +U+9571: yì # 镱 +U+9572: chǎ # 镲 +U+9573: biāo # 镳 +U+9574: là # 镴 +U+9575: chán # 镵 +U+9576: xiāng # 镶 +U+957F: zhǎng # 长 -> cháng +U+95E8: mén # 门 +U+95E9: shuān # 闩 +U+95EA: shǎn # 闪 +U+95EB: yán # 闫 +U+95ED: bì # 闭 +U+95EE: wèn # 问 +U+95EF: chuǎng # 闯 +U+95F0: rùn # 闰 +U+95F1: wéi # 闱 +U+95F2: xián # 闲 +U+95F3: hóng # 闳 +U+95F4: jiān # 间 +U+95F5: mǐn # 闵 +U+95F6: kāng # 闶 -> kàng +U+95F7: mèn # 闷 +U+95F8: zhá # 闸 +U+95F9: nào # 闹 +U+95FA: guī # 闺 +U+95FB: wén # 闻 +U+95FC: tà # 闼 +U+95FD: mǐn # 闽 +U+95FE: lǘ # 闾 +U+95FF: kǎi # 闿 +U+9600: fá # 阀 +U+9601: gé # 阁 +U+9602: hé # 阂 +U+9603: kǔn # 阃 +U+9604: jiū # 阄 +U+9605: yuè # 阅 +U+9606: láng # 阆 -> làng +U+9607: dū # 阇 +U+9608: yù # 阈 +U+9609: yān # 阉 +U+960A: chāng # 阊 +U+960B: xì # 阋 +U+960C: wén # 阌 +U+960D: hūn # 阍 +U+960E: yán # 阎 +U+960F: è # 阏 +U+9610: chǎn # 阐 +U+9611: lán # 阑 +U+9612: qù # 阒 +U+9614: kuò # 阔 +U+9615: què # 阕 +U+9616: hé # 阖 +U+9617: tián # 阗 +U+9618: dá # 阘 -> tà +U+9619: quē # 阙 +U+961A: hǎn # 阚 -> kàn +U+961C: fù # 阜 +U+961F: duì # 队 +U+9621: qiān # 阡 +U+962A: bǎn # 阪 +U+962E: ruǎn # 阮 +U+9631: jǐng # 阱 +U+9632: fáng # 防 +U+9633: yáng # 阳 +U+9634: yīn # 阴 +U+9635: zhèn # 阵 +U+9636: jiē # 阶 +U+963B: zǔ # 阻 +U+963C: zuò # 阼 +U+963D: diàn # 阽 +U+963F: ā # 阿 +U+9640: tuó # 陀 +U+9642: bēi # 陂 +U+9644: fù # 附 +U+9645: jì # 际 +U+9646: lù # 陆 +U+9647: lǒng # 陇 +U+9648: chén # 陈 +U+9649: xíng # 陉 +U+964B: lòu # 陋 +U+964C: mò # 陌 +U+964D: jiàng # 降 +U+964E: shū # 陎 +U+9650: xiàn # 限 +U+9651: ér # 陑 +U+9654: gāi # 陔 +U+9655: shǎn # 陕 +U+965B: bì # 陛 +U+965E: shēng # 陞 +U+965F: zhì # 陟 +U+9661: dǒu # 陡 +U+9662: yuàn # 院 +U+9664: chú # 除 +U+9667: niè # 陧 +U+9668: yǔn # 陨 +U+9669: xiǎn # 险 +U+966A: péi # 陪 +U+966C: zōu # 陬 +U+9672: chuí # 陲 +U+9674: pí # 陴 -> pī +U+9675: líng # 陵 +U+9676: táo # 陶 +U+9677: xiàn # 陷 +U+9683: shù # 隃 -> yú +U+9685: yú # 隅 +U+9686: lóng # 隆 +U+9688: wēi # 隈 +U+968B: suí # 隋 +U+968D: huáng # 隍 +U+968F: suí # 随 +U+9690: yǐn # 隐 +U+9694: gé # 隔 +U+9697: kuí # 隗 -> wěi +U+9698: ài # 隘 +U+9699: xì # 隙 +U+969C: zhàng # 障 +U+96A7: suì # 隧 +U+96A9: ào # 隩 +U+96B0: xí # 隰 +U+96B3: huī # 隳 +U+96B6: lì # 隶 +U+96B9: zhuī # 隹 +U+96BA: hú # 隺 +U+96BC: sǔn # 隼 +U+96BD: juàn # 隽 -> jùn +U+96BE: nán # 难 +U+96C0: què # 雀 +U+96C1: yàn # 雁 +U+96C4: xióng # 雄 +U+96C5: yǎ # 雅 +U+96C6: jí # 集 +U+96C7: gù # 雇 +U+96C9: zhì # 雉 +U+96CA: gòu # 雊 +U+96CC: cí # 雌 +U+96CD: yōng # 雍 +U+96CE: jū # 雎 +U+96CF: chú # 雏 +U+96D2: luò # 雒 +U+96D5: diāo # 雕 +U+96E0: chóu # 雠 +U+96E8: yǔ # 雨 +U+96E9: yú # 雩 +U+96EA: xuě # 雪 +U+96EF: wén # 雯 +U+96F1: pāng # 雱 +U+96F3: lì # 雳 +U+96F6: líng # 零 +U+96F7: léi # 雷 +U+96F9: báo # 雹 +U+96FE: wù # 雾 +U+9700: xū # 需 +U+9701: jì # 霁 +U+9704: xiāo # 霄 +U+9705: zhà # 霅 -> zhá +U+9706: tíng # 霆 +U+9707: zhèn # 震 +U+9708: pèi # 霈 +U+9709: méi # 霉 +U+970D: huò # 霍 +U+970E: shà # 霎 +U+970F: fēi # 霏 +U+9713: ní # 霓 +U+9716: lín # 霖 +U+971C: shuāng # 霜 +U+971E: xiá # 霞 +U+9728: wèi # 霨 +U+972A: yín # 霪 +U+972D: ǎi # 霭 +U+9730: xiàn # 霰 +U+9732: lù # 露 -> lòu +U+9738: bà # 霸 +U+9739: pī # 霹 +U+973E: mái # 霾 +U+9752: qīng # 青 +U+9753: jìng # 靓 +U+9756: jìng # 靖 +U+9759: jìng # 静 +U+975B: diàn # 靛 +U+975E: fēi # 非 +U+9760: kào # 靠 +U+9761: mí # 靡 +U+9762: miàn # 面 +U+9765: yè # 靥 +U+9769: gé # 革 +U+976C: qián # 靬 -> jiān +U+9770: wù # 靰 +U+9773: jìn # 靳 +U+9774: xuē # 靴 +U+9776: bǎ # 靶 +U+9778: sǎ # 靸 +U+977A: mò # 靺 +U+977C: dá # 靼 +U+977D: bàn # 靽 +U+977F: yào # 靿 +U+9781: bèi # 鞁 +U+9785: yāng # 鞅 +U+978B: xié # 鞋 +U+978D: ān # 鞍 +U+9791: dá # 鞑 +U+9792: qiáo # 鞒 +U+9794: mán # 鞔 +U+9798: qiào # 鞘 +U+97A0: jū # 鞠 +U+97A1: la # 鞡 +U+97A3: róu # 鞣 +U+97A7: qiū # 鞧 +U+97A8: hé # 鞨 +U+97AB: jū # 鞫 +U+97AC: jiān # 鞬 -> jiàn +U+97AD: biān # 鞭 +U+97AE: dī # 鞮 +U+97AF: jiān # 鞯 +U+97B2: gōu # 鞲 +U+97B3: tà # 鞳 +U+97B4: bèi # 鞴 +U+97C2: chàn # 韂 +U+97E6: wéi # 韦 +U+97E7: rèn # 韧 +U+97E8: fú # 韨 +U+97E9: hán # 韩 +U+97EA: wěi # 韪 +U+97EB: yùn # 韫 +U+97EC: tāo # 韬 +U+97ED: jiǔ # 韭 +U+97F3: yīn # 音 +U+97F5: yùn # 韵 +U+97F6: sháo # 韶 +U+9875: yè # 页 +U+9876: dǐng # 顶 +U+9877: qǐng # 顷 +U+9878: hān # 顸 +U+9879: xiàng # 项 +U+987A: shùn # 顺 +U+987B: xū # 须 +U+987C: xū # 顼 +U+987D: wán # 顽 +U+987E: gù # 顾 +U+987F: dùn # 顿 +U+9880: qí # 颀 +U+9881: bān # 颁 +U+9882: sòng # 颂 +U+9883: háng # 颃 +U+9884: yù # 预 +U+9885: lú # 颅 +U+9886: lǐng # 领 +U+9887: pǒ # 颇 -> pō +U+9888: jǐng # 颈 +U+9889: jié # 颉 +U+988A: jiá # 颊 +U+988B: tǐng # 颋 +U+988C: hé # 颌 +U+988D: yǐng # 颍 +U+988E: jiǒng # 颎 +U+988F: kē # 颏 +U+9890: yí # 颐 +U+9891: pín # 频 +U+9893: tuí # 颓 +U+9894: hàn # 颔 +U+9896: yǐng # 颖 +U+9897: kē # 颗 +U+9898: tí # 题 +U+9899: yóng # 颙 +U+989A: è # 颚 +U+989B: zhuān # 颛 +U+989C: yán # 颜 +U+989D: é # 额 +U+989E: niè # 颞 +U+989F: mān # 颟 +U+98A0: diān # 颠 +U+98A1: sǎng # 颡 +U+98A2: hào # 颢 +U+98A4: chàn # 颤 +U+98A5: rú # 颥 +U+98A6: pín # 颦 +U+98A7: quán # 颧 +U+98CE: fēng # 风 +U+98CF: yáng # 飏 +U+98D0: zhǎn # 飐 +U+98D1: biāo # 飑 +U+98D2: sà # 飒 +U+98D3: jù # 飓 +U+98D4: sī # 飔 +U+98D5: sōu # 飕 +U+98D7: liú # 飗 +U+98D8: piāo # 飘 +U+98D9: biāo # 飙 +U+98DE: fēi # 飞 +U+98DF: shí # 食 +U+98E7: sūn # 飧 +U+98E8: xiǎng # 飨 +U+990D: yàn # 餍 +U+9910: cān # 餐 +U+992E: tiè # 餮 +U+9954: yōng # 饔 +U+9955: tāo # 饕 +U+9965: jī # 饥 +U+9967: táng # 饧 -> xíng +U+9968: tún # 饨 +U+9969: xì # 饩 +U+996A: rèn # 饪 +U+996B: yù # 饫 +U+996C: chì # 饬 +U+996D: fàn # 饭 +U+996E: yǐn # 饮 +U+996F: jiàn # 饯 +U+9970: shì # 饰 +U+9971: bǎo # 饱 +U+9972: sì # 饲 +U+9973: duò # 饳 +U+9974: yí # 饴 +U+9975: ěr # 饵 +U+9976: ráo # 饶 +U+9977: xiǎng # 饷 +U+9978: hé # 饸 +U+9979: le # 饹 -> gē +U+997A: jiǎo # 饺 +U+997B: xī # 饻 +U+997C: bǐng # 饼 +U+997D: bō # 饽 +U+997F: è # 饿 +U+9981: něi # 馁 +U+9983: guǒ # 馃 +U+9984: hún # 馄 +U+9985: xiàn # 馅 +U+9986: guǎn # 馆 +U+9987: chā # 馇 +U+9988: kuì # 馈 +U+9989: gǔ # 馉 +U+998A: sōu # 馊 +U+998B: chán # 馋 +U+998C: yè # 馌 +U+998D: mó # 馍 +U+998F: liú # 馏 -> liù +U+9990: xiū # 馐 +U+9991: jǐn # 馑 +U+9992: mán # 馒 +U+9993: sǎn # 馓 +U+9994: zhuàn # 馔 +U+9995: náng # 馕 +U+9996: shǒu # 首 +U+9997: kuí # 馗 +U+9998: guó # 馘 +U+9999: xiāng # 香 +U+999D: bì # 馝 +U+999E: bó # 馞 +U+99A5: fù # 馥 +U+99A7: yūn # 馧 +U+99A8: xīn # 馨 +U+9A6C: mǎ # 马 +U+9A6D: yù # 驭 +U+9A6E: tuó # 驮 +U+9A6F: xùn # 驯 +U+9A70: chí # 驰 +U+9A71: qū # 驱 +U+9A72: rì # 驲 +U+9A73: bó # 驳 +U+9A74: lǘ # 驴 +U+9A75: zǎng # 驵 +U+9A76: shǐ # 驶 +U+9A77: sì # 驷 +U+9A78: fù # 驸 +U+9A79: jū # 驹 +U+9A7A: zōu # 驺 +U+9A7B: zhù # 驻 +U+9A7C: tuó # 驼 +U+9A7D: nú # 驽 +U+9A7E: jià # 驾 +U+9A7F: yì # 驿 +U+9A80: dài # 骀 -> tái +U+9A81: xiāo # 骁 +U+9A82: mà # 骂 +U+9A83: yīn # 骃 +U+9A84: jiāo # 骄 +U+9A85: huá # 骅 +U+9A86: luò # 骆 +U+9A87: hài # 骇 +U+9A88: pián # 骈 +U+9A89: biāo # 骉 +U+9A8A: lí # 骊 +U+9A8B: chěng # 骋 +U+9A8C: yàn # 验 +U+9A8D: xīng # 骍 +U+9A8E: qīn # 骎 +U+9A8F: jùn # 骏 +U+9A90: qí # 骐 +U+9A91: qí # 骑 +U+9A92: kè # 骒 +U+9A93: zhuī # 骓 +U+9A95: sù # 骕 +U+9A96: cān # 骖 +U+9A97: piàn # 骗 +U+9A98: zhì # 骘 +U+9A99: kuí # 骙 +U+9A9A: sāo # 骚 +U+9A9B: wù # 骛 +U+9A9C: ào # 骜 -> áo +U+9A9D: liú # 骝 +U+9A9E: qiān # 骞 +U+9A9F: shàn # 骟 +U+9AA0: biāo # 骠 -> piào +U+9AA1: luó # 骡 +U+9AA2: cōng # 骢 +U+9AA3: chǎn # 骣 +U+9AA4: zhòu # 骤 +U+9AA5: jì # 骥 +U+9AA6: shuāng # 骦 +U+9AA7: xiāng # 骧 +U+9AA8: gǔ # 骨 +U+9AB0: tóu # 骰 +U+9AB1: jiè # 骱 +U+9AB6: dǐ # 骶 +U+9AB7: kū # 骷 +U+9AB8: hái # 骸 +U+9ABA: hóu # 骺 +U+9ABC: gé # 骼 +U+9AC0: bì # 髀 +U+9AC1: kē # 髁 +U+9AC2: qià # 髂 +U+9AC3: yú # 髃 +U+9AC5: lóu # 髅 +U+9ACB: kuān # 髋 +U+9ACC: bìn # 髌 +U+9ACE: liáo # 髎 +U+9AD1: dú # 髑 +U+9AD3: suǐ # 髓 +U+9AD8: gāo # 高 +U+9AE1: kūn # 髡 +U+9AE2: dí # 髢 +U+9AE6: máo # 髦 +U+9AEB: tiáo # 髫 +U+9AED: zī # 髭 +U+9AEF: rán # 髯 +U+9AF9: xiū # 髹 +U+9AFB: jì # 髻 +U+9AFD: zhuā # 髽 +U+9B03: zōng # 鬃 +U+9B08: quán # 鬈 +U+9B0F: jiū # 鬏 +U+9B12: zhěn # 鬒 +U+9B13: bìn # 鬓 +U+9B18: mán # 鬘 +U+9B1F: huán # 鬟 +U+9B23: liè # 鬣 +U+9B2F: chàng # 鬯 +U+9B32: gé # 鬲 +U+9B36: guī # 鬶 +U+9B37: zōng # 鬷 +U+9B3B: yù # 鬻 +U+9B3C: guǐ # 鬼 +U+9B41: kuí # 魁 +U+9B42: hún # 魂 +U+9B43: bá # 魃 +U+9B44: pò # 魄 +U+9B45: mèi # 魅 +U+9B46: xū # 魆 +U+9B47: yǎn # 魇 +U+9B48: xiāo # 魈 +U+9B49: liǎng # 魉 +U+9B4B: tuí # 魋 +U+9B4D: wǎng # 魍 +U+9B4F: wèi # 魏 +U+9B51: chī # 魑 +U+9B54: mó # 魔 +U+9C7C: yú # 鱼 +U+9C7D: dāo # 鱽 +U+9C7E: jǐ # 鱾 +U+9C7F: yóu # 鱿 +U+9C80: tún # 鲀 +U+9C81: lǔ # 鲁 +U+9C82: fáng # 鲂 +U+9C83: bā # 鲃 +U+9C85: bà # 鲅 +U+9C86: píng # 鲆 +U+9C87: nián # 鲇 +U+9C88: lú # 鲈 +U+9C89: yóu # 鲉 +U+9C8A: zhǎ # 鲊 +U+9C8B: fù # 鲋 +U+9C8C: bà # 鲌 -> bó +U+9C8D: bào # 鲍 +U+9C8E: hòu # 鲎 +U+9C8F: pí # 鲏 +U+9C90: tái # 鲐 +U+9C91: guī # 鲑 +U+9C92: jié # 鲒 +U+9C94: wěi # 鲔 +U+9C95: ér # 鲕 +U+9C96: tóng # 鲖 +U+9C97: zéi # 鲗 +U+9C98: hòu # 鲘 +U+9C99: kuài # 鲙 +U+9C9A: jì # 鲚 +U+9C9B: jiāo # 鲛 +U+9C9C: xiān # 鲜 +U+9C9D: zhǎ # 鲝 +U+9C9E: xiǎng # 鲞 +U+9C9F: xún # 鲟 +U+9CA0: gěng # 鲠 +U+9CA1: lí # 鲡 +U+9CA2: lián # 鲢 +U+9CA3: jiān # 鲣 +U+9CA4: lǐ # 鲤 +U+9CA5: shí # 鲥 +U+9CA6: tiáo # 鲦 +U+9CA7: gǔn # 鲧 +U+9CA8: shā # 鲨 +U+9CA9: huàn # 鲩 +U+9CAA: jūn # 鲪 +U+9CAB: jì # 鲫 +U+9CAC: yǒng # 鲬 +U+9CAD: qīng # 鲭 +U+9CAE: líng # 鲮 +U+9CAF: qí # 鲯 +U+9CB0: zōu # 鲰 +U+9CB1: fēi # 鲱 +U+9CB2: kūn # 鲲 +U+9CB3: chāng # 鲳 +U+9CB4: gù # 鲴 +U+9CB5: ní # 鲵 +U+9CB7: diāo # 鲷 +U+9CB8: jīng # 鲸 +U+9CB9: shēn # 鲹 +U+9CBA: shī # 鲺 +U+9CBB: zī # 鲻 +U+9CBC: fèn # 鲼 +U+9CBD: dié # 鲽 +U+9CBE: bī # 鲾 +U+9CBF: cháng # 鲿 +U+9CC0: tí # 鳀 +U+9CC1: wēn # 鳁 +U+9CC2: wēi # 鳂 +U+9CC3: sāi # 鳃 +U+9CC4: è # 鳄 +U+9CC5: qiū # 鳅 +U+9CC7: huáng # 鳇 +U+9CC8: quán # 鳈 +U+9CC9: jiāng # 鳉 +U+9CCA: biān # 鳊 +U+9CCC: áo # 鳌 +U+9CCD: qí # 鳍 +U+9CCE: tǎ # 鳎 +U+9CCF: guān # 鳏 +U+9CD0: yáo # 鳐 +U+9CD1: páng # 鳑 +U+9CD2: jiān # 鳒 +U+9CD3: lè # 鳓 +U+9CD4: biào # 鳔 +U+9CD5: xuě # 鳕 +U+9CD6: biē # 鳖 +U+9CD7: mán # 鳗 +U+9CD8: mǐn # 鳘 +U+9CD9: yōng # 鳙 +U+9CDA: wèi # 鳚 +U+9CDB: xí # 鳛 +U+9CDC: guì # 鳜 +U+9CDD: shàn # 鳝 +U+9CDE: lín # 鳞 +U+9CDF: zūn # 鳟 +U+9CE0: hù # 鳠 +U+9CE1: gǎn # 鳡 +U+9CE2: lǐ # 鳢 +U+9CE3: zhān # 鳣 +U+9CE4: guǎn # 鳤 +U+9E1F: niǎo # 鸟 +U+9E20: jiū # 鸠 +U+9E21: jī # 鸡 +U+9E22: yuān # 鸢 +U+9E23: míng # 鸣 +U+9E24: shī # 鸤 +U+9E25: ōu # 鸥 +U+9E26: yā # 鸦 +U+9E27: cāng # 鸧 +U+9E28: bǎo # 鸨 +U+9E29: zhèn # 鸩 +U+9E2A: gū # 鸪 +U+9E2B: dōng # 鸫 +U+9E2C: lú # 鸬 +U+9E2D: yā # 鸭 +U+9E2E: xiāo # 鸮 +U+9E2F: yāng # 鸯 +U+9E30: líng # 鸰 +U+9E31: chī # 鸱 +U+9E32: qú # 鸲 +U+9E33: yuān # 鸳 +U+9E35: tuó # 鸵 +U+9E36: sī # 鸶 +U+9E37: zhì # 鸷 +U+9E38: ér # 鸸 +U+9E39: guā # 鸹 +U+9E3A: xiū # 鸺 +U+9E3B: héng # 鸻 +U+9E3C: zhōu # 鸼 +U+9E3D: gē # 鸽 +U+9E3E: luán # 鸾 +U+9E3F: hóng # 鸿 +U+9E40: wú # 鹀 +U+9E41: bó # 鹁 +U+9E42: lí # 鹂 +U+9E43: juān # 鹃 +U+9E44: gǔ # 鹄 -> hú +U+9E45: é # 鹅 +U+9E46: yù # 鹆 +U+9E47: xián # 鹇 +U+9E48: tí # 鹈 +U+9E49: wǔ # 鹉 +U+9E4A: què # 鹊 +U+9E4B: miáo # 鹋 +U+9E4C: ān # 鹌 +U+9E4D: kūn # 鹍 +U+9E4E: bēi # 鹎 +U+9E4F: péng # 鹏 +U+9E50: qiān # 鹐 +U+9E51: chún # 鹑 +U+9E52: gēng # 鹒 +U+9E54: sù # 鹔 +U+9E55: hú # 鹕 +U+9E56: hé # 鹖 +U+9E57: è # 鹗 +U+9E58: gǔ # 鹘 +U+9E59: qiū # 鹙 +U+9E5A: cí # 鹚 +U+9E5B: méi # 鹛 +U+9E5C: wù # 鹜 +U+9E5D: yì # 鹝 +U+9E5E: yào # 鹞 +U+9E5F: wēng # 鹟 +U+9E60: liú # 鹠 +U+9E61: jí # 鹡 -> jī +U+9E62: yì # 鹢 +U+9E63: jiān # 鹣 +U+9E64: hè # 鹤 +U+9E66: yīng # 鹦 +U+9E67: zhè # 鹧 +U+9E68: liù # 鹨 +U+9E69: liáo # 鹩 +U+9E6A: jiāo # 鹪 +U+9E6B: jiù # 鹫 +U+9E6C: yù # 鹬 +U+9E6D: lù # 鹭 +U+9E6E: huán # 鹮 +U+9E6F: zhān # 鹯 +U+9E70: yīng # 鹰 +U+9E71: hù # 鹱 +U+9E72: méng # 鹲 +U+9E73: guàn # 鹳 +U+9E74: shuāng # 鹴 +U+9E7E: cuó # 鹾 +U+9E7F: lù # 鹿 +U+9E80: yōu # 麀 +U+9E82: jǐ # 麂 +U+9E87: jūn # 麇 +U+9E88: zhǔ # 麈 +U+9E8B: mí # 麋 +U+9E91: ní # 麑 +U+9E92: qí # 麒 +U+9E93: lù # 麓 +U+9E96: jīng # 麖 +U+9E9D: shè # 麝 +U+9E9F: lín # 麟 +U+9EA6: mài # 麦 +U+9EB8: fū # 麸 +U+9EB9: qū # 麹 +U+9EBB: má # 麻 +U+9EBD: mó # 麽 +U+9EBE: huī # 麾 +U+9EC4: huáng # 黄 +U+9EC7: tiān # 黇 +U+9EC9: hóng # 黉 +U+9ECD: shǔ # 黍 +U+9ECE: lí # 黎 +U+9ECF: nián # 黏 +U+9ED1: hēi # 黑 +U+9ED4: qián # 黔 +U+9ED8: mò # 默 +U+9EDB: dài # 黛 +U+9EDC: chù # 黜 +U+9EDD: yǒu # 黝 +U+9EDF: yī # 黟 +U+9EE0: xiá # 黠 +U+9EE1: yǎn # 黡 +U+9EE2: qū # 黢 +U+9EE5: qíng # 黥 +U+9EE7: lí # 黧 +U+9EE9: dú # 黩 +U+9EEA: cǎn # 黪 +U+9EEF: àn # 黯 +U+9EF9: zhǐ # 黹 +U+9EFB: fú # 黻 +U+9EFC: fǔ # 黼 +U+9EFE: mǐn # 黾 +U+9F0B: yuán # 鼋 +U+9F0D: tuó # 鼍 +U+9F0E: dǐng # 鼎 +U+9F10: nài # 鼐 +U+9F12: zī # 鼒 +U+9F13: gǔ # 鼓 +U+9F17: táo # 鼗 +U+9F19: pí # 鼙 +U+9F20: shǔ # 鼠 +U+9F22: fén # 鼢 +U+9F29: qú # 鼩 +U+9F2B: shí # 鼫 +U+9F2C: yòu # 鼬 +U+9F2F: wú # 鼯 +U+9F31: jīng # 鼱 +U+9F37: xī # 鼷 +U+9F39: yǎn # 鼹 +U+9F3B: bí # 鼻 +U+9F3D: qiú # 鼽 +U+9F3E: hān # 鼾 +U+9F41: hōu # 齁 +U+9F47: zhā # 齇 +U+9F49: nàng # 齉 +U+9F50: qí # 齐 +U+9F51: jī # 齑 +U+9F7F: chǐ # 齿 +U+9F80: chèn # 龀 +U+9F81: hé # 龁 +U+9F82: yín # 龂 +U+9F83: jǔ # 龃 +U+9F84: líng # 龄 +U+9F85: bāo # 龅 +U+9F86: tiáo # 龆 +U+9F87: zī # 龇 +U+9F88: kěn # 龈 -> yín +U+9F89: yǔ # 龉 +U+9F8A: chuò # 龊 +U+9F8B: qǔ # 龋 +U+9F8C: wò # 龌 +U+9F99: lóng # 龙 +U+9F9A: gōng # 龚 +U+9F9B: kān # 龛 +U+9F9F: guī # 龟 +U+9FA0: yuè # 龠 +U+9FA2: hé # 龢 +U+9FCD: gàng # 鿍 +U+9FCE: tǎ # 鿎 +U+9FCF: mài # 鿏 +# Extension A (77) +U+3447: zhòu # 㑇 +U+344A: yì # 㑊 +U+356E: fǔ # 㕮 +U+360E: hǎn # 㘎 +U+364D: duō # 㙍 +U+3658: yāo # 㙘 +U+3666: xié # 㙦 +U+36C3: jié # 㛃 +U+36DA: tǒng # 㛚 +U+36F9: pián # 㛹 +U+37C3: sī # 㟃 +U+3807: jiù # 㠇 +U+3813: méng # 㠓 +U+3918: zhòu # 㤘 -> chù +U+3944: líng # 㥄 +U+39D0: sǒng # 㧐 +U+39D1: huī # 㧑 -> wéi +U+39DF: kuǎi # 㧟 +U+3AF0: làng # 㫰 -> lǎng +U+3B0A: huàn # 㬊 -> huǎn +U+3B0E: xiǎn # 㬎 +U+3B1A: chè # 㬚 +U+3B4E: gāng # 㭎 +U+3B55: qū # 㭕 +U+3BBE: lǎng # 㮾 +U+3C00: lí # 㰀 +U+3CC7: fù # 㳇 +U+3CD8: chōng # 㳘 +U+3CDA: xù # 㳚 -> yù +U+3D14: xī # 㴔 -> sè +U+3D50: jué # 㵐 +U+3DB2: yòng # 㶲 +U+3E06: kào # 㸆 +U+3E0C: huò # 㸌 +U+3E84: yǔ # 㺄 +U+3EEC: tū # 㻬 -> tú +U+3F4F: gàn # 㽏 +U+3FE0: huàng # 㿠 +U+4056: lōu # 䁖 +U+40AE: lüè # 䂮 +U+40C5: dī # 䃅 +U+40CE: zhà # 䃎 -> zhǎ +U+415F: cǎn # 䅟 +U+4339: jiǒng # 䌹 +U+4383: rǎn # 䎃 +U+4396: zēng # 䎖 +U+43DD: zhuān # 䏝 -> chún +U+43E1: shì # 䏡 +U+43F2: dié # 䏲 +U+4403: jùn # 䐃 -> jiǒng +U+44D6: qióng # 䓖 +U+44DB: qū # 䓛 -> fǔ +U+44E8: yīng # 䓨 +U+44EB: qí # 䓫 -> jì +U+44EC: zhuó # 䓬 +U+45D6: dì # 䗖 +U+45DB: xiū # 䗛 +U+45EA: zhè # 䗪 +U+45F4: tíng # 䗴 +U+4723: xīn # 䜣 -> xī +U+4759: chū # 䝙 +U+48BA: chū # 䢺 +U+48BC: gōng # 䢼 +U+48D8: táng # 䣘 +U+497D: pō # 䥽 +U+4983: zhuō # 䦃 +U+4C9F: yìn # 䲟 +U+4CA0: chūn # 䲠 +U+4CA2: téng # 䲢 +U+4D13: shī # 䴓 +U+4D14: jiāo # 䴔 +U+4D15: liè # 䴕 +U+4D16: jīng # 䴖 +U+4D17: jú # 䴗 +U+4D18: tī # 䴘 +U+4D19: pì # 䴙 +U+4DAE: yǎn # 䶮 +# Extension B (36) +U+20164: xí # 𠅤 +U+20676: ǒu # 𠙶 +U+20CD0: bāng # 𠳐 +U+2139A: piǎn # 𡎚 +U+21413: kāng # 𡐓 +U+235CB: dǎng # 𣗋 +U+23C97: wéi # 𣲗 +U+23C98: wǔ # 𣲘 +U+23E23: fén # 𣸣 +U+249DB: dì # 𤧛 +U+24A7D: huán # 𤩽 +U+24AC9: xiè # 𤫉 +U+25532: è # 𥔲 +U+25562: cáo # 𥕢 +U+255A8: zào # 𥖨 +U+25ED7: chá # 𥻗 +U+26221: xū # 𦈡 +U+2648D: tóng # 𦒍 +U+26676: gǔ # 𦙶 +U+2677C: lǘ # 𦝼 +U+26B5C: zhī # 𦭜 +U+26C21: nà # 𦰡 +U+27FF9: mǔ # 𧿹 +U+28408: guāng # 𨐈 +U+28678: qí # 𨙸 +U+28695: biàn # 𨚕 +U+287E0: quān # 𨟠 -> què +U+28B49: bān # 𨭉 +U+28C47: qiú # 𨱇 +U+28C4F: dā # 𨱏 +U+28C51: huáng # 𨱑 +U+28C54: zūn # 𨱔 +U+28E99: nì # 𨺙 +U+29F7E: ān # 𩽾 -> ān +U+29F83: miǎn # 𩾃 +U+29F8C: kāng # 𩾌 -> kāng +# Extension C (44) +U+2A7DD: jì # 𪟝 +U+2A8FB: lóu # 𪣻 +U+2A917: liào # 𪤗 +U+2AA30: qū # 𪨰 +U+2AA36: shē # 𪨶 +U+2AA58: yǎn # 𪩘 +U+2AFA2: xiàn # 𪾢 +U+2B127: yán # 𫄧 +U+2B128: chī # 𫄨 +U+2B137: yì # 𫄷 +U+2B138: xūn # 𫄸 +U+2B1ED: wěi # 𫇭 +U+2B300: jī # 𫌀 +U+2B363: tóng # 𫍣 +U+2B36F: xián # 𫍯 +U+2B372: xiǎo # 𫍲 +U+2B37D: xuān # 𫍽 +U+2B404: yuè # 𫐄 +U+2B410: ní # 𫐐 +U+2B413: bù # 𫐓 +U+2B461: méng # 𫑡 +U+2B4E7: fū # 𫓧 +U+2B4EF: jī # 𫓯 +U+2B4F6: xuān # 𫓶 +U+2B4F9: jī # 𫓹 +U+2B50D: fán # 𫔍 +U+2B50E: jué # 𫔎 +U+2B536: niè # 𫔶 +U+2B5AE: yǐ # 𫖮 +U+2B5AF: fǔ # 𫖯 +U+2B5B3: yūn # 𫖳 +U+2B5E7: sù # 𫗧 +U+2B5F4: zhān # 𫗴 +U+2B61C: wén # 𫘜 +U+2B61D: jué # 𫘝 +U+2B626: táo # 𫘦 => U+9A0A +U+2B627: lù # 𫘧 => U+9A04 +U+2B628: tí # 𫘨 +U+2B62A: yuán # 𫘪 => U+9A35 +U+2B62C: xí # 𫘬 => U+9A31 +U+2B695: shī # 𫚕 +U+2B696: cǐ # 𫚖 => U+9B86 ?-> jì +U+2B6AD: liè # 𫚭 => U+9C72 +U+2B6ED: kuáng # 𫛭 => U+9D5F +# Extension D (8) +U+2B7A9: mén # 𫞩 +U+2B7C5: liáng # 𫟅 +U+2B7E6: suì # 𫟦 +U+2B7F9: hóng # 𫟹 +U+2B7FC: dá # 𫟼 +U+2B806: kuǐ # 𫠆 +U+2B80A: xuán # 𫠊 +U+2B81C: ní # 𫠜 +# Extension E (108) +U+2B8B8: dàn # 𫢸 => U+50E4 +U+2BAC7: ě # 𫫇 => U+5641 +U+2BB5F: ōu # 𫭟 => U+5878 ?-> qiū +U+2BB62: lǔn # 𫭢 => U+57E8 +U+2BB7C: láo # 𫭼 => U+2144D +U+2BB83: shàn # 𫮃 => U+58A0 +U+2BC1B: xíng # 𫰛 => U+5A19 +U+2BD77: lì # 𫵷 => U+3823 +U+2BD87: dié # 𫶇 => U+5D7D ?-> dì +U+2BDF7: xīn # 𫷷 => U+5EDE +U+2BE29: kōu # 𫸩 => U+5F44 +U+2C029: wěi # 𬀩 => U+6690 +U+2C02A: xiàn # 𬀪 => U+665B +U+2C0A9: jiā # 𬂩 => U+689C +U+2C0CA: zhì # 𬃊 => U+6ACD +U+2C1D5: wàn # 𬇕 => U+6FAB ?-> màn +U+2C1D9: pèi # 𬇙 => U+6D7F +U+2C1F9: guó # 𬇹 => U+6F0D +U+2C27C: ōu # 𬉼 => U+71B0 ?-> ǒu +U+2C288: xún # 𬊈 => U+71D6 +U+2C2A4: chǎn # 𬊤 => U+71C0 ?-> dǎn,chàn +U+2C317: hé # 𬌗 +U+2C35B: lì # 𬍛 => U+74C5 +U+2C361: dàng # 𬍡 => U+7497 +U+2C364: xún # 𬍤 => U+7495 +U+2C488: què # 𬒈 => U+7910 ?-> hú +U+2C494: gěng # 𬒔 +U+2C497: lán # 𬒗 +U+2C542: gōng # 𬕂 => U+7BE2 ?-> gǎn,lǒng +U+2C613: xún # 𬘓 => U+7D03 +U+2C618: dǎn # 𬘘 => U+7D1E +U+2C621: yīn # 𬘡 => U+7D6A +U+2C629: tīng # 𬘩 => U+7D8E +U+2C62B: huán # 𬘫 => U+7D84 ?-> huàn,wàn +U+2C62C: qiàn # 𬘬 => U+7DAA ?-> qīng,zhēng +U+2C62D: lín # 𬘭 => U+7D9D ?-> chēn +U+2C62F: zhǔn # 𬘯 => U+7DA7 ?-> zhùn +U+2C642: yǎn # 𬙂 => U+7E2F ?-> yǐn +U+2C64A: mò # 𬙊 => U+7E86 +U+2C64B: xiāng # 𬙋 => U+7E95 ?-> rǎng +U+2C72C: màn # 𬜬 => U+8504 +U+2C72F: liǎng # 𬜯 => U+44E3 +U+2C79F: pín # 𬞟 => U+860B ?-> píng +U+2C7C1: yì # 𬟁 => U+8649 +U+2C7FD: dōng # 𬟽 => U+8740 +U+2C8D9: xū # 𬣙 => U+8A0F +U+2C8DE: zhǔ # 𬣞 => U+8A5D +U+2C8E1: jiàn # 𬣡 => U+8AD3 +U+2C8F3: hěn # 𬣳 => U+8A6A +U+2C907: yīn # 𬤇 => U+8AF2 +U+2C90A: shì # 𬤊 => U+8ADF ?-> dì +U+2C91D: huì # 𬤝 => U+8B53 +U+2CA02: qí # 𬨂 => U+8EDD +U+2CA0E: yóu # 𬨎 => U+8F36 +U+2CA7D: xún # 𬩽 => U+9129 +U+2CAA9: nóng # 𬪩 => U+91B2 +U+2CB29: yì # 𬬩 => U+91F4 +U+2CB2D: lún # 𬬭 => U+9300 +U+2CB2E: chǎng # 𬬮 => U+92F9 +U+2CB31: jīn # 𬬱 => U+91FF +U+2CB38: shù # 𬬸 => U+9265 +U+2CB39: shén # 𬬹 => U+926E +U+2CB3B: lú # 𬬻 => U+946A +U+2CB3F: zhāo # 𬬿 => U+924A +U+2CB41: mǔ # 𬭁 => U+9267 +U+2CB4A: dù # 𬭊 => U+289C0 +U+2CB4E: hóng # 𬭎 => U+92D0 +U+2CB5A: chún # 𬭚 => U+931E +U+2CB5B: bō # 𬭛 => U+28A0F +U+2CB64: hóu # 𬭤 => U+936D +U+2CB69: wēng # 𬭩 => U+9393 +U+2CB6C: wèi # 𬭬 => U+93CF +U+2CB6F: piě # 𬭯 => U+4955 +U+2CB73: xǐ # 𬭳 => U+28B4E +U+2CB76: hēi # 𬭶 => U+28B46 +U+2CB78: lín # 𬭸 => U+93FB +U+2CB7C: suì # 𬭼 => U+9429 +U+2CBB1: yīn # 𬮱 => U+95C9 +U+2CBBF: qí # 𬮿 => U+9691 ?-> gāi,ái +U+2CBC0: jī # 𬯀 => U+96AE +U+2CBCE: tuí # 𬯎 => U+96A4 +U+2CC56: dí # 𬱖 => U+9814 +U+2CC5F: wěi # 𬱟 => U+9820 +U+2CCF5: pī # 𬳵 => U+99D3 +U+2CCF6: jiōng # 𬳶 => U+99C9 +U+2CCFD: shēn # 𬳽 => U+99EA +U+2CCFF: tú # 𬳿 => U+99FC +U+2CD02: fēi # 𬴂 => U+9A11 +U+2CD03: huō # 𬴃 => U+9A1E +U+2CD0A: lín # 𬴊 => U+9A4E +U+2CD8B: jū # 𬶋 => U+9B88 +U+2CD8D: tuó # 𬶍 => U+9B80 +U+2CD8F: wéi # 𬶏 => U+9BA0 +U+2CD90: zhào # 𬶐 => U+9BA1 +U+2CD9F: là # 𬶟 => U+9BFB +U+2CDA0: liàn # 𬶠 => U+9C0A +U+2CDA8: jì # 𬶨 => U+9C40 +U+2CDAD: jì # 𬶭 => U+9C36 +U+2CDAE: xǐ # 𬶮 => U+9C5A +U+2CDD5: bū # 𬷕 => U+9D4F +U+2CE18: yǎn # 𬸘 => U+9DA0 +U+2CE1A: yuè # 𬸚 => U+9E11 +U+2CE23: xiān # 𬸣 => U+9DB1 +U+2CE26: zhuó # 𬸦 => U+9DDF +U+2CE2A: fán # 𬸪 => U+9DED +U+2CE7C: xiè # 𬹼 => U+9F58 +U+2CE88: yǐ # 𬺈 => U+9F6E +U+2CE93: chǔ # 𬺓 => U+9F7C +# EOF diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/pinyin/parser.js b/plugins/lx-music-ztools/vendor/lx/src/common/utils/pinyin/parser.js new file mode 100644 index 000000000..ee99f90a4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/pinyin/parser.js @@ -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) + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/pinyin/pinyin.json b/plugins/lx-music-ztools/vendor/lx/src/common/utils/pinyin/pinyin.json new file mode 100644 index 000000000..72abc1f83 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/pinyin/pinyin.json @@ -0,0 +1 @@ +{"一":["yi"],"丁":["ding"],"七":["qi"],"万":["wan"],"丈":["zhang"],"三":["san"],"上":["shang"],"下":["xia"],"不":["bu"],"与":["yu"],"丏":["mian"],"丐":["gai"],"丑":["chou"],"专":["zhuan"],"且":["qie"],"丕":["pi"],"世":["shi"],"丘":["qiu"],"丙":["bing"],"业":["ye"],"丛":["cong"],"东":["dong"],"丝":["si"],"丞":["cheng"],"丢":["diu"],"两":["liang"],"严":["yan"],"丧":["sang"],"个":["ge"],"丫":["ya"],"中":["zhong"],"丰":["feng"],"串":["chuan"],"临":["lin"],"丸":["wan"],"丹":["dan"],"为":["wei"],"主":["zhu"],"丽":["li"],"举":["ju"],"乂":["yi"],"乃":["nai"],"久":["jiu"],"么":["me"],"义":["yi"],"之":["zhi"],"乌":["wu"],"乍":["zha"],"乎":["hu"],"乏":["fa"],"乐":["le"],"乒":["ping"],"乓":["pang"],"乔":["qiao"],"乖":["guai"],"乘":["cheng"],"乙":["yi"],"乜":["mie"],"九":["jiu"],"乞":["qi"],"也":["ye"],"习":["xi"],"乡":["xiang"],"书":["shu"],"乩":["ji"],"买":["mai"],"乱":["luan"],"乳":["ru"],"乸":["na"],"乾":["qian","gan"],"了":["le"],"予":["yu"],"争":["zheng"],"事":["shi"],"二":["er"],"亍":["chu"],"于":["yu"],"亏":["kui"],"云":["yun"],"互":["hu"],"亓":["qi"],"五":["wu"],"井":["jing"],"亘":["gen"],"亚":["ya"],"些":["xie"],"亟":["ji"],"亡":["wang"],"亢":["kang"],"交":["jiao"],"亥":["hai"],"亦":["yi"],"产":["chan"],"亨":["heng"],"亩":["mu"],"享":["xiang"],"京":["jing"],"亭":["ting"],"亮":["liang"],"亲":["qin"],"亳":["bo"],"亵":["xie"],"亶":["dan"],"亸":["duo"],"亹":["wei","men"],"人":["ren"],"亿":["yi"],"什":["shen"],"仁":["ren"],"仂":["le"],"仃":["ding"],"仄":["ze"],"仅":["jin"],"仆":["pu"],"仇":["chou"],"仉":["zhang"],"今":["jin"],"介":["jie"],"仍":["reng"],"从":["cong"],"仑":["lun"],"仓":["cang"],"仔":["zai","zi"],"仕":["shi"],"他":["ta"],"仗":["zhang"],"付":["fu"],"仙":["xian"],"仝":["tong"],"仞":["ren"],"仟":["qian"],"仡":["ge","yi"],"代":["dai"],"令":["ling"],"以":["yi"],"仨":["sa"],"仪":["yi"],"仫":["mu"],"们":["men"],"仰":["yang"],"仲":["zhong"],"仳":["pi"],"仵":["wu"],"件":["jian"],"价":["jia"],"任":["ren"],"份":["fen"],"仿":["fang"],"企":["qi"],"伈":["xin"],"伉":["kang"],"伊":["yi"],"伋":["ji"],"伍":["wu"],"伎":["ji"],"伏":["fu"],"伐":["fa"],"休":["xiu"],"众":["zhong"],"优":["you"],"伙":["huo"],"会":["hui"],"伛":["yu"],"伞":["san"],"伟":["wei"],"传":["chuan"],"伢":["ya"],"伣":["qian"],"伤":["shang"],"伥":["chang"],"伦":["lun"],"伧":["cang"],"伪":["wei"],"伫":["zhu"],"伭":["xian"],"伯":["bo"],"估":["gu"],"伲":["ni"],"伴":["ban"],"伶":["ling"],"伸":["shen"],"伺":["ci"],"似":["shi","si"],"伽":["ga","jia","qie"],"伾":["pi"],"佁":["yi"],"佃":["dian"],"但":["dan"],"位":["wei"],"低":["di"],"住":["zhu"],"佐":["zuo"],"佑":["you"],"体":["ti"],"何":["he"],"佖":["bi"],"佗":["tuo"],"佘":["she"],"余":["yu"],"佚":["yi"],"佛":["fu","fo"],"作":["zuo"],"佝":["gou"],"佞":["ning"],"佟":["tong"],"你":["ni"],"佣":["yong"],"佤":["wa"],"佥":["qian"],"佩":["pei"],"佬":["lao"],"佯":["yang"],"佰":["bai"],"佳":["jia"],"佴":["er"],"佶":["ji"],"佸":["huo"],"佺":["quan"],"佻":["tiao"],"佼":["jiao"],"佽":["ci"],"佾":["yi"],"使":["shi"],"侁":["shen"],"侂":["tuo"],"侃":["kan"],"侄":["zhi"],"侈":["chi"],"侉":["kua"],"例":["li"],"侍":["shi"],"侏":["zhu"],"侑":["you"],"侔":["mou"],"侗":["dong"],"侘":["cha"],"供":["gong"],"依":["yi"],"侠":["xia"],"侣":["lv"],"侥":["jiao"],"侦":["zhen"],"侧":["ce"],"侨":["qiao"],"侩":["kuai"],"侪":["chai"],"侬":["nong"],"侮":["wu"],"侯":["hou"],"侴":["chou","hao"],"侵":["qin"],"侹":["ting"],"便":["bian"],"促":["cu"],"俄":["e"],"俅":["qiu"],"俊":["jun"],"俍":["liang"],"俎":["zu"],"俏":["qiao"],"俐":["li"],"俑":["yong"],"俗":["su"],"俘":["fu"],"俙":["xi"],"俚":["li"],"俜":["ping"],"保":["bao"],"俞":["yu"],"俟":["qi","si"],"信":["xin"],"俣":["yu"],"俦":["chou"],"俨":["yan"],"俩":["lia","liang"],"俪":["li"],"俫":["lai"],"俭":["jian"],"修":["xiu"],"俯":["fu"],"俱":["ju"],"俳":["pai"],"俵":["biao"],"俶":["chu"],"俸":["feng"],"俺":["an"],"俾":["bi"],"倌":["guan"],"倍":["bei"],"倏":["shu"],"倒":["dao"],"倓":["tan"],"倔":["jue"],"倕":["chui"],"倘":["tang"],"候":["hou"],"倚":["yi"],"倜":["ti"],"倞":["jing","liang"],"借":["jie"],"倡":["chang"],"倥":["kong"],"倦":["juan"],"倧":["zong"],"倨":["ju"],"倩":["qian"],"倪":["ni"],"倬":["zhuo"],"倭":["wo"],"倮":["luo"],"倴":["ben"],"债":["zhai"],"倻":["ye"],"值":["zhi"],"倾":["qing"],"偁":["cheng"],"偃":["yan"],"假":["jia"],"偈":["ji"],"偌":["ruo"],"偎":["wei"],"偏":["pian"],"偓":["wo"],"偕":["xie"],"做":["zuo"],"停":["ting"],"偡":["zhan"],"健":["jian"],"偬":["zong"],"偭":["mian"],"偰":["xie"],"偲":["cai"],"偶":["ou"],"偷":["tou"],"偻":["lou","lv"],"偾":["fen"],"偿":["chang"],"傀":["gui","kui"],"傃":["su"],"傅":["fu"],"傈":["li"],"傉":["nu"],"傍":["bang"],"傒":["xi"],"傕":["jue","que"],"傣":["dai"],"傥":["tang"],"傧":["bin"],"储":["chu"],"傩":["nuo"],"催":["cui"],"傲":["ao"],"傺":["chi"],"傻":["sha"],"僇":["lu"],"僎":["zhuan"],"像":["xiang"],"僔":["zun"],"僖":["xi"],"僚":["liao"],"僦":["jiu"],"僧":["seng"],"僬":["jiao"],"僭":["jian"],"僮":["tong"],"僰":["bo"],"僳":["su"],"僵":["jiang"],"僻":["pi"],"儆":["jing"],"儇":["xuan"],"儋":["dan"],"儒":["ru"],"儡":["lei"],"儦":["biao"],"儳":["chan"],"儴":["rang"],"儿":["er"],"兀":["wu"],"允":["yun"],"元":["yuan"],"兄":["xiong"],"充":["chong"],"兆":["zhao"],"先":["xian"],"光":["guang"],"克":["ke"],"免":["mian"],"兑":["dui"],"兔":["tu"],"兕":["si"],"兖":["yan"],"党":["dang"],"兜":["dou"],"兢":["jing"],"入":["ru"],"全":["quan"],"八":["ba"],"公":["gong"],"六":["liu"],"兮":["xi"],"兰":["lan"],"共":["gong"],"关":["guan"],"兴":["xing"],"兵":["bing"],"其":["qi"],"具":["ju"],"典":["dian"],"兹":["zi"],"养":["yang"],"兼":["jian"],"兽":["shou"],"冀":["ji"],"冁":["chan"],"内":["nei"],"冈":["gang"],"冉":["ran"],"册":["ce"],"再":["zai"],"冏":["jiong"],"冒":["mao"],"冔":["xu"],"冕":["mian"],"冗":["rong"],"写":["xie"],"军":["jun"],"农":["nong"],"冠":["guan"],"冢":["zhong"],"冤":["yuan"],"冥":["ming"],"冬":["dong"],"冮":["gang"],"冯":["feng"],"冰":["bing"],"冱":["hu"],"冲":["chong"],"决":["jue"],"况":["kuang"],"冶":["ye"],"冷":["leng"],"冻":["dong"],"冼":["xian"],"冽":["lie"],"净":["jing"],"凄":["qi"],"准":["zhun"],"凇":["song"],"凉":["liang"],"凋":["diao"],"凌":["ling"],"减":["jian"],"凑":["cou"],"凓":["li"],"凘":["si"],"凛":["lin"],"凝":["ning"],"几":["ji"],"凡":["fan"],"凤":["feng"],"凫":["fu"],"凭":["ping"],"凯":["kai"],"凰":["huang"],"凳":["deng"],"凶":["xiong"],"凸":["tu"],"凹":["ao"],"出":["chu"],"击":["ji"],"凼":["dang"],"函":["han"],"凿":["zao"],"刀":["dao"],"刁":["diao"],"刃":["ren"],"分":["fen"],"切":["qie"],"刈":["yi"],"刊":["kan"],"刍":["chu"],"刎":["wen"],"刑":["xing"],"划":["hua"],"刖":["yue"],"列":["lie"],"刘":["liu"],"则":["ze"],"刚":["gang"],"创":["chuang"],"初":["chu"],"删":["shan"],"判":["pan"],"刨":["pao"],"利":["li"],"别":["bie"],"刬":["chan"],"刭":["jing"],"刮":["gua"],"到":["dao"],"刳":["ku"],"制":["zhi"],"刷":["shua"],"券":["quan"],"刹":["sha","cha"],"刺":["ci"],"刻":["ke"],"刽":["gui"],"刿":["gui"],"剀":["kai"],"剁":["duo"],"剂":["ji"],"剃":["ti"],"剅":["lou"],"削":["xue","xiao"],"剋":["ke","kei"],"剌":["la"],"前":["qian"],"剐":["gua"],"剑":["jian"],"剔":["ti"],"剕":["fei"],"剖":["pou"],"剜":["wan"],"剞":["ji"],"剟":["duo"],"剡":["shan","yan"],"剥":["bo","bao"],"剧":["ju"],"剩":["sheng"],"剪":["jian"],"副":["fu"],"割":["ge"],"剽":["piao"],"剿":["jiao"],"劁":["qiao"],"劂":["jue"],"劄":["zha"],"劈":["pi"],"劐":["huo"],"劓":["yi"],"力":["li"],"劝":["quan"],"办":["ban"],"功":["gong"],"加":["jia"],"务":["wu"],"劢":["mai"],"劣":["lie"],"动":["dong"],"助":["zhu"],"努":["nu"],"劫":["jie"],"劬":["qu"],"劭":["shao"],"励":["li"],"劲":["jin"],"劳":["lao"],"劼":["jie"],"劾":["he"],"势":["shi"],"勃":["bo"],"勇":["yong"],"勉":["mian"],"勋":["xun"],"勍":["qing"],"勐":["meng"],"勒":["lei","le"],"勔":["mian"],"勖":["xu"],"勘":["kan"],"勚":["yi"],"募":["mu"],"勠":["lu"],"勤":["qin"],"勰":["xie"],"勺":["shao"],"勾":["gou"],"勿":["wu"],"匀":["yun"],"包":["bao"],"匆":["cong"],"匈":["xiong"],"匍":["pu"],"匏":["pao"],"匐":["fu"],"匕":["bi"],"化":["hua"],"北":["bei"],"匙":["shi","chi"],"匜":["yi"],"匝":["za"],"匠":["jiang"],"匡":["kuang"],"匣":["xia"],"匦":["gui"],"匪":["fei"],"匮":["kui"],"匹":["pi"],"区":["qu"],"医":["yi"],"匼":["ke"],"匾":["bian"],"匿":["ni"],"十":["shi"],"千":["qian"],"卅":["sa"],"升":["sheng"],"午":["wu"],"卉":["hui"],"半":["ban"],"华":["hua"],"协":["xie"],"卑":["bei"],"卒":["zu"],"卓":["zhuo"],"单":["dan"],"卖":["mai"],"南":["nan"],"博":["bo"],"卜":["bo","bu"],"卞":["bian"],"卟":["bu"],"占":["zhan"],"卡":["ka"],"卢":["lu"],"卣":["you"],"卤":["lu"],"卦":["gua"],"卧":["wo"],"卫":["wei"],"卬":["ang","yang"],"卮":["zhi"],"卯":["mao"],"印":["yin"],"危":["wei"],"即":["ji"],"却":["que"],"卵":["luan"],"卷":["juan"],"卸":["xie"],"卺":["jin"],"卿":["qing"],"厂":["chang"],"厄":["e"],"厅":["ting"],"历":["li"],"厉":["li"],"压":["ya"],"厌":["yan"],"厍":["she"],"厕":["ce"],"厖":["pang","mang"],"厘":["li"],"厚":["hou"],"厝":["cuo"],"原":["yuan"],"厢":["xiang"],"厣":["yan"],"厥":["jue"],"厦":["sha"],"厨":["chu"],"厩":["jiu"],"厮":["si"],"去":["qu"],"厾":["du"],"县":["xian"],"叁":["san"],"参":["can"],"叆":["ai"],"叇":["dai"],"又":["you"],"叉":["cha"],"及":["ji"],"友":["you"],"双":["shuang"],"反":["fan"],"发":["fa"],"叔":["shu"],"叕":["zhuo"],"取":["qu"],"受":["shou"],"变":["bian"],"叙":["xu"],"叚":["jia"],"叛":["pan"],"叟":["sou"],"叠":["die"],"口":["kou"],"古":["gu"],"句":["ju"],"另":["ling"],"叨":["dao","tao"],"叩":["kou"],"只":["zhi"],"叫":["jiao"],"召":["zhao"],"叭":["ba"],"叮":["ding"],"可":["ke"],"台":["tai"],"叱":["chi"],"史":["shi"],"右":["you"],"叵":["po"],"叶":["ye"],"号":["hao"],"司":["si"],"叹":["tan"],"叻":["le"],"叼":["diao"],"叽":["ji"],"吁":["xu"],"吃":["chi"],"各":["ge"],"吆":["yao"],"合":["he"],"吉":["ji"],"吊":["diao"],"同":["tong"],"名":["ming"],"后":["hou"],"吏":["li"],"吐":["tu"],"向":["xiang"],"吒":["zha"],"吓":["xia"],"吕":["lv"],"吖":["ya"],"吗":["ma"],"君":["jun"],"吝":["lin"],"吞":["tun"],"吟":["yin"],"吠":["fei"],"吡":["bi","pi"],"吣":["qin"],"否":["fou"],"吧":["ba"],"吨":["dun"],"吩":["fen"],"含":["han"],"听":["ting"],"吭":["keng","hang"],"吮":["shun"],"启":["qi"],"吱":["zhi"],"吲":["yin"],"吴":["wu"],"吵":["chao"],"吸":["xi"],"吹":["chui"],"吻":["wen"],"吼":["hou"],"吽":["hong","hou"],"吾":["wu"],"呀":["ya"],"呃":["e"],"呆":["dai"],"呇":["qi"],"呈":["cheng"],"告":["gao"],"呋":["fu"],"呐":["na"],"呒":["wu","fu"],"呓":["yi"],"呔":["dai"],"呕":["ou"],"呖":["li"],"呗":["bei"],"员":["yuan"],"呙":["guo","wai"],"呛":["qiang"],"呜":["wu"],"呢":["ne"],"呣":["ḿ"],"呤":["ling"],"呦":["you"],"周":["zhou"],"呱":["gu","gua"],"呲":["ci","zi"],"味":["wei"],"呵":["he"],"呶":["nao"],"呷":["ga","xia"],"呸":["pei"],"呻":["shen"],"呼":["hu"],"命":["ming"],"咀":["ju"],"咂":["za"],"咄":["duo"],"咆":["pao"],"咇":["bie","bi"],"咉":["yang"],"咋":["za"],"和":["he"],"咍":["hai"],"咎":["jiu"],"咏":["yong"],"咐":["fu"],"咒":["zhou"],"咔":["ka"],"咕":["gu"],"咖":["ka"],"咙":["long"],"咚":["dong"],"咛":["ning"],"咝":["si"],"咡":["er"],"咣":["guang"],"咤":["zha"],"咥":["xi","die"],"咦":["yi"],"咧":["lie"],"咨":["zi"],"咩":["mie"],"咪":["mi"],"咫":["zhi"],"咬":["yao"],"咯":["ge","lo"],"咱":["zan"],"咳":["ke"],"咴":["hui"],"咸":["xian"],"咺":["xuan"],"咻":["xiu"],"咽":["yan"],"咿":["yi"],"哀":["ai"],"品":["pin"],"哂":["shen"],"哃":["tong"],"哄":["hong"],"哆":["duo"],"哇":["wa"],"哈":["ha"],"哉":["zai"],"哌":["pai"],"响":["xiang"],"哎":["ai"],"哏":["gen"],"哐":["kuang"],"哑":["ya"],"哒":["da"],"哓":["xiao"],"哔":["bi"],"哕":["hui","yue"],"哗":["hua"],"哙":["kuai"],"哚":["duo"],"哝":["nong"],"哞":["mou"],"哟":["yo"],"哢":["long"],"哥":["ge"],"哦":["o"],"哧":["chi"],"哨":["shao"],"哩":["li"],"哪":["na"],"哭":["ku"],"哮":["xiao"],"哱":["bo","po"],"哲":["zhe"],"哳":["zha"],"哺":["bu"],"哼":["heng"],"哽":["geng"],"哿":["ge"],"唁":["yan"],"唆":["suo"],"唇":["chun"],"唉":["ai"],"唏":["xi"],"唐":["tang"],"唑":["zuo"],"唔":["wu"],"唛":["ma"],"唝":["gong"],"唠":["lao"],"唢":["suo"],"唣":["zao"],"唤":["huan"],"唧":["ji"],"唪":["feng"],"唬":["hu"],"售":["shou"],"唯":["wei"],"唰":["shua"],"唱":["chang"],"唳":["li"],"唵":["an"],"唷":["yo"],"唼":["sha"],"唾":["tuo"],"唿":["hu"],"啁":["zhao","zhou"],"啃":["ken"],"啄":["zhuo"],"商":["shang"],"啉":["lin","lan"],"啊":["a"],"啐":["cui"],"啕":["tao"],"啖":["dan"],"啜":["chuai","chuo"],"啡":["fei"],"啤":["pi"],"啥":["sha"],"啦":["la"],"啧":["ze"],"啪":["pa"],"啫":["zhe"],"啬":["se"],"啭":["zhuan"],"啮":["nie"],"啰":["luo"],"啴":["chan","tan"],"啵":["bo"],"啶":["ding"],"啷":["lang"],"啸":["xiao"],"啻":["chi"],"啼":["ti"],"啾":["jiu"],"喀":["ka"],"喁":["yong"],"喂":["wei"],"喃":["nan"],"善":["shan"],"喆":["zhe"],"喇":["la"],"喈":["jie"],"喉":["hou"],"喊":["han"],"喋":["die"],"喏":["nuo"],"喑":["yin"],"喔":["o"],"喘":["chuan"],"喙":["hui"],"喜":["xi"],"喝":["he"],"喟":["kui"],"喤":["huang"],"喧":["xuan"],"喱":["li"],"喳":["zha"],"喵":["miao"],"喷":["pen"],"喹":["kui"],"喻":["yu"],"喽":["lou"],"喾":["ku"],"嗄":["a","sha"],"嗅":["xiu"],"嗉":["su"],"嗌":["ai","yi"],"嗍":["suo"],"嗐":["hai"],"嗑":["ke"],"嗒":["da","ta"],"嗓":["sang"],"嗔":["chen"],"嗖":["sou"],"嗜":["shi"],"嗝":["ge"],"嗞":["zi"],"嗟":["jie"],"嗡":["weng"],"嗣":["si"],"嗤":["chi"],"嗥":["hao"],"嗦":["suo"],"嗨":["hai"],"嗪":["qin"],"嗫":["nie"],"嗬":["he"],"嗯":["ń","ǹg"],"嗲":["die","dia"],"嗳":["ai"],"嗵":["tong"],"嗷":["ao"],"嗽":["sou"],"嗾":["sou"],"嘀":["di"],"嘁":["qi"],"嘈":["cao"],"嘉":["jia"],"嘌":["piao"],"嘎":["ga"],"嘏":["gu"],"嘘":["xu"],"嘚":["de"],"嘛":["ma"],"嘞":["lei"],"嘟":["du"],"嘡":["tang"],"嘣":["beng"],"嘤":["ying"],"嘧":["mi"],"嘬":["chuai","zuo"],"嘭":["peng"],"嘱":["zhu"],"嘲":["chao"],"嘴":["zui"],"嘶":["si"],"嘹":["liao"],"嘻":["xi"],"嘿":["hei"],"噀":["xun"],"噂":["zun"],"噇":["chuang"],"噌":["ceng"],"噍":["jiao"],"噎":["ye"],"噔":["deng"],"噗":["pu"],"噘":["jue"],"噙":["qin"],"噜":["lu"],"噢":["o"],"噤":["jin"],"器":["qi"],"噩":["e"],"噪":["zao"],"噫":["yi"],"噬":["shi"],"噱":["jue"],"噶":["ga"],"噻":["sai"],"噼":["pi"],"嚄":["huo"],"嚅":["ru"],"嚆":["hao"],"嚎":["hao"],"嚏":["ti"],"嚓":["ca"],"嚚":["yin"],"嚣":["xiao"],"嚭":["pi"],"嚯":["huo"],"嚷":["rang"],"嚼":["jue","jiao"],"囊":["nang"],"囔":["nang"],"囚":["qiu"],"四":["si"],"回":["hui"],"囟":["xin"],"因":["yin"],"囡":["nan"],"团":["tuan"],"囤":["dun"],"囫":["hu"],"园":["yuan"],"困":["kun"],"囱":["cong"],"围":["wei"],"囵":["lun"],"囷":["qun"],"囹":["ling"],"固":["gu"],"国":["guo"],"图":["tu"],"囿":["you"],"圃":["pu"],"圄":["yu"],"圆":["yuan"],"圈":["quan"],"圉":["yu"],"圊":["qing"],"圌":["chuan"],"圐":["ku"],"圙":["lüe"],"圜":["huan"],"土":["tu"],"圢":["ting"],"圣":["sheng"],"在":["zai"],"圩":["wei"],"圪":["ge"],"圫":["yu"],"圬":["wu"],"圭":["gui"],"圮":["pi"],"圯":["yi"],"地":["di"],"圲":["qian"],"圳":["zhen"],"圹":["kuang"],"场":["chang"],"圻":["qi"],"圾":["ji"],"址":["zhi"],"坂":["ban"],"均":["jun"],"坉":["tun"],"坊":["fang"],"坋":["ben"],"坌":["ben"],"坍":["tan"],"坎":["kan"],"坏":["huai"],"坐":["zuo"],"坑":["keng"],"坒":["bi"],"块":["kuai"],"坚":["jian"],"坛":["tan"],"坜":["li"],"坝":["ba"],"坞":["wu"],"坟":["fen"],"坠":["zhui"],"坡":["po"],"坤":["kun"],"坥":["qu"],"坦":["tan"],"坨":["tuo"],"坩":["gan"],"坪":["ping"],"坫":["dian"],"坬":["gua"],"坭":["ni"],"坯":["pi"],"坰":["jiong"],"坳":["ao"],"坷":["ke"],"坻":["chi","di"],"坼":["che"],"坽":["ling"],"垂":["chui"],"垃":["la"],"垄":["long"],"垆":["lu"],"垈":["dai"],"型":["xing"],"垌":["dong"],"垍":["ji"],"垎":["he"],"垏":["lv"],"垒":["lei"],"垓":["gai"],"垕":["hou"],"垙":["guang"],"垚":["yao"],"垛":["duo"],"垞":["cha"],"垟":["yang"],"垠":["yin"],"垡":["fa"],"垢":["gou"],"垣":["yuan"],"垤":["die"],"垦":["ken"],"垧":["shang"],"垩":["e"],"垫":["dian"],"垭":["ya"],"垮":["kua"],"垯":["da"],"垱":["dang"],"垲":["kai"],"垴":["nao"],"垵":["an"],"垸":["yuan"],"垺":["fu","pou"],"垾":["han"],"垿":["xu"],"埂":["geng"],"埃":["ai"],"埆":["que"],"埇":["yong"],"埋":["mai"],"埌":["lang"],"城":["cheng"],"埏":["shan","yan"],"埒":["lie"],"埔":["pu"],"埕":["cheng"],"埗":["bu"],"埘":["shi"],"埙":["xun"],"埚":["guo"],"埝":["nian"],"域":["yu"],"埠":["bu"],"埤":["pi"],"埪":["kong"],"埫":["chong"],"埭":["dai"],"埯":["an"],"埴":["zhi"],"埵":["duo"],"埸":["yi"],"培":["pei"],"基":["ji"],"埼":["qi"],"埽":["sao"],"堂":["tang"],"堃":["kun"],"堆":["dui"],"堇":["jin"],"堉":["yu"],"堋":["peng"],"堌":["gu"],"堍":["tu"],"堎":["leng"],"堐":["ya"],"堑":["qian"],"堕":["duo"],"堙":["yin"],"堞":["die"],"堠":["hou"],"堡":["bao"],"堤":["di"],"堧":["ruan"],"堨":["ye","e"],"堪":["kan"],"堰":["yan"],"堲":["ci","ji"],"堵":["du"],"堼":["heng","feng"],"堽":["gang"],"堾":["chun","chuan"],"塄":["leng"],"塅":["duan"],"塆":["wan"],"塌":["ta"],"塍":["cheng"],"塑":["su"],"塔":["ta"],"塘":["tang"],"塝":["bang"],"塞":["sai"],"塥":["ge"],"填":["tian"],"塬":["yuan"],"塱":["lang"],"塾":["shu"],"墀":["chi"],"墁":["man"],"境":["jing"],"墅":["shu"],"墈":["kan"],"墉":["yong"],"墐":["jin"],"墒":["shang"],"墓":["mu"],"墕":["yan"],"墘":["qian"],"墙":["qiang"],"墚":["liang"],"增":["zeng"],"墟":["xu"],"墡":["shan"],"墣":["pu"],"墦":["fan"],"墨":["mo"],"墩":["dun"],"墼":["ji"],"壁":["bi"],"壅":["yong"],"壑":["he"],"壕":["hao"],"壤":["rang"],"士":["shi"],"壬":["ren"],"壮":["zhuang"],"声":["sheng"],"壳":["ke"],"壶":["hu"],"壸":["kun"],"壹":["yi"],"处":["chu"],"备":["bei"],"复":["fu"],"夏":["xia"],"夐":["xiong"],"夔":["kui"],"夕":["xi"],"外":["wai"],"夙":["su"],"多":["duo"],"夜":["ye"],"够":["gou"],"夤":["yin"],"夥":["huo"],"大":["da"],"天":["tian"],"太":["tai"],"夫":["fu"],"夬":["guai"],"夭":["yao"],"央":["yang"],"夯":["hang"],"失":["shi"],"头":["tou"],"夷":["yi"],"夸":["kua"],"夹":["jia"],"夺":["duo"],"夼":["kuang"],"奁":["lian"],"奂":["huan"],"奄":["yan"],"奇":["qi"],"奈":["nai"],"奉":["feng"],"奋":["fen"],"奎":["kui"],"奏":["zou"],"契":["qi"],"奓":["zha","she"],"奔":["ben"],"奕":["yi"],"奖":["jiang"],"套":["tao"],"奘":["zang"],"奚":["xi"],"奠":["dian"],"奡":["ao"],"奢":["she"],"奥":["ao"],"奭":["shi"],"女":["nv"],"奴":["nu"],"奶":["nai"],"奸":["jian"],"她":["ta"],"好":["hao"],"妁":["shuo"],"如":["ru"],"妃":["fei"],"妄":["wang"],"妆":["zhuang"],"妇":["fu"],"妈":["ma"],"妊":["ren"],"妍":["yan"],"妒":["du"],"妓":["ji"],"妖":["yao"],"妗":["jin"],"妘":["yun"],"妙":["miao"],"妞":["niu"],"妣":["bi"],"妤":["yu"],"妥":["tuo"],"妧":["wan"],"妨":["fang"],"妩":["wu"],"妪":["yu"],"妫":["gui"],"妭":["ba"],"妮":["ni"],"妯":["zhou"],"妲":["da"],"妹":["mei"],"妻":["qi"],"妾":["qie"],"姆":["mu"],"姈":["ling"],"姊":["zi"],"始":["shi"],"姐":["jie"],"姑":["gu"],"姒":["si"],"姓":["xing"],"委":["wei"],"姗":["shan"],"姘":["pin"],"姚":["yao"],"姜":["jiang"],"姝":["shu"],"姞":["ji"],"姣":["jiao"],"姤":["gou"],"姥":["lao","mu"],"姨":["yi"],"姬":["ji"],"姮":["heng"],"姱":["kua"],"姶":["e"],"姹":["cha"],"姻":["yin"],"姽":["gui"],"姿":["zi"],"娀":["song"],"威":["wei"],"娃":["wa"],"娄":["lou"],"娅":["ya"],"娆":["rao"],"娇":["jiao"],"娈":["luan"],"娉":["ping"],"娌":["li"],"娑":["suo"],"娓":["wei"],"娘":["niang"],"娜":["na"],"娟":["juan"],"娠":["shen"],"娣":["di"],"娥":["e"],"娩":["mian"],"娱":["yu"],"娲":["wa"],"娴":["xian"],"娵":["ju"],"娶":["qu"],"娼":["chang"],"婀":["e"],"婆":["po"],"婉":["wan"],"婊":["biao"],"婌":["shu"],"婍":["qi"],"婕":["jie"],"婘":["quan"],"婚":["hun"],"婞":["xing"],"婠":["wan"],"婢":["bi"],"婤":["chou","zhou"],"婧":["jing"],"婪":["lan"],"婫":["kun","hun"],"婳":["hua"],"婴":["ying"],"婵":["chan"],"婶":["shen"],"婷":["ting"],"婺":["wu"],"婻":["nan"],"婼":["chuo","ruo"],"婿":["xu"],"媂":["di"],"媄":["mei"],"媆":["ruan"],"媒":["mei"],"媓":["huang"],"媖":["ying"],"媚":["mei"],"媛":["yuan"],"媞":["shi","ti"],"媪":["ao"],"媭":["xu"],"媱":["yao"],"媲":["pi"],"媳":["xi"],"媵":["ying"],"媸":["chi"],"媾":["gou"],"嫁":["jia"],"嫂":["sao"],"嫄":["yuan"],"嫉":["ji"],"嫌":["xian"],"嫒":["ai"],"嫔":["pin"],"嫕":["yi"],"嫖":["piao"],"嫘":["lei"],"嫚":["man"],"嫜":["zhang"],"嫠":["li"],"嫡":["di"],"嫣":["yan"],"嫦":["chang"],"嫩":["nen"],"嫪":["lao"],"嫫":["mo"],"嫭":["hu"],"嫱":["qiang"],"嫽":["liao"],"嬉":["xi"],"嬖":["bi"],"嬗":["shan"],"嬛":["huan"],"嬥":["tiao"],"嬬":["ru"],"嬴":["ying"],"嬷":["ma","mo"],"嬿":["yan"],"孀":["shuang"],"孅":["qian","xian"],"子":["zi"],"孑":["jie"],"孓":["jue"],"孔":["kong"],"孕":["yun"],"孖":["ma","zi"],"字":["zi"],"存":["cun"],"孙":["sun"],"孚":["fu"],"孛":["bei"],"孜":["zi"],"孝":["xiao"],"孟":["meng"],"孢":["bao"],"季":["ji"],"孤":["gu"],"孥":["nu"],"学":["xue"],"孩":["hai"],"孪":["luan"],"孬":["nao"],"孰":["shu"],"孱":["can","chan"],"孳":["zi"],"孵":["fu"],"孺":["ru"],"孽":["nie"],"宁":["ning"],"它":["ta"],"宄":["gui"],"宅":["zhai"],"宇":["yu"],"守":["shou"],"安":["an"],"宋":["song"],"完":["wan"],"宏":["hong"],"宓":["mi"],"宕":["dang"],"宗":["zong"],"官":["guan"],"宙":["zhou"],"定":["ding"],"宛":["wan"],"宜":["yi"],"宝":["bao"],"实":["shi"],"宠":["chong"],"审":["shen"],"客":["ke"],"宣":["xuan"],"室":["shi"],"宥":["you"],"宦":["huan"],"宧":["yi"],"宪":["xian"],"宫":["gong"],"宬":["cheng"],"宰":["zai"],"害":["hai"],"宴":["yan"],"宵":["xiao"],"家":["jia"],"宸":["chen"],"容":["rong"],"宽":["kuan"],"宾":["bin"],"宿":["su"],"寁":["zan"],"寂":["ji"],"寄":["ji"],"寅":["yin"],"密":["mi"],"寇":["kou"],"富":["fu"],"寐":["mei"],"寒":["han"],"寓":["yu"],"寝":["qin"],"寞":["mo"],"察":["cha"],"寡":["gua"],"寤":["wu"],"寥":["liao"],"寨":["zhai"],"寮":["liao"],"寰":["huan"],"寸":["cun"],"对":["dui"],"寺":["si"],"寻":["xun"],"导":["dao"],"寿":["shou"],"封":["feng"],"射":["she"],"将":["jiang"],"尉":["wei"],"尊":["zun"],"小":["xiao"],"少":["shao"],"尔":["er"],"尕":["ga"],"尖":["jian"],"尘":["chen"],"尚":["shang"],"尜":["ga"],"尝":["chang"],"尢":["you"],"尤":["you"],"尥":["liao"],"尧":["yao"],"尨":["mang","long"],"尪":["wang"],"尬":["ga"],"就":["jiu"],"尴":["gan"],"尸":["shi"],"尹":["yin"],"尺":["chi"],"尻":["kao"],"尼":["ni"],"尽":["jin"],"尾":["wei"],"尿":["niao"],"局":["ju"],"屁":["pi"],"层":["ceng"],"屃":["xi"],"居":["ju"],"屈":["qu"],"屉":["ti"],"届":["jie"],"屋":["wu"],"屎":["shi"],"屏":["ping"],"屐":["ji"],"屑":["xie"],"展":["zhan"],"屙":["e"],"属":["shu"],"屠":["tu"],"屡":["lv"],"屣":["xi"],"履":["lv"],"屦":["ju"],"屯":["tun"],"山":["shan"],"屹":["yi"],"屺":["qi"],"屼":["wu"],"屾":["shen"],"屿":["yu"],"岁":["sui"],"岂":["qi"],"岈":["ya"],"岊":["jie"],"岌":["ji"],"岍":["qian"],"岐":["qi"],"岑":["cen"],"岔":["cha"],"岖":["qu"],"岗":["gang"],"岘":["xian"],"岙":["ao"],"岚":["lan"],"岛":["dao"],"岜":["ba"],"岞":["zuo"],"岠":["ju"],"岢":["ke"],"岣":["gou"],"岨":["qu","ju"],"岩":["yan"],"岫":["xiu"],"岬":["jia"],"岭":["ling"],"岱":["dai"],"岳":["yue"],"岵":["hu"],"岷":["min"],"岸":["an"],"岽":["dong"],"岿":["kui"],"峁":["mao"],"峂":["tong"],"峃":["xue"],"峄":["yi"],"峋":["xun"],"峒":["dong","tong"],"峗":["wei"],"峘":["huan"],"峙":["zhi"],"峛":["li"],"峡":["xia"],"峣":["yao"],"峤":["jiao"],"峥":["zheng"],"峦":["luan"],"峧":["jiao"],"峨":["e"],"峪":["yu"],"峭":["qiao"],"峰":["feng"],"峱":["nao"],"峻":["jun"],"峿":["yu","wu"],"崀":["lang"],"崁":["kan"],"崂":["lao"],"崃":["lai"],"崄":["xian"],"崆":["kong"],"崇":["chong"],"崌":["ju"],"崎":["qi"],"崒":["zu"],"崔":["cui"],"崖":["ya"],"崚":["leng","ling"],"崛":["jue"],"崞":["guo"],"崟":["yin"],"崡":["han"],"崤":["xiao"],"崦":["yan"],"崧":["song"],"崩":["beng"],"崭":["zhan"],"崮":["gu"],"崴":["wai"],"崶":["feng"],"崽":["zai"],"崾":["yao"],"崿":["e"],"嵁":["kan"],"嵅":["han"],"嵇":["ji"],"嵊":["sheng"],"嵋":["mei"],"嵌":["qian"],"嵎":["yu"],"嵖":["cha"],"嵘":["rong"],"嵚":["qin"],"嵛":["yu"],"嵝":["lou"],"嵩":["song"],"嵫":["zi"],"嵬":["wei"],"嵯":["cuo"],"嵲":["nie"],"嵴":["ji"],"嶂":["zhang"],"嶅":["ao"],"嶍":["xi"],"嶒":["ceng"],"嶓":["bo"],"嶙":["lin"],"嶝":["deng"],"嶟":["zun"],"嶦":["zhan"],"嶲":["xi","gui"],"嶷":["yi"],"巅":["dian"],"巇":["xi"],"巉":["chan"],"巍":["wei"],"川":["chuan"],"州":["zhou"],"巡":["xun"],"巢":["chao"],"工":["gong"],"左":["zuo"],"巧":["qiao"],"巨":["ju"],"巩":["gong"],"巫":["wu"],"差":["cha"],"巯":["qiu"],"己":["ji"],"已":["yi"],"巳":["si"],"巴":["ba"],"巷":["xiang"],"巽":["xun"],"巾":["jin"],"币":["bi"],"市":["shi"],"布":["bu"],"帅":["shuai"],"帆":["fan"],"师":["shi"],"希":["xi"],"帏":["wei"],"帐":["zhang"],"帑":["tang"],"帔":["pei"],"帕":["pa"],"帖":["tie"],"帘":["lian"],"帙":["zhi"],"帚":["zhou"],"帛":["bo"],"帜":["zhi"],"帝":["di"],"帡":["ping"],"带":["dai"],"帧":["zhen"],"帨":["shui"],"席":["xi"],"帮":["bang"],"帱":["chou"],"帷":["wei"],"常":["chang"],"帻":["ze"],"帼":["guo"],"帽":["mao"],"幂":["mi"],"幄":["wo"],"幅":["fu"],"幌":["huang"],"幔":["man"],"幕":["mu"],"幖":["biao"],"幛":["zhang"],"幞":["fu"],"幡":["fan"],"幢":["chuang"],"幪":["meng"],"干":["gan"],"平":["ping"],"年":["nian"],"并":["bing"],"幸":["xing"],"幺":["yao"],"幻":["huan"],"幼":["you"],"幽":["you"],"广":["guang"],"庄":["zhuang"],"庆":["qing"],"庇":["bi"],"床":["chuang"],"庋":["gui"],"序":["xu"],"庐":["lu"],"庑":["wu"],"库":["ku"],"应":["ying"],"底":["di"],"庖":["pao"],"店":["dian"],"庙":["miao"],"庚":["geng"],"府":["fu"],"庞":["pang"],"废":["fei"],"庠":["xiang"],"庤":["zhi"],"庥":["xiu"],"度":["du"],"座":["zuo"],"庭":["ting"],"庱":["cheng"],"庳":["bi","bei"],"庵":["an"],"庶":["shu"],"康":["kang"],"庸":["yong"],"庹":["tuo"],"庼":["qing"],"庾":["yu"],"廆":["gui"],"廉":["lian"],"廊":["lang"],"廋":["sou"],"廑":["jin"],"廒":["ao"],"廓":["kuo"],"廖":["liao"],"廙":["yi"],"廛":["chan"],"廨":["xie"],"廪":["lin"],"延":["yan"],"廷":["ting"],"建":["jian"],"廿":["nian"],"开":["kai"],"弁":["bian"],"异":["yi"],"弃":["qi"],"弄":["nong"],"弆":["ju"],"弇":["yan"],"弈":["yi"],"弊":["bi"],"弋":["yi"],"式":["shi"],"弑":["shi"],"弓":["gong"],"引":["yin"],"弗":["fu"],"弘":["hong"],"弛":["chi"],"弟":["di"],"张":["zhang"],"弢":["tao"],"弥":["mi"],"弦":["xian"],"弧":["hu"],"弨":["chao"],"弩":["nu"],"弭":["mi"],"弯":["wan"],"弱":["ruo"],"弶":["jiang"],"弸":["peng"],"弹":["dan"],"强":["qiang"],"弼":["bi"],"彀":["gou"],"归":["gui"],"当":["dang"],"录":["lu"],"彖":["tuan"],"彗":["hui"],"彘":["zhi"],"彝":["yi"],"彟":["yue","huo"],"形":["xing"],"彤":["tong"],"彦":["yan"],"彧":["yu"],"彩":["cai"],"彪":["biao"],"彬":["bin"],"彭":["peng"],"彰":["zhang"],"影":["ying"],"彳":["chi"],"彷":["pang"],"役":["yi"],"彻":["che"],"彼":["bi"],"往":["wang"],"征":["zheng"],"徂":["cu"],"径":["jing"],"待":["dai"],"徇":["xun"],"很":["hen"],"徉":["yang"],"徊":["huai"],"律":["lv"],"徐":["xu"],"徒":["tu"],"徕":["lai"],"得":["de"],"徘":["pai"],"徙":["xi"],"徛":["ji"],"徜":["chang"],"御":["yu"],"徨":["huang"],"循":["xun"],"徭":["yao"],"微":["wei"],"徵":["zheng","zhi"],"德":["de"],"徼":["jiao"],"徽":["hui"],"心":["xin"],"必":["bi"],"忆":["yi"],"忉":["dao"],"忌":["ji"],"忍":["ren"],"忏":["chan"],"忐":["tan"],"忑":["te"],"忒":["te"],"忖":["cun"],"志":["zhi"],"忘":["wang"],"忙":["mang"],"忝":["tian"],"忞":["min"],"忠":["zhong"],"忡":["chong"],"忤":["wu"],"忧":["you"],"忪":["song"],"快":["kuai"],"忭":["bian"],"忮":["zhi"],"忱":["chen"],"忳":["tun"],"念":["nian"],"忸":["niu"],"忺":["xian"],"忻":["xin"],"忽":["hu"],"忾":["kai"],"忿":["fen"],"怀":["huai"],"态":["tai"],"怂":["song"],"怃":["wu"],"怄":["ou"],"怅":["chang"],"怆":["chuang"],"怊":["chao"],"怍":["zuo"],"怎":["zen"],"怏":["yang"],"怒":["nu"],"怔":["zheng"],"怕":["pa"],"怖":["bu"],"怙":["hu"],"怛":["da"],"怜":["lian"],"思":["si"],"怠":["dai"],"怡":["yi"],"急":["ji"],"怦":["peng"],"性":["xing"],"怨":["yuan"],"怩":["ni"],"怪":["guai"],"怫":["fu"],"怯":["qie"],"怵":["chu"],"总":["zong"],"怼":["dui"],"怿":["yi"],"恁":["nen"],"恂":["xun"],"恃":["shi"],"恋":["lian"],"恍":["huang"],"恐":["kong"],"恒":["heng"],"恓":["xi"],"恔":["jiao","xiao"],"恕":["shu"],"恙":["yang"],"恚":["hui"],"恝":["jia"],"恢":["hui"],"恣":["zi"],"恤":["xu"],"恧":["nv"],"恨":["hen"],"恩":["en"],"恪":["ke"],"恫":["dong"],"恬":["tian"],"恭":["gong"],"息":["xi"],"恰":["qia"],"恳":["ken"],"恶":["e"],"恸":["tong"],"恹":["yan"],"恺":["kai"],"恻":["ce"],"恼":["nao"],"恽":["yun"],"恿":["yong"],"悃":["kun"],"悄":["qiao"],"悆":["yu"],"悈":["jie"],"悉":["xi"],"悌":["ti"],"悍":["han"],"悒":["yi"],"悔":["hui"],"悖":["bei"],"悚":["song"],"悛":["quan"],"悝":["kui"],"悟":["wu"],"悠":["you"],"悢":["liang"],"患":["huan"],"悦":["yue"],"您":["nin"],"悫":["que"],"悬":["xuan"],"悭":["qian"],"悯":["min"],"悰":["cong"],"悱":["fei"],"悲":["bei"],"悴":["cui"],"悸":["ji"],"悻":["xing"],"悼":["dao"],"情":["qing"],"惆":["chou"],"惇":["dun"],"惊":["jing"],"惋":["wan"],"惎":["ji"],"惑":["huo"],"惔":["tan"],"惕":["ti"],"惘":["wang"],"惙":["chuo"],"惚":["hu"],"惛":["hun"],"惜":["xi"],"惝":["chang"],"惟":["wei"],"惠":["hui"],"惦":["dian"],"惧":["ju"],"惨":["can"],"惩":["cheng"],"惫":["bei"],"惬":["qie"],"惭":["can"],"惮":["dan"],"惯":["guan"],"惰":["duo"],"想":["xiang"],"惴":["zhui"],"惶":["huang"],"惹":["re"],"惺":["xing"],"愀":["qiao"],"愁":["chou"],"愃":["xuan"],"愆":["qian"],"愈":["yu"],"愉":["yu"],"愍":["min"],"愎":["bi"],"意":["yi"],"愐":["mian"],"愔":["yin"],"愕":["e"],"愚":["yu"],"感":["gan"],"愠":["yun"],"愣":["leng"],"愤":["fen"],"愦":["kui"],"愧":["kui"],"愫":["su"],"愭":["qi"],"愿":["yuan"],"慆":["tao"],"慈":["ci"],"慊":["qian"],"慌":["huang"],"慎":["shen"],"慑":["she"],"慕":["mu"],"慝":["te"],"慢":["man"],"慥":["zao"],"慧":["hui"],"慨":["kai"],"慬":["qin"],"慭":["yin"],"慰":["wei"],"慵":["yong"],"慷":["kang"],"憋":["bie"],"憎":["zeng"],"憔":["qiao"],"憕":["cheng"],"憙":["xi"],"憧":["chong"],"憨":["han"],"憩":["qi"],"憬":["jing"],"憭":["liao"],"憷":["chu"],"憺":["dan"],"憾":["han"],"懂":["dong"],"懈":["xie"],"懊":["ao"],"懋":["mao"],"懑":["men"],"懒":["lan"],"懔":["lin"],"懦":["nuo"],"懵":["meng"],"懿":["yi"],"戆":["gang"],"戈":["ge"],"戊":["wu"],"戋":["jian"],"戌":["xu"],"戍":["shu"],"戎":["rong"],"戏":["xi"],"成":["cheng"],"我":["wo"],"戒":["jie"],"戕":["qiang"],"或":["huo"],"戗":["qiang"],"战":["zhan"],"戚":["qi"],"戛":["jia"],"戟":["ji"],"戡":["kan"],"戢":["ji"],"戣":["kui"],"戤":["gai"],"戥":["deng"],"截":["jie"],"戬":["jian"],"戭":["yan"],"戮":["lu"],"戳":["chuo"],"戴":["dai"],"户":["hu"],"戽":["hu"],"戾":["li"],"房":["fang"],"所":["suo"],"扁":["bian"],"扂":["dian"],"扃":["jiong"],"扅":["yi"],"扆":["yi"],"扇":["shan"],"扈":["hu"],"扉":["fei"],"扊":["yan"],"手":["shou"],"才":["cai"],"扎":["zha","za"],"扑":["pu"],"扒":["ba"],"打":["da"],"扔":["reng"],"托":["tuo"],"扛":["kang"],"扞":["gan","han"],"扣":["kou"],"扦":["qian"],"执":["zhi"],"扩":["kuo"],"扪":["men"],"扫":["sao"],"扬":["yang"],"扭":["niu"],"扮":["ban"],"扯":["che"],"扰":["rao"],"扳":["ban"],"扶":["fu"],"批":["pi"],"扺":["zhi"],"扼":["e"],"扽":["den"],"找":["zhao"],"承":["cheng"],"技":["ji"],"抃":["bian"],"抄":["chao"],"抉":["jue"],"把":["ba"],"抑":["yi"],"抒":["shu"],"抓":["zhua"],"抔":["pou"],"投":["tou"],"抖":["dou"],"抗":["kang"],"折":["zhe"],"抚":["fu"],"抛":["pao"],"抟":["tuan"],"抠":["kou"],"抡":["lun"],"抢":["qiang"],"护":["hu"],"报":["bao"],"抨":["peng"],"披":["pi"],"抬":["tai"],"抱":["bao"],"抵":["di"],"抹":["mo"],"抻":["chen"],"押":["ya"],"抽":["chou"],"抿":["min"],"拂":["fu"],"拃":["zha"],"拄":["zhu"],"担":["dan"],"拆":["chai"],"拇":["mu"],"拈":["nian"],"拉":["la"],"拊":["fu"],"拌":["ban"],"拍":["pai"],"拎":["lin"],"拐":["guai"],"拒":["ju"],"拓":["tuo"],"拔":["ba"],"拖":["tuo"],"拗":["ao"],"拘":["ju"],"拙":["zhuo"],"招":["zhao"],"拜":["bai"],"拟":["ni"],"拢":["long"],"拣":["jian"],"拤":["qia"],"拥":["yong"],"拦":["lan"],"拧":["ning"],"拨":["bo"],"择":["ze"],"括":["kuo"],"拭":["shi"],"拮":["jie"],"拯":["zheng"],"拱":["gong"],"拳":["quan"],"拴":["shuan"],"拶":["za"],"拷":["kao"],"拼":["pin"],"拽":["zhuai"],"拾":["shi"],"拿":["na"],"持":["chi"],"挂":["gua"],"指":["zhi"],"挈":["qie"],"按":["an"],"挎":["kua"],"挑":["tiao"],"挓":["zha"],"挖":["wa"],"挚":["zhi"],"挛":["luan"],"挝":["wo","zhua"],"挞":["ta"],"挟":["xie"],"挠":["nao"],"挡":["dang"],"挣":["zheng"],"挤":["ji"],"挥":["hui"],"挦":["xian"],"挨":["ai"],"挪":["nuo"],"挫":["cuo"],"振":["zhen"],"挲":["sa","suo"],"挹":["yi"],"挺":["ting"],"挽":["wan"],"捂":["wu"],"捃":["jun"],"捅":["tong"],"捆":["kun"],"捉":["zhuo"],"捋":["lv","luo"],"捌":["ba"],"捍":["han"],"捎":["shao"],"捏":["nie"],"捐":["juan"],"捕":["bu"],"捞":["lao"],"损":["sun"],"捡":["jian"],"换":["huan"],"捣":["dao"],"捧":["peng"],"捩":["lie"],"捭":["bai"],"据":["ju"],"捯":["dao"],"捶":["chui"],"捷":["jie"],"捺":["na"],"捻":["nian"],"捽":["zuo"],"掀":["xian"],"掂":["dian"],"掇":["duo"],"授":["shou"],"掉":["diao"],"掊":["pou"],"掌":["zhang"],"掎":["ji"],"掏":["tao"],"掐":["qia"],"排":["pai"],"掖":["ye"],"掘":["jue"],"掞":["shan"],"掠":["lüe"],"探":["tan"],"掣":["che"],"接":["jie"],"控":["kong"],"推":["tui"],"掩":["yan"],"措":["cuo"],"掬":["ju"],"掭":["tian"],"掮":["qian"],"掰":["bai"],"掳":["lu"],"掴":["guai","guo"],"掷":["zhi"],"掸":["dan"],"掺":["can","chan"],"掼":["guan"],"掾":["yuan"],"揄":["yu"],"揆":["kui"],"揉":["rou"],"揍":["zou"],"描":["miao"],"提":["ti"],"插":["cha"],"揕":["zhen"],"揖":["yi"],"揠":["ya"],"握":["wo"],"揣":["chuai"],"揩":["kai"],"揪":["jiu"],"揭":["jie"],"揳":["xie"],"援":["yuan"],"揶":["ye"],"揸":["zha"],"揽":["lan"],"揿":["qin"],"搀":["chan"],"搁":["ge"],"搂":["lou"],"搅":["jiao"],"搋":["chuai"],"搌":["zhan"],"搏":["bo"],"搐":["chu"],"搒":["bang"],"搓":["cuo"],"搔":["sao"],"搛":["jian"],"搜":["sou"],"搞":["gao"],"搠":["shuo"],"搡":["sang"],"搦":["nuo"],"搪":["tang"],"搬":["ban"],"搭":["da"],"搴":["qian"],"携":["xie"],"搽":["cha"],"摁":["en"],"摄":["she"],"摅":["shu"],"摆":["bai"],"摇":["yao"],"摈":["bin"],"摊":["tan"],"摏":["chong"],"摒":["bing"],"摔":["shuai"],"摘":["zhai"],"摛":["chi"],"摞":["luo"],"摧":["cui"],"摩":["mo"],"摭":["zhi"],"摴":["chu"],"摸":["mo"],"摹":["mo"],"摽":["biao"],"撂":["liao"],"撄":["ying"],"撅":["jue"],"撇":["pie"],"撑":["cheng"],"撒":["sa"],"撕":["si"],"撖":["han"],"撙":["zun"],"撞":["zhuang"],"撤":["che"],"撩":["liao"],"撬":["qiao"],"播":["bo"],"撮":["cuo"],"撰":["zhuan"],"撵":["nian"],"撷":["xie"],"撸":["lu"],"撺":["cuan"],"撼":["han"],"擀":["gan"],"擂":["lei"],"擅":["shan"],"操":["cao"],"擎":["qing"],"擐":["huan"],"擒":["qin"],"擘":["bai","bo"],"擞":["sou"],"擢":["zhuo"],"擤":["xing"],"擦":["ca"],"擿":["ti"],"攀":["pan"],"攉":["huo"],"攒":["zan"],"攘":["rang"],"攥":["zuan"],"攫":["jue"],"攮":["nang"],"支":["zhi"],"收":["shou"],"攸":["you"],"改":["gai"],"攻":["gong"],"攽":["ban"],"放":["fang"],"政":["zheng"],"故":["gu"],"效":["xiao"],"敉":["mi"],"敌":["di"],"敏":["min"],"救":["jiu"],"敔":["yu"],"敕":["chi"],"敖":["ao"],"教":["jiao"],"敛":["lian"],"敝":["bi"],"敞":["chang"],"敢":["gan"],"散":["san"],"敦":["dun"],"敩":["xiao"],"敫":["jiao"],"敬":["jing"],"数":["shu"],"敲":["qiao"],"整":["zheng"],"敷":["fu"],"文":["wen"],"斋":["zhai"],"斌":["bin"],"斐":["fei"],"斑":["ban"],"斓":["lan"],"斗":["dou"],"料":["liao"],"斛":["hu"],"斜":["xie"],"斝":["jia"],"斟":["zhen"],"斠":["jiao"],"斡":["wo"],"斤":["jin"],"斥":["chi"],"斧":["fu"],"斩":["zhan"],"斫":["zhuo"],"断":["duan"],"斯":["si"],"新":["xin"],"斶":["chu"],"方":["fang"],"於":["yu"],"施":["shi"],"旁":["pang"],"旃":["zhan"],"旄":["mao"],"旅":["lv"],"旆":["pei"],"旋":["xuan"],"旌":["jing"],"旎":["ni"],"族":["zu"],"旐":["zhao"],"旒":["liu"],"旖":["yi"],"旗":["qi"],"旞":["sui"],"无":["wu"],"既":["ji"],"日":["ri"],"旦":["dan"],"旧":["jiu"],"旨":["zhi"],"早":["zao"],"旬":["xun"],"旭":["xu"],"旮":["ga"],"旯":["la"],"旰":["gan"],"旱":["han"],"旴":["xu"],"旵":["chan"],"时":["shi"],"旷":["kuang"],"旸":["yang"],"旺":["wang"],"旻":["min"],"旿":["wu"],"昀":["yun"],"昂":["ang"],"昃":["ze"],"昄":["ban"],"昆":["kun"],"昇":["sheng"],"昈":["hu"],"昉":["fang"],"昊":["hao"],"昌":["chang"],"明":["ming"],"昏":["hun"],"昒":["hu"],"易":["yi"],"昔":["xi"],"昕":["xin"],"昙":["tan"],"昝":["zan"],"星":["xing"],"映":["ying"],"昡":["xuan"],"昣":["zhen"],"昤":["ling"],"春":["chun"],"昧":["mei"],"昨":["zuo"],"昪":["bian"],"昫":["xu"],"昭":["zhao"],"是":["shi"],"昱":["yu"],"昳":["die"],"昴":["mao"],"昵":["ni"],"昶":["chang"],"昺":["bing"],"昼":["zhou"],"昽":["long"],"显":["xian"],"晁":["chao"],"晃":["huang"],"晅":["xuan"],"晊":["zhi"],"晋":["jin"],"晌":["shang"],"晏":["yan"],"晐":["gai"],"晒":["shai"],"晓":["xiao"],"晔":["ye"],"晕":["yun"],"晖":["hui"],"晗":["han"],"晙":["jun"],"晚":["wan"],"晞":["xi"],"晟":["cheng","sheng"],"晡":["bu"],"晢":["zhe"],"晤":["wu"],"晦":["hui"],"晨":["chen"],"晪":["tian"],"晫":["zhuo"],"普":["pu"],"景":["jing"],"晰":["xi"],"晱":["shan"],"晴":["qing"],"晶":["jing"],"晷":["gui"],"智":["zhi"],"晾":["liang"],"暂":["zan"],"暄":["xuan"],"暅":["geng","xuan"],"暇":["xia"],"暌":["kui"],"暑":["shu"],"暕":["jian"],"暖":["nuan"],"暗":["an"],"暝":["ming"],"暧":["ai"],"暨":["ji"],"暮":["mu"],"暲":["zhang"],"暴":["bao"],"暵":["han"],"暶":["xuan"],"暹":["xian"],"暾":["tun"],"暿":["xi"],"曈":["tong"],"曌":["zhao"],"曙":["shu"],"曛":["xun"],"曜":["yao"],"曝":["pu"],"曦":["xi"],"曩":["nang"],"曰":["yue"],"曲":["qu"],"曳":["ye"],"更":["geng"],"曷":["he"],"曹":["cao"],"曼":["man"],"曾":["ceng","zeng"],"替":["ti"],"最":["zui"],"月":["yue"],"有":["you"],"朋":["peng"],"服":["fu"],"朏":["fei"],"朐":["qu"],"朓":["tiao"],"朔":["shuo"],"朕":["zhen"],"朗":["lang"],"望":["wang"],"朝":["chao","zhao"],"期":["qi"],"朦":["meng"],"木":["mu"],"未":["wei"],"末":["mo"],"本":["ben"],"札":["zha"],"术":["shu"],"朱":["zhu"],"朳":["ba"],"朴":["pu"],"朵":["duo"],"朸":["li"],"机":["ji"],"朽":["xiu"],"杀":["sha"],"杂":["za"],"权":["quan"],"杄":["qian"],"杆":["gan"],"杈":["cha"],"杉":["shan"],"杌":["wu"],"李":["li"],"杏":["xing"],"材":["cai"],"村":["cun"],"杓":["biao","shao"],"杕":["di"],"杖":["zhang"],"杙":["yi"],"杜":["du"],"杞":["qi"],"束":["shu"],"杠":["gang"],"条":["tiao"],"来":["lai"],"杧":["mang"],"杨":["yang"],"杩":["ma"],"杪":["miao"],"杭":["hang"],"杯":["bei"],"杰":["jie"],"杲":["gao"],"杳":["yao"],"杵":["chu"],"杷":["pa"],"杻":["chou","niu"],"杼":["zhu"],"松":["song"],"板":["ban"],"极":["ji"],"构":["gou"],"枅":["ji"],"枇":["pi"],"枉":["wang"],"枋":["fang"],"枍":["yi"],"析":["xi"],"枕":["zhen"],"林":["lin"],"枘":["rui"],"枚":["mei"],"果":["guo"],"枝":["zhi"],"枞":["cong"],"枢":["shu"],"枣":["zao"],"枥":["li"],"枧":["jian"],"枨":["cheng"],"枪":["qiang"],"枫":["feng"],"枭":["xiao"],"枯":["ku"],"枰":["ping"],"枲":["xi"],"枳":["zhi"],"枵":["xiao"],"架":["jia"],"枷":["jia"],"枸":["gou","ju"],"枹":["bao"],"柁":["duo","tuo"],"柃":["ling"],"柄":["bing"],"柈":["ban","pan"],"柊":["zhong"],"柏":["bai"],"某":["mou"],"柑":["gan"],"柒":["qi"],"染":["ran"],"柔":["rou"],"柖":["shao"],"柘":["zhe"],"柙":["xia"],"柚":["you"],"柜":["gui"],"柝":["tuo"],"柞":["zha","zuo"],"柠":["ning"],"柢":["di"],"查":["cha"],"柩":["jiu"],"柬":["jian"],"柯":["ke"],"柰":["nai"],"柱":["zhu"],"柳":["liu"],"柴":["chai"],"柷":["chu","zhu"],"柽":["cheng"],"柿":["shi"],"栀":["zhi"],"栅":["zha"],"标":["biao"],"栈":["zhan"],"栉":["zhi"],"栊":["long"],"栋":["dong"],"栌":["lu"],"栎":["li"],"栏":["lan"],"栐":["yong"],"树":["shu"],"栒":["xun"],"栓":["shuan"],"栖":["qi"],"栗":["li"],"栝":["gua"],"栟":["ben"],"校":["xiao"],"栩":["xu"],"株":["zhu"],"栲":["kao"],"栳":["lao"],"栴":["zhan"],"样":["yang"],"核":["he"],"根":["gen"],"栻":["shi"],"格":["ge"],"栽":["zai"],"栾":["luan"],"桀":["jie"],"桁":["heng"],"桂":["gui"],"桃":["tao"],"桄":["guang"],"桅":["wei"],"框":["kuang"],"案":["an"],"桉":["an"],"桊":["juan"],"桌":["zhuo"],"桎":["zhi"],"桐":["tong"],"桑":["sang"],"桓":["huan"],"桔":["ju","jie"],"桕":["jiu"],"桠":["ya"],"桡":["rao"],"桢":["zhen"],"档":["dang"],"桤":["qi"],"桥":["qiao"],"桦":["hua"],"桧":["gui"],"桨":["jiang"],"桩":["zhuang"],"桫":["suo"],"桯":["ting"],"桲":["po","bo"],"桴":["fu"],"桶":["tong"],"桷":["jue"],"桹":["lang"],"梁":["liang"],"梃":["ting"],"梅":["mei"],"梆":["bang"],"梌":["tu"],"梏":["gu"],"梓":["zi"],"梗":["geng"],"梠":["lv"],"梢":["shao"],"梣":["cen","chen"],"梦":["meng"],"梧":["wu"],"梨":["li"],"梭":["suo"],"梯":["ti"],"械":["xie"],"梳":["shu"],"梴":["chan"],"梵":["fan"],"梼":["tao","chou"],"梽":["zhi"],"梾":["lai"],"梿":["lian"],"检":["jian"],"棁":["zhuo"],"棂":["ling"],"棉":["mian"],"棋":["qi"],"棍":["gun"],"棐":["fei"],"棒":["bang"],"棓":["bang"],"棕":["zong"],"棘":["ji"],"棚":["peng"],"棠":["tang"],"棣":["di"],"棤":["cuo","que"],"棨":["qi"],"棪":["yan"],"棫":["yu"],"棬":["quan"],"森":["sen"],"棰":["chui"],"棱":["leng"],"棵":["ke"],"棹":["zhao"],"棺":["guan"],"棻":["fen"],"棼":["fen"],"棽":["shen","chen"],"椀":["wan"],"椁":["guo"],"椅":["yi"],"椆":["chou"],"椋":["liang"],"植":["zhi"],"椎":["chui","zhui"],"椐":["ju"],"椑":["bei"],"椒":["jiao"],"椓":["zhuo"],"椟":["du"],"椠":["qian"],"椤":["luo"],"椪":["peng"],"椭":["tuo"],"椰":["ye"],"椴":["duan"],"椸":["yi"],"椹":["shen","zhen"],"椽":["chuan"],"椿":["chun"],"楂":["zha"],"楒":["si"],"楔":["xie"],"楗":["jian"],"楙":["mao"],"楚":["chu"],"楝":["lian"],"楞":["leng"],"楠":["nan"],"楣":["mei"],"楦":["xuan"],"楩":["pian"],"楪":["ye","die"],"楫":["ji"],"楮":["chu"],"楯":["dun","shun"],"楷":["kai"],"楸":["qiu"],"楹":["ying"],"楼":["lou"],"概":["gai"],"榃":["tan"],"榄":["lan"],"榅":["wen"],"榆":["yu"],"榇":["chen"],"榈":["lv"],"榉":["ju"],"榍":["xie"],"榑":["fu"],"榔":["lang"],"榕":["rong"],"榖":["gu"],"榛":["zhen"],"榜":["bang"],"榧":["fei"],"榨":["zha"],"榫":["sun"],"榭":["xie"],"榰":["zhi"],"榱":["cui"],"榴":["liu"],"榷":["que"],"榻":["ta"],"槁":["gao"],"槃":["pan"],"槊":["shuo"],"槌":["chui"],"槎":["cha"],"槐":["huai"],"槔":["gao"],"槚":["jia"],"槛":["kan","jian"],"槜":["zui"],"槟":["bin"],"槠":["zhu"],"槭":["qi"],"槱":["you"],"槲":["hu"],"槽":["cao"],"槿":["jin"],"樊":["fan"],"樗":["chu"],"樘":["tang"],"樟":["zhang"],"模":["mo"],"樨":["xi"],"横":["heng"],"樯":["qiang"],"樱":["ying"],"樵":["qiao"],"樽":["zun"],"樾":["yue"],"橄":["gan"],"橇":["qiao"],"橐":["tuo"],"橑":["lao","liao"],"橘":["ju"],"橙":["cheng"],"橛":["jue"],"橞":["hui"],"橡":["xiang"],"橥":["zhu"],"橦":["tong"],"橱":["chu"],"橹":["lu"],"橼":["yuan"],"檀":["tan"],"檄":["xi"],"檎":["qin"],"檐":["yan"],"檑":["lei"],"檗":["bo"],"檞":["jie"],"檠":["qing"],"檩":["lin"],"檫":["cha"],"檬":["meng"],"櫆":["kui"],"欂":["bo"],"欠":["qian"],"次":["ci"],"欢":["huan"],"欣":["xin"],"欤":["yu"],"欧":["ou"],"欲":["yu"],"欸":["ai","ei"],"欹":["yi","qi"],"欺":["qi"],"欻":["chua","xu"],"款":["kuan"],"歃":["sha"],"歅":["yin","yan"],"歆":["xin"],"歇":["xie"],"歉":["qian"],"歌":["ge"],"歙":["she","xi"],"止":["zhi"],"正":["zheng"],"此":["ci"],"步":["bu"],"武":["wu"],"歧":["qi"],"歪":["wai"],"歹":["dai"],"死":["si"],"歼":["jian"],"殁":["mo"],"殂":["cu"],"殃":["yang"],"殄":["tian"],"殆":["dai"],"殇":["shang"],"殉":["xun"],"殊":["shu"],"残":["can"],"殍":["piao"],"殒":["yun"],"殓":["lian"],"殖":["zhi"],"殚":["dan"],"殛":["ji"],"殡":["bin"],"殣":["jin"],"殪":["yi"],"殳":["shu"],"殴":["ou"],"段":["duan"],"殷":["yin"],"殿":["dian"],"毁":["hui"],"毂":["gu"],"毅":["yi"],"毋":["wu"],"毌":["guan"],"母":["mu"],"每":["mei"],"毐":["ai"],"毒":["du"],"毓":["yu"],"比":["bi"],"毕":["bi"],"毖":["bi"],"毗":["pi"],"毙":["bi"],"毛":["mao"],"毡":["zhan"],"毪":["mu"],"毫":["hao"],"毯":["tan"],"毳":["cui"],"毵":["san"],"毹":["shu"],"毽":["jian"],"氅":["chang"],"氆":["pu"],"氇":["lu"],"氍":["qu"],"氏":["shi"],"氐":["di"],"民":["min"],"氓":["mang","meng"],"气":["qi"],"氕":["pie"],"氖":["nai"],"氘":["dao"],"氙":["xian"],"氚":["chuan"],"氛":["fen"],"氟":["fu"],"氡":["dong"],"氢":["qing"],"氤":["yin"],"氦":["hai"],"氧":["yang"],"氨":["an"],"氩":["ya"],"氪":["ke"],"氮":["dan"],"氯":["lv"],"氰":["qing"],"氲":["yun"],"水":["shui"],"永":["yong"],"氾":["fan"],"氿":["gui"],"汀":["ting"],"汁":["zhi"],"求":["qiu"],"汆":["cuan"],"汇":["hui"],"汈":["diao"],"汉":["han"],"汊":["cha"],"汋":["zhuo"],"汐":["xi"],"汔":["qi"],"汕":["shan"],"汗":["han"],"汛":["xun"],"汜":["si"],"汝":["ru"],"汞":["gong"],"江":["jiang"],"池":["chi"],"污":["wu"],"汤":["tang"],"汧":["qian"],"汨":["mi"],"汩":["gu"],"汪":["wang"],"汫":["jing"],"汭":["rui"],"汰":["tai"],"汲":["ji"],"汴":["bian"],"汶":["wen"],"汹":["xiong"],"汽":["qi"],"汾":["fen"],"沁":["qin"],"沂":["yi"],"沃":["wo"],"沄":["yun"],"沅":["yuan"],"沆":["hang"],"沇":["yan"],"沈":["shen"],"沉":["chen"],"沌":["dun"],"沏":["qi"],"沐":["mu"],"沓":["da","ta"],"沔":["mian"],"沘":["bi"],"沙":["sha"],"沚":["zhi"],"沛":["pei"],"沟":["gou"],"没":["mei"],"沣":["feng"],"沤":["ou"],"沥":["li"],"沦":["lun"],"沧":["cang"],"沨":["feng"],"沩":["wei"],"沪":["hu"],"沫":["mo"],"沭":["shu"],"沮":["ju"],"沱":["tuo"],"河":["he"],"沸":["fei"],"油":["you"],"沺":["tian"],"治":["zhi"],"沼":["zhao"],"沽":["gu"],"沾":["zhan"],"沿":["yan"],"泂":["jiong"],"泃":["ju"],"泄":["xie"],"泅":["qiu"],"泇":["jia"],"泉":["quan"],"泊":["po","bo"],"泌":["mi"],"泐":["le"],"泓":["hong"],"泔":["gan"],"法":["fa"],"泖":["mao"],"泗":["si"],"泙":["ping","peng"],"泚":["ci"],"泛":["fan"],"泜":["zhi"],"泞":["ning"],"泠":["ling"],"泡":["pao"],"波":["bo"],"泣":["qi"],"泥":["ni"],"注":["zhu"],"泪":["lei"],"泫":["xuan"],"泮":["pan"],"泯":["min"],"泰":["tai"],"泱":["yang"],"泳":["yong"],"泵":["beng"],"泷":["long"],"泸":["lu"],"泺":["luo"],"泻":["xie"],"泼":["po"],"泽":["ze"],"泾":["jing"],"洁":["jie"],"洄":["hui"],"洇":["yin"],"洈":["wei"],"洋":["yang"],"洌":["lie"],"洎":["ji"],"洑":["fu"],"洒":["sa"],"洓":["se"],"洗":["xi"],"洘":["kao"],"洙":["zhu"],"洚":["jiang"],"洛":["luo"],"洞":["dong"],"洢":["yi"],"洣":["mi"],"津":["jin"],"洧":["wei"],"洨":["xiao"],"洪":["hong"],"洫":["xu"],"洭":["kuang"],"洮":["tao"],"洱":["er"],"洲":["zhou"],"洳":["ru"],"洴":["ping"],"洵":["xun"],"洸":["guang"],"洹":["huan"],"洺":["ming"],"活":["huo"],"洼":["wa"],"洽":["qia"],"派":["pai"],"洿":["wu"],"流":["liu"],"浃":["jia"],"浅":["qian"],"浆":["jiang"],"浇":["jiao"],"浈":["zhen"],"浉":["shi"],"浊":["zhuo"],"测":["ce"],"浍":["hui","kuai"],"济":["ji"],"浏":["liu"],"浐":["chan"],"浑":["hun"],"浒":["hu"],"浓":["nong"],"浔":["xun"],"浕":["jin"],"浙":["zhe"],"浚":["jun"],"浛":["han"],"浜":["bang"],"浞":["zhuo"],"浟":["you"],"浠":["xi"],"浡":["bo"],"浣":["huan"],"浥":["yi"],"浦":["pu"],"浩":["hao"],"浪":["lang"],"浬":["li"],"浭":["geng"],"浮":["fu"],"浯":["wu"],"浰":["lian","li"],"浲":["feng"],"浴":["yu"],"海":["hai"],"浸":["jin"],"浼":["mei"],"涂":["tu"],"涄":["ping"],"涅":["nie"],"消":["xiao"],"涉":["she"],"涌":["yong"],"涍":["xiao"],"涎":["xian"],"涐":["e"],"涑":["su"],"涓":["juan"],"涔":["cen"],"涕":["ti"],"涘":["si"],"涛":["tao"],"涝":["lao"],"涞":["lai"],"涟":["lian"],"涠":["wei"],"涡":["wo"],"涢":["yun"],"涣":["huan"],"涤":["di"],"润":["run"],"涧":["jian"],"涨":["zhang"],"涩":["se"],"涪":["fu"],"涫":["guan"],"涮":["shuan"],"涯":["ya"],"液":["ye"],"涴":["wo","wan"],"涵":["han"],"涸":["he"],"涿":["zhuo"],"淀":["dian"],"淄":["zi"],"淅":["xi"],"淆":["xiao"],"淇":["qi"],"淋":["lin"],"淌":["tang"],"淏":["hao"],"淑":["shu"],"淖":["nao"],"淘":["tao"],"淙":["cong"],"淜":["ping"],"淝":["fei"],"淞":["song"],"淟":["tian"],"淠":["pi"],"淡":["dan"],"淤":["yu"],"淦":["gan"],"淫":["yin"],"淬":["cui"],"淮":["huai"],"淯":["yu"],"深":["shen"],"淳":["chun"],"淴":["hu"],"混":["hun"],"淹":["yan"],"添":["tian"],"淼":["miao"],"清":["qing"],"渊":["yuan"],"渌":["lu"],"渍":["zi"],"渎":["du"],"渐":["jian"],"渑":["mian"],"渔":["yu"],"渗":["shen"],"渚":["zhu"],"渝":["yu"],"渟":["ting"],"渠":["qu"],"渡":["du"],"渣":["zha"],"渤":["bo"],"渥":["wo"],"温":["wen"],"渫":["xie"],"渭":["wei"],"港":["gang"],"渰":["yan"],"渲":["xuan"],"渴":["ke"],"游":["you"],"渺":["miao"],"渼":["mei"],"湃":["pai"],"湄":["mei"],"湉":["tian"],"湍":["tuan"],"湎":["mian"],"湑":["xu"],"湓":["pen"],"湔":["jian"],"湖":["hu"],"湘":["xiang"],"湛":["zhan"],"湜":["shi"],"湝":["jie"],"湟":["huang"],"湣":["min"],"湫":["jiao","qiu"],"湮":["yan"],"湲":["yuan"],"湴":["ban"],"湾":["wan"],"湿":["shi"],"溁":["ying"],"溃":["kui"],"溅":["jian"],"溆":["xu"],"溇":["lou"],"溉":["gai"],"溍":["jin"],"溏":["tang"],"源":["yuan"],"溘":["ke"],"溚":["ta","da"],"溜":["liu"],"溞":["sao"],"溟":["ming"],"溠":["zha"],"溢":["yi"],"溥":["pu"],"溦":["wei"],"溧":["li"],"溪":["xi"],"溯":["su"],"溱":["qin","zhen"],"溲":["sou"],"溴":["xiu"],"溵":["yin"],"溶":["rong"],"溷":["hun"],"溹":["suo"],"溺":["ni"],"溻":["ta"],"溽":["ru"],"滁":["chu"],"滂":["pang"],"滃":["weng"],"滆":["ge"],"滇":["dian"],"滉":["huang"],"滋":["zi"],"滍":["zhi"],"滏":["fu"],"滑":["hua"],"滓":["zi"],"滔":["tao"],"滕":["teng"],"滗":["bi"],"滘":["jiao"],"滚":["gun"],"滞":["zhi"],"滟":["yan"],"滠":["she"],"满":["man"],"滢":["ying"],"滤":["lv"],"滥":["lan"],"滦":["luan"],"滧":["yao"],"滨":["bin"],"滩":["tan"],"滪":["yu"],"滫":["xiu"],"滴":["di"],"滹":["hu"],"漂":["piao"],"漆":["qi"],"漈":["ji"],"漉":["lu"],"漋":["long"],"漏":["lou"],"漓":["li"],"演":["yan"],"漕":["cao"],"漖":["jiao"],"漠":["mo"],"漤":["lan"],"漦":["chi"],"漩":["xuan"],"漪":["yi"],"漫":["man"],"漭":["mang"],"漯":["luo"],"漱":["shu"],"漳":["zhang"],"漴":["zhuang","chong"],"漶":["huan"],"漷":["huo"],"漹":["yan"],"漻":["liao"],"漼":["cui"],"漾":["yang"],"潆":["ying"],"潇":["xiao"],"潋":["lian"],"潍":["wei"],"潏":["yu"],"潖":["pa"],"潘":["pan"],"潜":["qian"],"潞":["lu"],"潟":["xi"],"潢":["huang"],"潦":["lao"],"潩":["yi"],"潭":["tan"],"潮":["chao"],"潲":["shao"],"潴":["zhu"],"潵":["sa","san"],"潸":["shan"],"潺":["chan"],"潼":["tong"],"潽":["pu"],"潾":["lin"],"澂":["cheng"],"澄":["cheng"],"澈":["che"],"澉":["gan"],"澌":["si"],"澍":["shu"],"澎":["peng"],"澛":["lu"],"澜":["lan"],"澡":["zao"],"澥":["xie"],"澧":["li"],"澪":["ling"],"澭":["yong"],"澳":["ao"],"澴":["huan"],"澶":["chan"],"澹":["dan"],"澼":["pi"],"澽":["ju"],"激":["ji"],"濂":["lian"],"濉":["sui"],"濋":["chu"],"濑":["lai"],"濒":["bin"],"濞":["bi"],"濠":["hao"],"濡":["ru"],"濩":["huo"],"濮":["pu"],"濯":["zhuo"],"瀌":["biao"],"瀍":["chan"],"瀑":["pu"],"瀔":["gu"],"瀚":["han"],"瀛":["ying"],"瀣":["xie"],"瀱":["ji"],"瀵":["fen"],"瀹":["yue"],"瀼":["rang"],"灈":["qu"],"灌":["guan"],"灏":["hao"],"灞":["ba"],"火":["huo"],"灭":["mie"],"灯":["deng"],"灰":["hui"],"灵":["ling"],"灶":["zao"],"灸":["jiu"],"灼":["zhuo"],"灾":["zai"],"灿":["can"],"炀":["yang"],"炅":["jiong"],"炆":["wen"],"炉":["lu"],"炊":["chui"],"炌":["kai"],"炎":["yan"],"炒":["chao"],"炔":["gui","que"],"炕":["kang"],"炖":["dun"],"炘":["xin"],"炙":["zhi"],"炜":["wei"],"炝":["qiang"],"炟":["da"],"炣":["ke"],"炫":["xuan"],"炬":["ju"],"炭":["tan"],"炮":["pao"],"炯":["jiong"],"炱":["tai"],"炳":["bing"],"炷":["zhu"],"炸":["zha"],"点":["dian"],"炻":["shi"],"炼":["lian"],"炽":["chi"],"烀":["hu"],"烁":["shuo"],"烂":["lan"],"烃":["ting"],"烈":["lie"],"烊":["yang"],"烔":["tong"],"烘":["hong"],"烙":["lao"],"烛":["zhu"],"烜":["xuan"],"烝":["zheng"],"烟":["yan"],"烠":["hui"],"烤":["kao"],"烦":["fan"],"烧":["shao"],"烨":["ye"],"烩":["hui"],"烫":["tang"],"烬":["jin"],"热":["re"],"烯":["xi"],"烶":["ting"],"烷":["wan"],"烹":["peng"],"烺":["lang"],"烻":["yan"],"烽":["feng"],"焆":["juan"],"焉":["yan"],"焊":["han"],"焌":["jun","qu"],"焐":["wu"],"焓":["han"],"焕":["huan"],"焖":["men"],"焗":["ju"],"焘":["dao"],"焙":["bei"],"焚":["fen"],"焜":["kun"],"焞":["tun"],"焦":["jiao"],"焯":["chao","zhuo"],"焰":["yan"],"焱":["yan"],"然":["ran"],"煁":["chen"],"煃":["kui"],"煅":["duan"],"煊":["xuan"],"煋":["xing"],"煌":["huang"],"煎":["jian"],"煓":["tuan"],"煜":["yu"],"煞":["sha"],"煟":["wei"],"煤":["mei"],"煦":["xu"],"照":["zhao"],"煨":["wei"],"煮":["zhu"],"煲":["bao"],"煳":["hu"],"煴":["yun"],"煸":["bian"],"煺":["tui"],"煽":["shan"],"熄":["xi"],"熇":["he"],"熊":["xiong"],"熏":["xun"],"熔":["rong"],"熘":["liu"],"熙":["xi"],"熛":["biao"],"熜":["cong"],"熟":["shu"],"熠":["yi"],"熥":["teng"],"熨":["yun"],"熬":["ao"],"熵":["shang"],"熹":["xi"],"熻":["xi"],"燃":["ran"],"燊":["shen"],"燋":["jiao"],"燎":["liao"],"燏":["yu"],"燔":["fan"],"燕":["yan"],"燚":["yi"],"燠":["yu"],"燥":["zao"],"燧":["sui"],"燮":["xie"],"燹":["xian"],"爆":["bao"],"爇":["ruo"],"爔":["xi"],"爚":["yue"],"爝":["jue"],"爟":["guan"],"爨":["cuan"],"爪":["zhao"],"爬":["pa"],"爰":["yuan"],"爱":["ai"],"爵":["jue"],"父":["fu"],"爷":["ye"],"爸":["ba"],"爹":["die"],"爻":["yao"],"爽":["shuang"],"爿":["pan"],"牁":["ke"],"牂":["zang"],"片":["pian"],"版":["ban"],"牌":["pai"],"牍":["du"],"牒":["die"],"牖":["you"],"牙":["ya"],"牚":["cheng"],"牛":["niu"],"牝":["pin"],"牟":["mou"],"牡":["mu"],"牢":["lao"],"牤":["mang"],"牥":["fang"],"牦":["mao"],"牧":["mu"],"物":["wu"],"牮":["jian"],"牯":["gu"],"牲":["sheng"],"牵":["qian"],"特":["te"],"牺":["xi"],"牻":["mang"],"牾":["wu"],"牿":["gu"],"犀":["xi"],"犁":["li"],"犄":["ji"],"犇":["ben"],"犊":["du"],"犋":["ju"],"犍":["jian"],"犏":["pian"],"犒":["kao"],"犟":["jiang"],"犨":["chou"],"犬":["quan"],"犯":["fan"],"犰":["qiu"],"犴":["an","han"],"状":["zhuang"],"犷":["guang"],"犸":["ma"],"犹":["you"],"狁":["yun"],"狂":["kuang"],"狃":["niu"],"狄":["di"],"狈":["bei"],"狉":["pi"],"狍":["pao"],"狎":["xia"],"狐":["hu"],"狒":["fei"],"狗":["gou"],"狙":["ju"],"狝":["xian"],"狞":["ning"],"狠":["hen"],"狡":["jiao"],"狨":["rong"],"狩":["shou"],"独":["du"],"狭":["xia"],"狮":["shi"],"狯":["kuai"],"狰":["zheng"],"狱":["yu"],"狲":["sun"],"狳":["yu"],"狴":["bi"],"狷":["juan"],"狸":["li"],"狺":["yin"],"狻":["suan"],"狼":["lang"],"猁":["li"],"猃":["xian"],"猄":["jing"],"猇":["xiao"],"猊":["ni"],"猎":["lie"],"猕":["mi"],"猖":["chang"],"猗":["yi"],"猛":["meng"],"猜":["cai"],"猝":["cu"],"猞":["she"],"猡":["luo"],"猢":["hu"],"猥":["wei"],"猩":["xing"],"猪":["zhu"],"猫":["mao"],"猬":["wei"],"献":["xian"],"猯":["tuan"],"猰":["ya"],"猱":["nao"],"猴":["hou"],"猷":["you"],"猹":["cha"],"猺":["yao"],"猾":["hua"],"猿":["yuan"],"獍":["jing"],"獐":["zhang"],"獒":["ao"],"獗":["jue"],"獠":["liao"],"獬":["xie"],"獭":["ta"],"獯":["xun"],"獴":["meng"],"獾":["huan"],"玃":["jue"],"玄":["xuan"],"率":["lv","shuai"],"玉":["yu"],"王":["wang"],"玎":["ding"],"玑":["ji"],"玒":["hong"],"玓":["di"],"玕":["gan"],"玖":["jiu"],"玘":["qi"],"玙":["yu"],"玚":["chang"],"玛":["ma"],"玞":["fu"],"玟":["wen","min"],"玠":["jie"],"玡":["ya"],"玢":["bin"],"玤":["bang"],"玥":["yue"],"玦":["jue"],"玩":["wan"],"玫":["mei"],"玭":["pin"],"玮":["wei"],"环":["huan"],"现":["xian"],"玱":["qiang"],"玲":["ling"],"玳":["dai"],"玶":["ping"],"玷":["dian"],"玹":["xuan"],"玺":["xi"],"玻":["bo"],"玼":["ci"],"玿":["shao"],"珀":["po"],"珂":["ke"],"珅":["shen"],"珇":["zu"],"珈":["jia"],"珉":["min"],"珊":["shan"],"珋":["liu"],"珌":["bi"],"珍":["zhen"],"珏":["jue"],"珐":["fa"],"珑":["long"],"珒":["jin"],"珕":["li"],"珖":["guang"],"珙":["gong"],"珛":["xiu"],"珝":["xu"],"珞":["luo"],"珠":["zhu"],"珢":["yin"],"珣":["xun"],"珥":["er"],"珦":["xiang"],"珧":["yao"],"珩":["hang","heng"],"珪":["gui"],"珫":["chong"],"班":["ban"],"珰":["dang"],"珲":["hui","hun"],"珵":["cheng"],"珷":["wu"],"珸":["wu"],"珹":["cheng"],"珺":["jun"],"珽":["ting"],"琀":["han"],"球":["qiu"],"琄":["xuan"],"琅":["lang"],"理":["li"],"琇":["xiu"],"琈":["fu"],"琉":["liu"],"琊":["ya"],"琎":["jin"],"琏":["lian"],"琐":["suo"],"琔":["dian"],"琚":["ju"],"琛":["chen"],"琟":["wei"],"琡":["chu","shu"],"琢":["zuo","zhuo"],"琤":["cheng"],"琥":["hu"],"琦":["qi"],"琨":["kun"],"琪":["qi"],"琫":["beng"],"琬":["wan"],"琭":["lu"],"琮":["cong"],"琯":["guan"],"琰":["yan"],"琲":["bei"],"琳":["lin"],"琴":["qin"],"琵":["pi"],"琶":["pa"],"琼":["qiong"],"瑀":["yu"],"瑁":["mao"],"瑂":["mei"],"瑃":["chun"],"瑄":["xuan"],"瑅":["ti"],"瑆":["xing"],"瑑":["zhuan"],"瑓":["lian"],"瑔":["quan"],"瑕":["xia"],"瑖":["duan"],"瑗":["yuan"],"瑙":["nao"],"瑚":["hu"],"瑛":["ying"],"瑜":["yu"],"瑝":["huang"],"瑞":["rui"],"瑟":["se"],"瑢":["rong"],"瑧":["zhen"],"瑨":["jin"],"瑬":["liu"],"瑭":["tang"],"瑰":["gui"],"瑱":["zhen","tian"],"瑳":["cuo"],"瑶":["yao"],"瑷":["ai"],"瑾":["jin"],"璀":["cui"],"璁":["cong"],"璃":["li"],"璆":["qiu"],"璇":["xuan"],"璈":["ao"],"璋":["zhang"],"璎":["ying"],"璐":["lu"],"璒":["deng"],"璘":["lin"],"璜":["huang"],"璞":["pu"],"璟":["jing"],"璠":["fan"],"璥":["jing"],"璧":["bi"],"璨":["can"],"璩":["qu"],"璪":["zao"],"璬":["jiao"],"璮":["tan"],"璱":["se"],"璲":["sui"],"璺":["wen"],"瓀":["ruan"],"瓒":["zan"],"瓖":["xiang"],"瓘":["guan"],"瓜":["gua"],"瓞":["die"],"瓠":["hu"],"瓢":["piao"],"瓣":["ban"],"瓤":["rang"],"瓦":["wa"],"瓮":["weng"],"瓯":["ou"],"瓴":["ling"],"瓶":["ping"],"瓷":["ci"],"瓻":["chi"],"瓿":["bu"],"甄":["zhen"],"甍":["meng"],"甏":["beng"],"甑":["zeng"],"甓":["pi"],"甗":["yan"],"甘":["gan"],"甚":["shen"],"甜":["tian"],"生":["sheng"],"甡":["shen"],"甥":["sheng"],"甦":["su"],"用":["yong"],"甩":["shuai"],"甪":["lu"],"甫":["fu"],"甬":["yong"],"甭":["beng"],"甯":["ning"],"田":["tian"],"由":["you"],"甲":["jia"],"申":["shen"],"电":["dian"],"男":["nan"],"甸":["dian"],"町":["ting"],"画":["hua"],"甾":["zai"],"畀":["bi"],"畅":["chang"],"畈":["fan"],"畋":["tian"],"界":["jie"],"畎":["quan"],"畏":["wei"],"畔":["pan"],"畖":["wa"],"留":["liu"],"畚":["ben"],"畛":["zhen"],"畜":["chu","xu"],"畤":["zhi"],"略":["lüe"],"畦":["qi"],"番":["fan"],"畬":["she"],"畯":["jun"],"畲":["she"],"畴":["chou"],"畸":["ji"],"畹":["wan"],"畿":["ji"],"疁":["liu"],"疃":["tuan"],"疆":["jiang"],"疍":["dan"],"疏":["shu"],"疐":["zhi"],"疑":["yi"],"疔":["ding"],"疖":["jie"],"疗":["liao"],"疙":["ge"],"疚":["jiu"],"疝":["shan"],"疟":["nüe"],"疠":["li"],"疡":["yang"],"疢":["chen"],"疣":["you"],"疤":["ba"],"疥":["jie"],"疫":["yi"],"疬":["li"],"疭":["zong"],"疮":["chuang"],"疯":["feng"],"疰":["zhu"],"疱":["pao"],"疲":["pi"],"疳":["gan"],"疴":["ke"],"疵":["ci"],"疸":["dan"],"疹":["zhen"],"疼":["teng"],"疽":["ju"],"疾":["ji"],"痂":["jia"],"痃":["xuan"],"痄":["zha"],"病":["bing"],"症":["zheng"],"痈":["yong"],"痉":["jing"],"痊":["quan"],"痍":["yi"],"痒":["yang"],"痓":["chi","zhi"],"痔":["zhi"],"痕":["hen"],"痘":["dou"],"痛":["tong"],"痞":["pi"],"痢":["li"],"痣":["zhi"],"痤":["cuo"],"痦":["wu"],"痧":["sha"],"痨":["lao"],"痪":["huan"],"痫":["xian"],"痰":["tan"],"痱":["fei"],"痴":["chi"],"痹":["bi"],"痼":["gu"],"痿":["wei"],"瘀":["yu"],"瘁":["cui"],"瘃":["zhu"],"瘅":["dan"],"瘆":["shen"],"瘊":["hou"],"瘌":["la"],"瘐":["yu"],"瘕":["jia"],"瘗":["yi"],"瘘":["lou"],"瘙":["sao"],"瘛":["chi"],"瘟":["wen"],"瘠":["ji"],"瘢":["ban"],"瘤":["liu"],"瘥":["chai"],"瘦":["shou"],"瘩":["da"],"瘪":["bie"],"瘫":["tan"],"瘭":["biao"],"瘰":["luo"],"瘳":["chou"],"瘴":["zhang"],"瘵":["zhai"],"瘸":["que"],"瘼":["mo"],"瘾":["yin"],"瘿":["ying"],"癀":["huang"],"癃":["long"],"癌":["ai"],"癍":["ban"],"癔":["yi"],"癖":["pi"],"癗":["lei"],"癜":["dian"],"癞":["lai"],"癣":["xuan"],"癫":["dian"],"癯":["qu"],"癸":["gui"],"登":["deng"],"白":["bai"],"百":["bai"],"癿":["qie"],"皂":["zao"],"的":["de"],"皆":["jie"],"皇":["huang"],"皈":["gui"],"皋":["gao"],"皎":["jiao"],"皑":["ai"],"皓":["hao"],"皕":["bi"],"皖":["wan"],"皙":["xi"],"皛":["xiao"],"皞":["hao"],"皤":["po"],"皦":["jiao"],"皭":["jiao"],"皮":["pi"],"皱":["zhou"],"皲":["jun"],"皴":["cun"],"皿":["min"],"盂":["yu"],"盅":["zhong"],"盆":["pen"],"盈":["ying"],"盉":["he"],"益":["yi"],"盍":["he"],"盎":["ang"],"盏":["zhan"],"盐":["yan"],"监":["jian"],"盒":["he"],"盔":["kui"],"盖":["gai"],"盗":["dao"],"盘":["pan"],"盛":["sheng"],"盟":["meng"],"盥":["guan"],"盦":["an"],"目":["mu"],"盯":["ding"],"盱":["xu"],"盲":["mang"],"直":["zhi"],"盷":["tian","xian"],"相":["xiang"],"盹":["dun"],"盼":["pan"],"盾":["dun"],"省":["sheng"],"眄":["mian"],"眇":["miao"],"眈":["dan"],"眉":["mei"],"眊":["mao"],"看":["kan"],"眍":["kou"],"眙":["yi"],"眚":["sheng"],"真":["zhen"],"眠":["mian"],"眢":["yuan"],"眦":["zi"],"眨":["zha"],"眩":["xuan"],"眬":["long"],"眭":["sui","gui"],"眯":["mi"],"眵":["chi"],"眶":["kuang"],"眷":["juan"],"眸":["mou"],"眺":["tiao"],"眼":["yan"],"着":["zhe"],"睁":["zheng"],"睃":["suo"],"睄":["shao","qiao"],"睇":["di"],"睎":["xi"],"睐":["lai"],"睑":["jian"],"睚":["ya"],"睛":["jing"],"睡":["shui"],"睢":["sui","hui"],"督":["du"],"睥":["pi","bi"],"睦":["mu"],"睨":["ni"],"睫":["jie"],"睬":["cai"],"睹":["du"],"睽":["kui"],"睾":["gao"],"睿":["rui"],"瞀":["mao"],"瞄":["miao"],"瞅":["chou"],"瞋":["chen"],"瞌":["ke"],"瞍":["sou"],"瞎":["xia"],"瞑":["ming"],"瞒":["man"],"瞟":["piao"],"瞠":["cheng"],"瞢":["meng"],"瞥":["pie"],"瞧":["qiao"],"瞩":["zhu"],"瞪":["deng"],"瞫":["shen"],"瞬":["shun"],"瞭":["liao"],"瞰":["kan"],"瞳":["tong"],"瞵":["lin"],"瞻":["zhan"],"瞽":["gu"],"瞿":["qu"],"矍":["jue"],"矗":["chu"],"矛":["mao"],"矜":["jin"],"矞":["yu"],"矢":["shi"],"矣":["yi"],"知":["zhi"],"矧":["shen"],"矩":["ju"],"矫":["jiao"],"矬":["cuo"],"短":["duan"],"矮":["ai"],"矰":["zeng"],"石":["shi"],"矶":["ji"],"矸":["gan"],"矻":["ku"],"矼":["gang"],"矾":["fan"],"矿":["kuang"],"砀":["dang"],"码":["ma"],"砂":["sha"],"砄":["jue"],"砆":["fu"],"砉":["huo","xu"],"砌":["qi"],"砍":["kan"],"砑":["ya"],"砒":["pi"],"研":["yan"],"砖":["zhuan"],"砗":["che"],"砘":["dun"],"砚":["yan"],"砜":["feng"],"砝":["fa"],"砟":["zha"],"砠":["ju"],"砣":["tuo"],"砥":["di"],"砧":["zhen"],"砫":["zhu"],"砬":["la","li"],"砭":["bian"],"砮":["nu"],"砰":["peng"],"破":["po"],"砵":["bo"],"砷":["shen"],"砸":["za"],"砹":["ai"],"砺":["li"],"砻":["long"],"砼":["tong"],"砾":["li"],"础":["chu"],"硁":["keng"],"硅":["gui"],"硇":["nao"],"硊":["wei"],"硌":["ge","luo"],"硍":["xian","ken"],"硎":["xing"],"硐":["dong"],"硒":["xi"],"硔":["hong"],"硕":["shuo"],"硖":["xia"],"硗":["qiao"],"硙":["wei"],"硚":["qiao"],"硝":["xiao"],"硪":["wo"],"硫":["liu"],"硬":["ying"],"硭":["mang"],"确":["que"],"硼":["peng"],"硿":["kong"],"碃":["qing"],"碇":["ding"],"碈":["min"],"碉":["diao"],"碌":["lu"],"碍":["ai"],"碎":["sui"],"碏":["que"],"碑":["bei"],"碓":["dui"],"碗":["wan"],"碘":["dian"],"碚":["bei"],"碛":["qi"],"碜":["chen"],"碟":["die"],"碡":["du","zhou"],"碣":["jie"],"碥":["bian"],"碧":["bi"],"碨":["wei"],"碰":["peng"],"碱":["jian"],"碲":["di"],"碳":["tan"],"碴":["cha"],"碶":["qi"],"碹":["xuan"],"碾":["nian"],"磁":["ci"],"磅":["bang"],"磉":["sang"],"磊":["lei"],"磋":["cuo"],"磏":["lian"],"磐":["pan"],"磔":["zhe"],"磕":["ke"],"磙":["gun"],"磜":["qi"],"磡":["kan"],"磨":["mo"],"磬":["qing"],"磲":["qu"],"磴":["deng"],"磷":["lin"],"磹":["tan","dian"],"磻":["pan"],"礁":["jiao"],"礅":["dun"],"礌":["lei"],"礓":["jiang"],"礞":["meng"],"礴":["bo"],"礵":["shuang"],"示":["shi"],"礼":["li"],"社":["she"],"祀":["si"],"祁":["qi"],"祃":["ma"],"祆":["xian"],"祇":["qi","zhi"],"祈":["qi"],"祉":["zhi"],"祊":["beng"],"祋":["dui"],"祎":["yi"],"祏":["shi"],"祐":["you"],"祓":["fu"],"祕":["mi"],"祖":["zu"],"祗":["zhi"],"祚":["zuo"],"祛":["qu"],"祜":["hu"],"祝":["zhu"],"神":["shen"],"祟":["sui"],"祠":["ci"],"祢":["mi"],"祥":["xiang"],"祧":["tiao"],"票":["piao"],"祭":["ji"],"祯":["zhen"],"祲":["jin"],"祷":["dao"],"祸":["huo"],"祺":["qi"],"祼":["guan"],"祾":["ling"],"禀":["bing"],"禁":["jin"],"禄":["lu"],"禅":["chan"],"禊":["xi"],"禋":["yin"],"福":["fu"],"禒":["xian"],"禔":["zhi","ti"],"禘":["di"],"禚":["zhuo"],"禛":["zhen"],"禤":["xuan"],"禧":["xi"],"禳":["rang"],"禹":["yu"],"禺":["yu"],"离":["li"],"禽":["qin"],"禾":["he"],"秀":["xiu"],"私":["si"],"秃":["tu"],"秆":["gan"],"秉":["bing"],"秋":["qiu"],"种":["zhong"],"科":["ke"],"秒":["miao"],"秕":["bi"],"秘":["mi"],"租":["zu"],"秣":["mo"],"秤":["cheng"],"秦":["qin"],"秧":["yang"],"秩":["zhi"],"秫":["shu"],"秬":["ju"],"秭":["zi"],"积":["ji"],"称":["cheng"],"秸":["jie"],"移":["yi"],"秽":["hui"],"秾":["nong"],"稀":["xi"],"稂":["lang"],"稃":["fu"],"稆":["lv"],"程":["cheng"],"稌":["tu"],"稍":["shao"],"税":["shui"],"稑":["lu"],"稔":["ren"],"稗":["bai"],"稙":["zhi"],"稚":["zhi"],"稞":["ke"],"稠":["chou"],"稣":["su"],"稳":["wen"],"稷":["ji"],"稹":["zhen"],"稻":["dao"],"稼":["jia"],"稽":["ji"],"稿":["gao"],"穄":["ji"],"穆":["mu"],"穑":["se"],"穗":["sui"],"穙":["pu"],"穜":["zhong","tong"],"穟":["sui"],"穰":["rang"],"穴":["xue"],"究":["jiu"],"穷":["qiong"],"穸":["xi"],"穹":["qiong"],"空":["kong"],"穿":["chuan"],"窀":["zhun"],"突":["tu"],"窃":["qie"],"窄":["zhai"],"窅":["yao"],"窈":["yao"],"窊":["wa"],"窍":["qiao"],"窎":["diao"],"窑":["yao"],"窒":["zhi"],"窕":["tiao"],"窖":["jiao"],"窗":["chuang"],"窘":["jiong"],"窜":["cuan"],"窝":["wo"],"窟":["ku"],"窠":["ke"],"窣":["su"],"窥":["kui"],"窦":["dou"],"窨":["xun","yin"],"窬":["yu"],"窭":["ju"],"窳":["yu"],"窸":["xi"],"窿":["long"],"立":["li"],"竑":["hong"],"竖":["shu"],"竘":["qu"],"站":["zhan"],"竞":["jing"],"竟":["jing"],"章":["zhang"],"竣":["jun"],"童":["tong"],"竦":["song"],"竫":["jing"],"竭":["jie"],"端":["duan"],"竹":["zhu"],"竺":["zhu"],"竽":["yu"],"竿":["gan"],"笃":["du"],"笄":["ji"],"笆":["ba"],"笈":["ji"],"笊":["zhao"],"笋":["sun"],"笏":["hu"],"笑":["xiao"],"笔":["bi"],"笕":["jian"],"笙":["sheng"],"笛":["di"],"笞":["chi"],"笠":["li"],"笤":["tiao"],"笥":["si"],"符":["fu"],"笨":["ben"],"笪":["da"],"笫":["zi"],"第":["di"],"笮":["ze","zuo"],"笯":["nu"],"笱":["gou"],"笳":["jia"],"笸":["po"],"笺":["jian"],"笼":["long"],"笾":["bian"],"筀":["gui"],"筅":["xian"],"筇":["qiong"],"等":["deng"],"筋":["jin"],"筌":["quan"],"筏":["fa"],"筐":["kuang"],"筑":["zhu"],"筒":["tong"],"答":["da"],"策":["ce"],"筘":["kou"],"筚":["bi"],"筛":["shai"],"筜":["dang"],"筝":["zheng"],"筠":["yun"],"筢":["pa"],"筤":["lang"],"筥":["ju"],"筦":["guan"],"筮":["shi"],"筱":["xiao"],"筲":["shao"],"筵":["yan"],"筶":["gao"],"筷":["kuai"],"筹":["chou"],"筻":["gang"],"筼":["yun"],"签":["qian"],"简":["jian"],"箅":["bi"],"箍":["gu"],"箐":["qing"],"箓":["lu"],"箔":["bo"],"箕":["ji"],"箖":["lin"],"算":["suan"],"箜":["kong"],"管":["guan"],"箢":["yuan","wan"],"箦":["ze"],"箧":["qie"],"箨":["tuo"],"箩":["luo"],"箪":["dan"],"箫":["xiao"],"箬":["ruo"],"箭":["jian"],"箱":["xiang"],"箴":["zhen"],"箸":["zhu"],"篁":["huang"],"篆":["zhuan"],"篇":["pian"],"篌":["hou"],"篑":["kui"],"篓":["lou"],"篙":["gao"],"篚":["fei"],"篝":["gou"],"篡":["cuan"],"篥":["li"],"篦":["bi"],"篪":["chi"],"篮":["lan"],"篯":["jian"],"篱":["li"],"篷":["peng"],"篼":["dou"],"篾":["mie"],"簃":["yi"],"簇":["cu"],"簉":["zao"],"簋":["gui"],"簌":["su"],"簏":["lu"],"簕":["le"],"簖":["duan"],"簝":["liao"],"簟":["dian"],"簠":["fu"],"簧":["huang"],"簪":["zan"],"簰":["pai"],"簸":["bo"],"簿":["bu"],"籀":["zhou"],"籁":["lai"],"籍":["ji"],"籥":["yue"],"米":["mi"],"籴":["di"],"类":["lei"],"籼":["xian"],"籽":["zi"],"粉":["fen"],"粑":["ba"],"粒":["li"],"粕":["po"],"粗":["cu"],"粘":["zhan","nian"],"粜":["tiao"],"粝":["li"],"粞":["xi"],"粟":["su"],"粢":["zi"],"粤":["yue"],"粥":["zhou"],"粪":["fen"],"粮":["liang"],"粱":["liang"],"粲":["can"],"粳":["jing"],"粹":["cui"],"粼":["lin"],"粽":["zong"],"精":["jing"],"粿":["guo"],"糁":["san"],"糅":["rou"],"糇":["hou"],"糈":["xu"],"糊":["hu"],"糌":["zan"],"糍":["ci"],"糒":["bei"],"糕":["gao"],"糖":["tang"],"糗":["qiu"],"糙":["cao"],"糜":["mi"],"糟":["zao"],"糠":["kang"],"糨":["jiang"],"糯":["nuo"],"糵":["nie"],"系":["xi"],"紊":["wen"],"素":["su"],"索":["suo"],"紧":["jin"],"紫":["zi"],"累":["lei"],"絜":["jie","xie"],"絮":["xu"],"絷":["zhi"],"綦":["qi"],"綮":["qi","qing"],"縠":["hu"],"縢":["teng"],"縻":["mi"],"繁":["fan"],"繄":["yi"],"繇":["yao"],"纂":["zuan"],"纛":["dao"],"纠":["jiu"],"纡":["yu"],"红":["hong"],"纣":["zhou"],"纤":["xian"],"纥":["ge","he"],"约":["yue"],"级":["ji"],"纨":["wan"],"纩":["kuang"],"纪":["ji"],"纫":["ren"],"纬":["wei"],"纭":["yun"],"纮":["hong"],"纯":["chun"],"纰":["pi"],"纱":["sha"],"纲":["gang"],"纳":["na"],"纴":["ren"],"纵":["zong"],"纶":["lun"],"纷":["fen"],"纸":["zhi"],"纹":["wen"],"纺":["fang"],"纻":["zhu"],"纼":["zhen"],"纽":["niu"],"纾":["shu"],"线":["xian"],"绀":["gan"],"绁":["xie"],"绂":["fu"],"练":["lian"],"组":["zu"],"绅":["shen"],"细":["xi"],"织":["zhi"],"终":["zhong"],"绉":["zhou"],"绊":["ban"],"绋":["fu"],"绌":["chu"],"绍":["shao"],"绎":["yi"],"经":["jing"],"绐":["dai"],"绑":["bang"],"绒":["rong"],"结":["jie"],"绔":["ku"],"绕":["rao"],"绖":["die"],"绗":["hang"],"绘":["hui"],"给":["gei"],"绚":["xuan"],"绛":["jiang"],"络":["luo"],"绝":["jue"],"绞":["jiao"],"统":["tong"],"绠":["geng"],"绡":["xiao"],"绢":["juan"],"绣":["xiu"],"绤":["xi"],"绥":["sui"],"绦":["tao"],"继":["ji"],"绨":["ti"],"绩":["ji"],"绪":["xu"],"绫":["ling"],"续":["xu"],"绮":["qi"],"绯":["fei"],"绰":["chuo"],"绱":["shang"],"绲":["gun"],"绳":["sheng"],"维":["wei"],"绵":["mian"],"绶":["shou"],"绷":["beng"],"绸":["chou"],"绹":["tao"],"绺":["liu"],"绻":["quan"],"综":["zong"],"绽":["zhan"],"绾":["wan"],"绿":["lv"],"缀":["zhui"],"缁":["zi"],"缂":["ke"],"缃":["xiang"],"缄":["jian"],"缅":["mian"],"缆":["lan"],"缇":["ti"],"缈":["miao"],"缉":["ji"],"缊":["yun"],"缌":["si"],"缎":["duan"],"缐":["xian"],"缑":["gou"],"缒":["zhui"],"缓":["huan"],"缔":["di"],"缕":["lv"],"编":["bian"],"缗":["min"],"缘":["yuan"],"缙":["jin"],"缚":["fu"],"缛":["ru"],"缜":["zhen"],"缝":["feng"],"缞":["cui"],"缟":["gao"],"缠":["chan"],"缡":["li"],"缢":["yi"],"缣":["jian"],"缤":["bin"],"缥":["piao"],"缦":["man"],"缧":["lei"],"缨":["ying"],"缩":["suo"],"缪":["mou"],"缫":["sao"],"缬":["xie"],"缭":["liao"],"缮":["shan"],"缯":["zeng"],"缰":["jiang"],"缱":["qian"],"缲":["qiao"],"缳":["huan"],"缴":["jiao"],"缵":["zuan"],"缶":["fou"],"缸":["gang"],"缺":["que"],"罂":["ying"],"罄":["qing"],"罅":["xia"],"罍":["lei"],"罐":["guan"],"网":["wang"],"罔":["wang"],"罕":["han"],"罗":["luo"],"罘":["fu"],"罚":["fa"],"罟":["gu"],"罡":["gang"],"罢":["ba"],"罨":["yan"],"罩":["zhao"],"罪":["zui"],"置":["zhi"],"罱":["lan"],"署":["shu"],"罴":["pi"],"罶":["liu"],"罹":["li"],"罽":["ji"],"罾":["zeng"],"羁":["ji"],"羊":["yang"],"羌":["qiang"],"美":["mei"],"羑":["you"],"羓":["ba"],"羔":["gao"],"羕":["yang"],"羖":["gu"],"羚":["ling"],"羝":["di"],"羞":["xiu"],"羟":["qiang"],"羡":["xian"],"群":["qun"],"羧":["suo"],"羯":["jie"],"羰":["tang"],"羱":["yuan"],"羲":["xi"],"羸":["lei"],"羹":["geng"],"羼":["chan"],"羽":["yu"],"羿":["yi"],"翀":["chong"],"翁":["weng"],"翂":["fen"],"翃":["hong"],"翅":["chi"],"翈":["xia"],"翊":["yi"],"翌":["yi"],"翎":["ling"],"翔":["xiang"],"翕":["xi"],"翘":["qiao"],"翙":["hui"],"翚":["hui"],"翛":["xiao"],"翟":["di"],"翠":["cui"],"翡":["fei"],"翥":["zhu"],"翦":["jian"],"翩":["pian"],"翮":["he"],"翯":["he"],"翰":["han"],"翱":["ao"],"翳":["yi"],"翷":["lin"],"翻":["fan"],"翼":["yi"],"翾":["xuan"],"耀":["yao"],"老":["lao"],"考":["kao"],"耄":["mao"],"者":["zhe"],"耆":["qi"],"耇":["gou"],"耋":["die"],"而":["er"],"耍":["shua"],"耏":["nai","er"],"耐":["nai"],"耑":["duan"],"耒":["lei"],"耔":["zi"],"耕":["geng"],"耖":["chao"],"耗":["hao"],"耘":["yun"],"耙":["ba"],"耜":["si"],"耠":["huo"],"耢":["lao"],"耤":["ji"],"耥":["tang"],"耦":["ou"],"耧":["lou"],"耨":["nou"],"耩":["jiang"],"耪":["pang"],"耰":["you"],"耱":["mo"],"耳":["er"],"耵":["ding"],"耶":["ye"],"耷":["da"],"耸":["song"],"耻":["chi"],"耽":["dan"],"耿":["geng"],"聂":["nie"],"聃":["dan"],"聆":["ling"],"聊":["liao"],"聋":["long"],"职":["zhi"],"聍":["ning"],"聒":["gua","guo"],"联":["lian"],"聘":["pin"],"聚":["ju"],"聩":["kui"],"聪":["cong"],"聱":["ao"],"聿":["yu"],"肃":["su"],"肄":["yi"],"肆":["si"],"肇":["zhao"],"肉":["rou"],"肋":["le","lei"],"肌":["ji"],"肓":["huang"],"肖":["xiao"],"肘":["zhou"],"肚":["du"],"肛":["gang"],"肝":["gan"],"肟":["wo"],"肠":["chang"],"股":["gu"],"肢":["zhi"],"肤":["fu"],"肥":["fei"],"肩":["jian"],"肪":["fang"],"肫":["zhun"],"肭":["na"],"肮":["ang"],"肯":["ken"],"肱":["gong"],"育":["yu"],"肴":["yao"],"肷":["qian"],"肸":["xi"],"肺":["fei"],"肼":["jing"],"肽":["tai"],"肾":["shen"],"肿":["zhong"],"胀":["zhang"],"胁":["xie"],"胂":["shen"],"胃":["wei"],"胄":["zhou"],"胆":["dan"],"胈":["ba"],"背":["bei"],"胍":["gua"],"胎":["tai"],"胖":["pang"],"胗":["zhen"],"胙":["zuo"],"胚":["pei"],"胛":["jia"],"胜":["sheng"],"胝":["zhi"],"胞":["bao"],"胠":["qu"],"胡":["hu"],"胣":["chi"],"胤":["yin"],"胥":["xu"],"胧":["long"],"胨":["dong"],"胩":["ka"],"胪":["lu"],"胫":["jing"],"胬":["nu"],"胭":["yan"],"胯":["kua"],"胰":["yi"],"胱":["guang"],"胲":["hai"],"胳":["ge"],"胴":["dong"],"胶":["jiao"],"胸":["xiong"],"胺":["an"],"胼":["pian"],"能":["neng"],"脂":["zhi"],"脆":["cui"],"脉":["mai"],"脊":["ji"],"脍":["kuai"],"脎":["sa"],"脏":["zang"],"脐":["qi"],"脑":["nao"],"脒":["mi"],"脓":["nong"],"脔":["luan"],"脖":["bo"],"脘":["wan"],"脚":["jiao"],"脞":["cuo"],"脟":["lie"],"脩":["xiu"],"脬":["pao"],"脯":["pu","fu"],"脱":["tuo"],"脲":["niao"],"脶":["luo"],"脸":["lian"],"脾":["pi"],"脿":["biao"],"腆":["tian"],"腈":["jing"],"腊":["la"],"腋":["ye"],"腌":["yan","a"],"腐":["fu"],"腑":["fu"],"腒":["ju"],"腓":["fei"],"腔":["qiang"],"腕":["wan"],"腘":["guo"],"腙":["zong"],"腚":["ding"],"腠":["cou"],"腥":["xing"],"腧":["shu"],"腨":["shuan"],"腩":["nan"],"腭":["e"],"腮":["sai"],"腯":["tu"],"腰":["yao"],"腱":["jian"],"腴":["yu"],"腹":["fu"],"腺":["xian"],"腻":["ni"],"腼":["mian"],"腽":["wa"],"腾":["teng"],"腿":["tui"],"膀":["bang"],"膂":["lv"],"膈":["ge"],"膊":["bo"],"膏":["gao"],"膑":["bin"],"膘":["biao"],"膙":["jiang"],"膛":["tang"],"膜":["mo"],"膝":["xi"],"膦":["lin"],"膨":["peng"],"膳":["shan"],"膺":["ying"],"膻":["shan"],"臀":["tun"],"臂":["bi"],"臃":["yong"],"臆":["yi"],"臊":["sao"],"臌":["gu"],"臑":["nao"],"臜":["za"],"臣":["chen"],"臧":["zang"],"自":["zi"],"臬":["nie"],"臭":["chou"],"至":["zhi"],"致":["zhi"],"臻":["zhen"],"臼":["jiu"],"臾":["yu"],"舀":["yao"],"舁":["yu"],"舂":["chong"],"舄":["xi"],"舅":["jiu"],"舆":["yu"],"舌":["she"],"舍":["she"],"舐":["shi"],"舒":["shu"],"舔":["tian"],"舛":["chuan"],"舜":["shun"],"舞":["wu"],"舟":["zhou"],"舠":["dao"],"舢":["shan"],"舣":["yi"],"舥":["pa"],"航":["hang"],"舫":["fang"],"般":["ban"],"舭":["bi"],"舯":["zhong"],"舰":["jian"],"舱":["cang"],"舲":["ling"],"舳":["zhu"],"舴":["ze"],"舵":["duo"],"舶":["bo"],"舷":["xian"],"舸":["ge"],"船":["chuan"],"舻":["lu"],"舾":["xi"],"艄":["shao"],"艅":["yu"],"艇":["ting"],"艉":["wei"],"艋":["meng"],"艎":["huang"],"艏":["shou"],"艘":["sou"],"艚":["cao"],"艟":["chong"],"艨":["meng"],"艮":["gen"],"良":["liang"],"艰":["jian"],"色":["se"],"艳":["yan"],"艴":["fu"],"艺":["yi"],"艽":["jiao"],"艾":["ai"],"艿":["nai"],"节":["jie"],"芃":["peng"],"芄":["wan"],"芈":["mi"],"芊":["qian"],"芋":["yu"],"芍":["shao"],"芎":["qiong","xiong"],"芏":["du"],"芑":["qi"],"芒":["mang"],"芗":["xiang"],"芘":["pi","bi"],"芙":["fu"],"芜":["wu"],"芝":["zhi"],"芟":["shan"],"芠":["wen"],"芡":["qian"],"芣":["fu"],"芤":["kou"],"芥":["jie"],"芦":["lu"],"芨":["ji"],"芩":["qin"],"芪":["qi"],"芫":["yan","yuan"],"芬":["fen"],"芭":["ba"],"芮":["rui"],"芯":["xin"],"芰":["ji"],"花":["hua"],"芳":["fang"],"芴":["wu"],"芷":["zhi"],"芸":["yun"],"芹":["qin"],"芼":["mao"],"芽":["ya"],"芾":["fei"],"苁":["cong"],"苄":["bian"],"苇":["wei"],"苈":["li"],"苉":["pi"],"苊":["e"],"苋":["xian"],"苌":["chang"],"苍":["cang"],"苎":["zhu"],"苏":["su"],"苑":["yuan"],"苒":["ran"],"苓":["ling"],"苔":["tai"],"苕":["shao","tiao"],"苗":["miao"],"苘":["qing"],"苛":["ke"],"苜":["mu"],"苞":["bao"],"苟":["gou"],"苠":["min"],"苡":["yi"],"苣":["ju"],"苤":["pie"],"若":["ruo"],"苦":["ku"],"苧":["ning","zhu"],"苫":["shan"],"苯":["ben"],"英":["ying"],"苴":["ju"],"苷":["gan"],"苹":["ping"],"苻":["fu"],"苾":["bi"],"茀":["fu"],"茁":["zhuo"],"茂":["mao"],"范":["fan"],"茄":["jia","qie"],"茅":["mao"],"茆":["mao"],"茈":["ci","zi"],"茉":["mo"],"茋":["zhi"],"茌":["chi"],"茎":["jing"],"茏":["long"],"茑":["niao"],"茓":["xue"],"茔":["ying"],"茕":["qiong"],"茗":["ming"],"茚":["yin"],"茛":["gen"],"茜":["qian"],"茝":["chai"],"茧":["jian"],"茨":["ci"],"茫":["mang"],"茬":["cha"],"茭":["jiao"],"茯":["fu"],"茱":["zhu"],"茳":["jiang"],"茴":["hui"],"茵":["yin"],"茶":["cha"],"茸":["rong"],"茹":["ru"],"茺":["chong"],"茼":["tong"],"茽":["zhong"],"荀":["xun"],"荁":["huan"],"荃":["quan"],"荄":["gai"],"荆":["jing"],"荇":["xing"],"草":["cao"],"荏":["ren"],"荐":["jian"],"荑":["ti","yi"],"荒":["huang"],"荓":["ping"],"荔":["li"],"荖":["lao"],"荙":["da"],"荚":["jia"],"荛":["rao"],"荜":["bi"],"荞":["qiao"],"荟":["hui"],"荠":["ji"],"荡":["dang"],"荣":["rong"],"荤":["hun"],"荥":["xing"],"荦":["luo"],"荧":["ying"],"荨":["xun","qian"],"荩":["jin"],"荪":["sun"],"荫":["yin"],"荬":["mai"],"荭":["hong"],"荮":["zhou"],"药":["yao"],"荷":["he"],"荸":["bi"],"荻":["di"],"荼":["tu"],"荽":["sui"],"莅":["li"],"莆":["pu"],"莉":["li"],"莎":["sha","suo"],"莒":["ju"],"莓":["mei"],"莘":["shen"],"莙":["jun"],"莛":["ting"],"莜":["you"],"莝":["cuo"],"莞":["guan"],"莠":["you"],"莨":["lang"],"莩":["fu"],"莪":["e"],"莫":["mo"],"莰":["kan"],"莱":["lai"],"莲":["lian"],"莳":["shi"],"莴":["wo"],"莶":["xian"],"获":["huo"],"莸":["you"],"莹":["ying"],"莺":["ying"],"莼":["chun"],"莽":["mang"],"莿":["ci"],"菀":["wan"],"菁":["jing"],"菂":["di"],"菅":["jian"],"菇":["gu"],"菉":["lu"],"菊":["ju"],"菌":["jun"],"菍":["nie"],"菏":["he"],"菔":["fu"],"菖":["chang"],"菘":["song"],"菜":["cai"],"菝":["ba"],"菟":["tu"],"菠":["bo"],"菡":["han"],"菥":["xi"],"菩":["pu"],"菪":["dang"],"菰":["gu"],"菱":["ling"],"菲":["fei"],"菹":["ju","zu"],"菼":["tan"],"菽":["shu"],"萁":["qi"],"萃":["cui"],"萄":["tao"],"萆":["bi"],"萋":["qi"],"萌":["meng"],"萍":["ping"],"萎":["wei"],"萏":["dan"],"萑":["huan"],"萘":["nai"],"萚":["tuo"],"萜":["tie"],"萝":["luo"],"萣":["ding"],"萤":["ying"],"营":["ying"],"萦":["ying"],"萧":["xiao"],"萨":["sa"],"萩":["qiu"],"萱":["xuan"],"萳":["nan"],"萸":["yu"],"萹":["bian"],"萼":["e"],"落":["luo","la"],"葆":["bao"],"葎":["lv"],"葑":["feng"],"葖":["tu"],"著":["zhu"],"葙":["xiang"],"葚":["ren","shen"],"葛":["ge"],"葜":["qia"],"葡":["pu"],"董":["dong"],"葩":["pa"],"葫":["hu"],"葬":["zang"],"葭":["jia"],"葰":["sui","jun"],"葱":["cong"],"葳":["wei"],"葴":["zhen"],"葵":["kui"],"葶":["ting"],"葸":["xi"],"葺":["qi"],"蒂":["di"],"蒄":["guan"],"蒇":["chan"],"蒈":["kai"],"蒉":["kui"],"蒋":["jiang"],"蒌":["lou"],"蒎":["pai"],"蒐":["sou"],"蒗":["lang"],"蒙":["meng"],"蒜":["suan"],"蒟":["ju"],"蒡":["bang"],"蒨":["qian"],"蒯":["kuai"],"蒱":["pu"],"蒲":["pu"],"蒴":["shuo"],"蒸":["zheng"],"蒹":["jian"],"蒺":["ji"],"蒻":["ruo"],"蒽":["en"],"蒿":["hao"],"蓁":["zhen"],"蓂":["ming"],"蓄":["xu"],"蓇":["gu"],"蓉":["rong"],"蓊":["weng"],"蓍":["shi"],"蓏":["luo"],"蓐":["ru"],"蓑":["suo"],"蓓":["bei"],"蓖":["bi"],"蓝":["lan"],"蓟":["ji"],"蓠":["li"],"蓢":["lang"],"蓣":["yu"],"蓥":["ying"],"蓦":["mo"],"蓬":["peng"],"蓰":["xi"],"蓼":["liao"],"蓿":["xu"],"蔀":["bu"],"蔃":["qiang"],"蔈":["biao"],"蔊":["han"],"蔌":["su"],"蔑":["mie"],"蔓":["man"],"蔗":["zhe"],"蔚":["wei"],"蔟":["cu"],"蔡":["cai"],"蔫":["nian"],"蔬":["shu"],"蔷":["qiang"],"蔸":["dou"],"蔹":["lian"],"蔺":["lin"],"蔻":["kou"],"蔼":["ai"],"蔽":["bi"],"蕃":["fan"],"蕈":["xun"],"蕉":["jiao"],"蕊":["rui"],"蕖":["qu"],"蕗":["lu"],"蕙":["hui"],"蕞":["zui"],"蕤":["rui"],"蕨":["jue"],"蕰":["wen","yun"],"蕲":["qi"],"蕴":["yun"],"蕹":["weng"],"蕺":["ji"],"蕻":["hong"],"蕾":["lei"],"薁":["yu"],"薄":["bao"],"薅":["hao"],"薇":["wei"],"薏":["yi"],"薛":["xue"],"薜":["bi"],"薢":["xie"],"薤":["xie"],"薨":["hong"],"薪":["xin"],"薮":["sou"],"薯":["shu"],"薰":["xun"],"薳":["wei","yuan"],"薷":["ru"],"薸":["piao"],"薹":["tai"],"薿":["ni"],"藁":["gao"],"藉":["ji","jie"],"藏":["cang"],"藐":["miao"],"藓":["xian"],"藕":["ou"],"藜":["li"],"藟":["lei"],"藠":["jiao"],"藤":["teng"],"藦":["mo"],"藨":["biao"],"藩":["fan"],"藻":["zao"],"藿":["huo"],"蘅":["heng"],"蘑":["mo"],"蘖":["nie"],"蘘":["rang"],"蘧":["qu"],"蘩":["fan"],"蘸":["zhan"],"蘼":["mi"],"虎":["hu"],"虏":["lu"],"虐":["nüe"],"虑":["lv"],"虒":["si"],"虓":["xiao"],"虔":["qian"],"虚":["xu"],"虞":["yu"],"虢":["guo"],"虤":["yan"],"虫":["chong"],"虬":["qiu"],"虮":["ji"],"虱":["shi"],"虷":["han"],"虸":["zi"],"虹":["hong"],"虺":["hui"],"虻":["meng"],"虼":["ge"],"虽":["sui"],"虾":["xia"],"虿":["chai"],"蚀":["shi"],"蚁":["yi"],"蚂":["ma"],"蚄":["fang"],"蚆":["ba"],"蚊":["wen"],"蚋":["rui"],"蚌":["bang"],"蚍":["pi"],"蚓":["yin"],"蚕":["can"],"蚜":["ya"],"蚝":["hao"],"蚣":["gong"],"蚤":["zao"],"蚧":["jie"],"蚨":["fu"],"蚩":["chi"],"蚪":["dou"],"蚬":["xian"],"蚯":["qiu"],"蚰":["you"],"蚱":["zha"],"蚲":["ping"],"蚴":["you"],"蚶":["han"],"蚺":["ran"],"蛀":["zhu"],"蛃":["bing"],"蛄":["gu"],"蛆":["qu"],"蛇":["she"],"蛉":["ling"],"蛊":["gu"],"蛋":["dan"],"蛎":["li"],"蛏":["cheng"],"蛐":["qu"],"蛑":["mou"],"蛔":["hui"],"蛘":["yang"],"蛙":["wa"],"蛛":["zhu"],"蛞":["kuo"],"蛟":["jiao"],"蛤":["ha","ge"],"蛩":["qiong"],"蛭":["zhi"],"蛮":["man"],"蛰":["zhe"],"蛱":["jia"],"蛲":["nao"],"蛳":["si"],"蛴":["qi"],"蛸":["shao","xiao"],"蛹":["yong"],"蛾":["e"],"蜀":["shu"],"蜂":["feng"],"蜃":["shen"],"蜇":["zhe"],"蜈":["wu"],"蜉":["fu"],"蜊":["li"],"蜍":["chu"],"蜎":["yuan"],"蜐":["jie"],"蜒":["yan"],"蜓":["ting"],"蜕":["tui"],"蜗":["wo"],"蜘":["zhi"],"蜚":["fei"],"蜜":["mi"],"蜞":["qi"],"蜡":["la"],"蜢":["meng"],"蜣":["qiang"],"蜥":["xi"],"蜩":["tiao"],"蜮":["yu"],"蜱":["pi"],"蜴":["yi"],"蜷":["quan"],"蜻":["qing"],"蜾":["guo"],"蜿":["wan"],"蝇":["ying"],"蝈":["guo"],"蝉":["chan"],"蝌":["ke"],"蝎":["xie"],"蝓":["yu"],"蝗":["huang"],"蝘":["yan"],"蝙":["bian"],"蝠":["fu"],"蝣":["you"],"蝤":["qiu"],"蝥":["mao"],"蝮":["fu"],"蝰":["kui"],"蝲":["la"],"蝴":["hu"],"蝶":["die"],"蝻":["nan"],"蝼":["lou"],"蝽":["chun"],"蝾":["rong"],"螂":["lang"],"螃":["pang"],"螅":["xi"],"螈":["yuan"],"螋":["sou"],"融":["rong"],"螗":["tang"],"螟":["ming"],"螠":["yi"],"螣":["te","teng"],"螨":["man"],"螫":["shi"],"螬":["cao"],"螭":["chi"],"螯":["ao"],"螱":["wei"],"螳":["tang"],"螵":["piao"],"螺":["luo"],"螽":["zhong"],"蟀":["shuai"],"蟆":["ma"],"蟊":["mao"],"蟋":["xi"],"蟏":["xiao"],"蟑":["zhang"],"蟒":["mang"],"蟛":["peng"],"蟠":["pan"],"蟥":["huang"],"蟪":["hui"],"蟫":["yin"],"蟮":["shan"],"蟹":["xie"],"蟾":["chan"],"蠃":["luo"],"蠊":["lian"],"蠋":["zhu"],"蠓":["meng"],"蠕":["ru"],"蠖":["huo"],"蠡":["li"],"蠢":["chun"],"蠲":["juan"],"蠹":["du"],"蠼":["qu"],"血":["xue","xie"],"衃":["pei"],"衄":["nv"],"衅":["xin"],"行":["xing","hang"],"衍":["yan"],"衎":["kan"],"衒":["xuan"],"衔":["xian"],"街":["jie"],"衙":["ya"],"衠":["zhun"],"衡":["heng"],"衢":["qu"],"衣":["yi"],"补":["bu"],"表":["biao"],"衩":["cha"],"衫":["shan"],"衬":["chen"],"衮":["gun"],"衰":["shuai"],"衲":["na"],"衷":["zhong"],"衽":["ren"],"衾":["qin"],"衿":["jin"],"袁":["yuan"],"袂":["mei"],"袄":["ao"],"袅":["niao"],"袆":["hui"],"袈":["jia"],"袋":["dai"],"袍":["pao"],"袒":["tan"],"袖":["xiu"],"袗":["zhen"],"袜":["wa"],"袢":["pan"],"袤":["mao"],"袪":["qu"],"被":["bei"],"袭":["xi"],"袯":["bo"],"袱":["fu"],"袷":["jia"],"袼":["ge"],"裁":["cai"],"裂":["lie"],"装":["zhuang"],"裆":["dang"],"裈":["kun"],"裉":["ken"],"裎":["cheng"],"裒":["pou"],"裔":["yi"],"裕":["yu"],"裘":["qiu"],"裙":["qun"],"裛":["yi"],"裟":["sha"],"裢":["lian"],"裣":["lian"],"裤":["ku"],"裥":["jian"],"裨":["bi"],"裰":["duo"],"裱":["biao"],"裳":["shang","chang"],"裴":["pei"],"裸":["luo"],"裹":["guo"],"裼":["ti","xi"],"裾":["ju"],"褂":["gua"],"褊":["bian"],"褐":["he"],"褒":["bao"],"褓":["bao"],"褕":["yu"],"褙":["bei"],"褚":["chu","zhu"],"褛":["lv"],"褟":["ta"],"褡":["da"],"褥":["ru"],"褪":["tui"],"褫":["chi"],"褯":["jie"],"褰":["qian"],"褴":["lan"],"褶":["zhe"],"襁":["qiang"],"襄":["xiang"],"襕":["lan"],"襚":["sui"],"襜":["chan"],"襞":["bi"],"襟":["jin"],"襦":["ru"],"襫":["shi"],"襻":["pan"],"西":["xi"],"要":["yao"],"覃":["tan"],"覆":["fu"],"见":["jian"],"观":["guan"],"觃":["yan"],"规":["gui"],"觅":["mi"],"视":["shi"],"觇":["chan"],"览":["lan"],"觉":["jue"],"觊":["ji"],"觋":["xi"],"觌":["di"],"觎":["yu"],"觏":["gou"],"觐":["jin"],"觑":["qu"],"角":["jiao"],"觖":["jue"],"觚":["gu"],"觜":["zi"],"觞":["shang"],"觟":["hua"],"解":["jie"],"觥":["gong"],"触":["chu"],"觫":["su"],"觭":["ji"],"觯":["zhi"],"觱":["bi"],"觳":["hu"],"觿":["xi"],"言":["yan"],"訄":["qiu"],"訇":["hong"],"訚":["yin"],"訾":["zi"],"詈":["li"],"詟":["zhe"],"詹":["zhan"],"誉":["yu"],"誊":["teng"],"誓":["shi"],"謇":["jian"],"警":["jing"],"譬":["pi"],"计":["ji"],"订":["ding"],"讣":["fu"],"认":["ren"],"讥":["ji"],"讦":["jie"],"讧":["hong"],"讨":["tao"],"让":["rang"],"讪":["shan"],"讫":["qi"],"训":["xun"],"议":["yi"],"讯":["xun"],"记":["ji"],"讱":["ren"],"讲":["jiang"],"讳":["hui"],"讴":["ou"],"讵":["ju"],"讶":["ya"],"讷":["ne"],"许":["xu"],"讹":["e"],"论":["lun"],"讻":["xiong"],"讼":["song"],"讽":["feng"],"设":["she"],"访":["fang"],"诀":["jue"],"证":["zheng"],"诂":["gu"],"诃":["he"],"评":["ping"],"诅":["zu"],"识":["shi"],"诇":["xiong"],"诈":["zha"],"诉":["su"],"诊":["zhen"],"诋":["di"],"诌":["zhou"],"词":["ci"],"诎":["qu"],"诏":["zhao"],"诐":["bi"],"译":["yi"],"诒":["yi"],"诓":["kuang"],"诔":["lei"],"试":["shi"],"诖":["gua"],"诗":["shi"],"诘":["ji","jie"],"诙":["hui"],"诚":["cheng"],"诛":["zhu"],"诜":["shen"],"话":["hua"],"诞":["dan"],"诟":["gou"],"诠":["quan"],"诡":["gui"],"询":["xun"],"诣":["yi"],"诤":["zheng"],"该":["gai"],"详":["xiang"],"诧":["cha"],"诨":["hun"],"诩":["xu"],"诫":["jie"],"诬":["wu"],"语":["yu"],"诮":["qiao"],"误":["wu"],"诰":["gao"],"诱":["you"],"诲":["hui"],"诳":["kuang"],"说":["shuo"],"诵":["song"],"请":["qing"],"诸":["zhu"],"诹":["zou"],"诺":["nuo"],"读":["du"],"诼":["zhuo"],"诽":["fei"],"课":["ke"],"诿":["wei"],"谀":["yu"],"谁":["shui"],"谂":["shen"],"调":["diao","tiao"],"谄":["chan"],"谅":["liang"],"谆":["zhun"],"谇":["sui"],"谈":["tan"],"谊":["yi"],"谋":["mou"],"谌":["chen"],"谍":["die"],"谎":["huang"],"谏":["jian"],"谐":["xie"],"谑":["xue"],"谒":["ye"],"谓":["wei"],"谔":["e"],"谕":["yu"],"谖":["xuan"],"谗":["chan"],"谙":["an"],"谚":["yan"],"谛":["di"],"谜":["mi"],"谝":["pian"],"谞":["xu"],"谟":["mo"],"谠":["dang"],"谡":["su"],"谢":["xie"],"谣":["yao"],"谤":["bang"],"谥":["shi"],"谦":["qian"],"谧":["mi"],"谨":["jin"],"谩":["man"],"谪":["zhe"],"谫":["jian"],"谬":["miu"],"谭":["tan"],"谮":["zen"],"谯":["qiao"],"谰":["lan"],"谱":["pu"],"谲":["jue"],"谳":["yan"],"谴":["qian"],"谵":["zhan"],"谶":["chen"],"谷":["gu"],"谼":["hong"],"谿":["xi"],"豁":["huo"],"豆":["dou"],"豇":["jiang"],"豉":["shi","chi"],"豌":["wan"],"豕":["shi"],"豚":["tun"],"象":["xiang"],"豢":["huan"],"豨":["xi"],"豪":["hao"],"豫":["yu"],"豮":["fen"],"豳":["bin"],"豸":["zhi"],"豹":["bao"],"豺":["chai"],"貂":["diao"],"貅":["xiu"],"貆":["huan"],"貉":["hao","he"],"貊":["mo"],"貌":["mao"],"貔":["pi"],"貘":["mo"],"贝":["bei"],"贞":["zhen"],"负":["fu"],"贡":["gong"],"财":["cai"],"责":["ze"],"贤":["xian"],"败":["bai"],"账":["zhang"],"货":["huo"],"质":["zhi"],"贩":["fan"],"贪":["tan"],"贫":["pin"],"贬":["bian"],"购":["gou"],"贮":["zhu"],"贯":["guan"],"贰":["er"],"贱":["jian"],"贲":["ben","bi"],"贳":["shi"],"贴":["tie"],"贵":["gui"],"贶":["kuang"],"贷":["dai"],"贸":["mao"],"费":["fei"],"贺":["he"],"贻":["yi"],"贼":["zei"],"贽":["zhi"],"贾":["jia","gu"],"贿":["hui"],"赀":["zi"],"赁":["lin"],"赂":["lu"],"赃":["zang"],"资":["zi"],"赅":["gai"],"赆":["jin"],"赇":["qiu"],"赈":["zhen"],"赉":["lai"],"赊":["she"],"赋":["fu"],"赌":["du"],"赍":["ji"],"赎":["shu"],"赏":["shang"],"赐":["ci"],"赑":["bi"],"赒":["zhou"],"赓":["geng"],"赔":["pei"],"赕":["dan"],"赖":["lai"],"赗":["feng"],"赘":["zhui"],"赙":["fu"],"赚":["zhuan"],"赛":["sai"],"赜":["ze"],"赝":["yan"],"赞":["zan"],"赟":["yun"],"赠":["zeng"],"赡":["shan"],"赢":["ying"],"赣":["gan"],"赤":["chi"],"赦":["she"],"赧":["nan"],"赪":["cheng"],"赫":["he"],"赭":["zhe"],"走":["zou"],"赳":["jiu"],"赴":["fu"],"赵":["zhao"],"赶":["gan"],"起":["qi"],"趁":["chen"],"趄":["ju","qie"],"超":["chao"],"越":["yue"],"趋":["qu"],"趑":["zi"],"趔":["lie"],"趟":["tang"],"趣":["qu"],"趯":["ti"],"趱":["zan"],"足":["zu"],"趴":["pa"],"趵":["bao"],"趸":["dun"],"趺":["fu"],"趼":["jian"],"趾":["zhi"],"趿":["ta"],"跂":["qi"],"跃":["yue"],"跄":["qiang"],"跆":["tai"],"跋":["ba"],"跌":["die"],"跎":["tuo"],"跏":["jia"],"跐":["ci"],"跑":["pao"],"跖":["zhi"],"跗":["fu"],"跚":["shan"],"跛":["bo"],"距":["ju"],"跞":["li"],"跟":["gen"],"跣":["xian"],"跤":["jiao"],"跨":["kua"],"跪":["gui"],"跬":["kui"],"路":["lu"],"跱":["zhi"],"跳":["tiao"],"践":["jian"],"跶":["da"],"跷":["qiao"],"跸":["bi"],"跹":["xian"],"跺":["duo"],"跻":["ji"],"跽":["ji"],"踅":["xue"],"踉":["liang"],"踊":["yong"],"踌":["chou"],"踏":["ta"],"踒":["wo"],"踔":["chuo"],"踝":["huai"],"踞":["ju"],"踟":["chi"],"踢":["ti"],"踣":["bo"],"踦":["yi","qi"],"踩":["cai"],"踪":["zong"],"踬":["zhi"],"踮":["dian"],"踯":["zhi"],"踱":["duo"],"踵":["zhong"],"踶":["di"],"踹":["chuai"],"踺":["jian"],"踽":["ju"],"蹀":["die"],"蹁":["pian"],"蹂":["rou"],"蹄":["ti"],"蹅":["cha"],"蹇":["jian"],"蹈":["dao"],"蹉":["cuo"],"蹊":["qi"],"蹋":["ta"],"蹐":["ji"],"蹑":["nie"],"蹒":["pan"],"蹙":["cu"],"蹚":["tang"],"蹜":["su"],"蹢":["di"],"蹦":["beng"],"蹩":["bie"],"蹬":["deng"],"蹭":["ceng"],"蹯":["fan"],"蹰":["chu"],"蹲":["dun"],"蹴":["cu"],"蹶":["jue"],"蹼":["pu"],"蹽":["liao"],"蹾":["dun"],"蹿":["cuan"],"躁":["zao"],"躅":["zhu"],"躇":["chu"],"躏":["lin"],"躐":["lie"],"躔":["chan"],"躜":["zuan"],"躞":["xie"],"身":["shen"],"躬":["gong"],"躯":["qu"],"躲":["duo"],"躺":["tang"],"车":["che"],"轧":["ya"],"轨":["gui"],"轩":["xuan"],"轪":["dai"],"轫":["ren"],"转":["zhuan"],"轭":["e"],"轮":["lun"],"软":["ruan"],"轰":["hong"],"轱":["gu"],"轲":["ke"],"轳":["lu"],"轴":["zhou"],"轵":["zhi"],"轶":["yi"],"轷":["hu"],"轸":["zhen"],"轹":["li"],"轺":["yao"],"轻":["qing"],"轼":["shi"],"载":["zai"],"轾":["zhi"],"轿":["jiao"],"辀":["zhou"],"辁":["quan"],"辂":["lu"],"较":["jiao"],"辄":["zhe"],"辅":["fu"],"辆":["liang"],"辇":["nian"],"辈":["bei"],"辉":["hui"],"辊":["gun"],"辋":["wang"],"辌":["liang"],"辍":["chuo"],"辎":["zi"],"辏":["cou"],"辐":["fu"],"辑":["ji"],"辒":["wen"],"输":["shu"],"辔":["pei"],"辕":["yuan"],"辖":["xia"],"辗":["nian","zhan"],"辘":["lu"],"辙":["zhe"],"辚":["lin"],"辛":["xin"],"辜":["gu"],"辞":["ci"],"辟":["pi","bi"],"辣":["la"],"辨":["bian"],"辩":["bian"],"辫":["bian"],"辰":["chen"],"辱":["ru"],"边":["bian"],"辽":["liao"],"达":["da"],"辿":["chan"],"迁":["qian"],"迂":["yu"],"迄":["qi"],"迅":["xun"],"过":["guo"],"迈":["mai"],"迎":["ying"],"运":["yun"],"近":["jin"],"迓":["ya"],"返":["fan"],"迕":["wu"],"还":["hai"],"这":["zhe"],"进":["jin"],"远":["yuan"],"违":["wei"],"连":["lian"],"迟":["chi"],"迢":["tiao"],"迤":["yi"],"迥":["jiong"],"迦":["jia"],"迨":["dai"],"迩":["er"],"迪":["di"],"迫":["po"],"迭":["die"],"迮":["ze"],"述":["shu"],"迳":["jing"],"迷":["mi"],"迸":["beng"],"迹":["ji"],"迺":["nai"],"追":["zhui"],"退":["tui"],"送":["song"],"适":["shi"],"逃":["tao"],"逄":["pang"],"逅":["hou"],"逆":["ni"],"选":["xuan"],"逊":["xun"],"逋":["bu"],"逍":["xiao"],"透":["tou"],"逐":["zhu"],"逑":["qiu"],"递":["di"],"途":["tu"],"逖":["ti"],"逗":["dou"],"通":["tong"],"逛":["guang"],"逝":["shi"],"逞":["cheng"],"速":["su"],"造":["zao"],"逡":["qun"],"逢":["feng"],"逦":["li"],"逭":["huan"],"逮":["dai"],"逯":["lu"],"逴":["chuo"],"逵":["kui"],"逶":["wei"],"逸":["yi"],"逻":["luo"],"逼":["bi"],"逾":["yu"],"遁":["dun"],"遂":["sui"],"遄":["chuan"],"遆":["ti","di"],"遇":["yu"],"遍":["bian"],"遏":["e"],"遐":["xia"],"遑":["huang"],"遒":["qiu"],"道":["dao"],"遗":["yi"],"遘":["gou"],"遛":["liu"],"遢":["ta"],"遣":["qian"],"遥":["yao"],"遨":["ao"],"遭":["zao"],"遮":["zhe"],"遴":["lin"],"遵":["zun"],"遹":["yu"],"遽":["ju"],"避":["bi"],"邀":["yao"],"邂":["xie"],"邃":["sui"],"邈":["miao"],"邋":["la"],"邑":["yi"],"邓":["deng"],"邕":["yong"],"邗":["han"],"邘":["yu"],"邙":["mang"],"邛":["qiong"],"邝":["kuang"],"邠":["bin"],"邡":["fang"],"邢":["xing"],"那":["na"],"邦":["bang"],"邨":["cun"],"邪":["xie"],"邬":["wu"],"邮":["you"],"邯":["han"],"邰":["tai"],"邱":["qiu"],"邲":["bi"],"邳":["pi"],"邴":["bing"],"邵":["shao"],"邶":["bei"],"邸":["di"],"邹":["zou"],"邺":["ye"],"邻":["lin"],"邽":["gui"],"邾":["zhu"],"邿":["shi"],"郁":["yu"],"郃":["he"],"郄":["qie"],"郅":["zhi"],"郇":["huan","xun"],"郈":["hou"],"郊":["jiao"],"郎":["lang"],"郏":["jia"],"郐":["kuai"],"郑":["zheng"],"郓":["yun"],"郗":["xi"],"郚":["wu"],"郛":["fu"],"郜":["gao"],"郝":["hao"],"郡":["jun"],"郢":["ying"],"郤":["xi"],"郦":["li"],"郧":["yun"],"部":["bu"],"郪":["qi"],"郫":["pi"],"郭":["guo"],"郯":["tan"],"郴":["chen"],"郸":["dan"],"都":["dou"],"郾":["yan"],"郿":["mei"],"鄀":["ruo"],"鄂":["e"],"鄃":["shu"],"鄄":["juan"],"鄅":["yu"],"鄌":["tang"],"鄑":["zi"],"鄗":["hao"],"鄘":["yong"],"鄙":["bi"],"鄚":["mao"],"鄜":["fu"],"鄞":["yin"],"鄠":["hu"],"鄢":["yan"],"鄣":["zhang"],"鄫":["zeng"],"鄯":["shan"],"鄱":["po"],"鄹":["zou"],"酂":["cuo"],"酃":["ling"],"酅":["xi"],"酆":["feng"],"酉":["you"],"酊":["ding"],"酋":["qiu"],"酌":["zhuo"],"配":["pei"],"酎":["zhou"],"酏":["yi"],"酐":["gan"],"酒":["jiu"],"酗":["xu"],"酚":["fen"],"酝":["yun"],"酞":["tai"],"酡":["tuo"],"酢":["cu","zuo"],"酣":["han"],"酤":["gu"],"酥":["su"],"酦":["po"],"酩":["ming"],"酪":["lao"],"酬":["chou"],"酮":["tong"],"酯":["zhi"],"酰":["xian"],"酱":["jiang"],"酲":["cheng"],"酴":["tu"],"酵":["jiao"],"酶":["mei"],"酷":["ku"],"酸":["suan"],"酹":["lei"],"酺":["pu"],"酽":["yan"],"酾":["shai","shi"],"酿":["niang"],"醅":["pei"],"醇":["chun"],"醉":["zui"],"醋":["cu"],"醌":["kun"],"醍":["ti"],"醐":["hu"],"醑":["xu"],"醒":["xing"],"醚":["mi"],"醛":["quan"],"醢":["hai"],"醨":["li"],"醪":["lao"],"醭":["bu"],"醮":["jiao"],"醯":["xi"],"醴":["li"],"醵":["ju"],"醺":["xun"],"醾":["mi"],"采":["cai"],"釉":["you"],"释":["shi"],"里":["li"],"重":["zhong"],"野":["ye"],"量":["liang"],"釐":["li"],"金":["jin"],"釜":["fu"],"鉴":["jian"],"銎":["qiong"],"銮":["luan"],"鋆":["yun"],"鋈":["wu"],"錾":["zan"],"鍪":["mou"],"鎏":["liu"],"鏊":["ao"],"鏖":["ao"],"鐾":["bei"],"鑫":["xin"],"钆":["ga"],"钇":["yi"],"针":["zhen"],"钉":["ding"],"钊":["zhao"],"钋":["po"],"钌":["liao"],"钍":["tu"],"钎":["qian"],"钏":["chuan"],"钐":["shan"],"钒":["fan"],"钓":["diao"],"钔":["men"],"钕":["nv"],"钖":["yang"],"钗":["chai"],"钘":["xing"],"钙":["gai"],"钚":["bu"],"钛":["tai"],"钜":["ju"],"钝":["dun"],"钞":["chao"],"钟":["zhong"],"钠":["na"],"钡":["bei"],"钢":["gang"],"钣":["ban"],"钤":["qian"],"钥":["yao","yue"],"钦":["qin"],"钧":["jun"],"钨":["wu"],"钩":["gou"],"钪":["kang"],"钫":["fang"],"钬":["huo"],"钭":["tou","dou"],"钮":["niu"],"钯":["ba"],"钰":["yu"],"钱":["qian"],"钲":["zheng"],"钳":["qian"],"钴":["gu"],"钵":["bo"],"钷":["po"],"钹":["bo"],"钺":["yue"],"钻":["zuan"],"钼":["mu"],"钽":["tan"],"钾":["jia"],"钿":["dian"],"铀":["you"],"铁":["tie"],"铂":["bo"],"铃":["ling"],"铄":["shuo"],"铅":["qian"],"铆":["mao"],"铈":["shi"],"铉":["xuan"],"铊":["ta"],"铋":["bi"],"铌":["ni"],"铍":["pi"],"铎":["duo"],"铏":["xing"],"铐":["kao"],"铑":["lao"],"铒":["er"],"铕":["you"],"铖":["cheng"],"铗":["jia"],"铘":["ye"],"铙":["nao"],"铚":["zhi"],"铛":["dang"],"铜":["tong"],"铝":["lv"],"铞":["diao"],"铟":["yin"],"铠":["kai"],"铡":["zha"],"铢":["zhu"],"铣":["xi","xian"],"铤":["ding","ting"],"铥":["diu"],"铧":["hua"],"铨":["quan"],"铩":["sha"],"铪":["ha"],"铫":["diao"],"铬":["ge"],"铭":["ming"],"铮":["zheng"],"铯":["se"],"铰":["jiao"],"铱":["yi"],"铲":["chan"],"铳":["chong"],"铴":["tang"],"铵":["an"],"银":["yin"],"铷":["ru"],"铸":["zhu"],"铹":["lao"],"铺":["pu"],"铻":["wu"],"铼":["lai"],"铽":["te"],"链":["lian"],"铿":["keng"],"销":["xiao"],"锁":["suo"],"锂":["li"],"锃":["zeng"],"锄":["chu"],"锅":["guo"],"锆":["gao"],"锇":["e"],"锈":["xiu"],"锉":["cuo"],"锊":["lüe"],"锋":["feng"],"锌":["xin"],"锍":["liu"],"锎":["kai"],"锏":["jian"],"锐":["rui"],"锑":["ti"],"锒":["lang"],"锓":["qin"],"锔":["ju"],"锕":["a"],"锖":["qiang"],"锗":["zhe"],"锘":["nuo"],"错":["cuo"],"锚":["mao"],"锛":["ben"],"锜":["qi"],"锝":["de"],"锞":["ke"],"锟":["kun"],"锡":["xi"],"锢":["gu"],"锣":["luo"],"锤":["chui"],"锥":["zhui"],"锦":["jin"],"锧":["zhi"],"锨":["xian"],"锩":["juan"],"锪":["huo"],"锫":["pei"],"锬":["tan"],"锭":["ding"],"键":["jian"],"锯":["ju"],"锰":["meng"],"锱":["zi"],"锲":["qie"],"锳":["ying"],"锴":["kai"],"锵":["qiang"],"锶":["si"],"锷":["e"],"锸":["cha"],"锹":["qiao"],"锺":["zhong"],"锻":["duan"],"锼":["sou"],"锽":["huang"],"锾":["huan"],"锿":["ai"],"镀":["du"],"镁":["mei"],"镂":["lou"],"镃":["zi"],"镄":["fei"],"镅":["mei"],"镆":["mo"],"镇":["zhen"],"镈":["bo"],"镉":["ge"],"镊":["nie"],"镋":["tang"],"镌":["juan"],"镍":["nie"],"镎":["na"],"镏":["liu"],"镐":["gao"],"镑":["bang"],"镒":["yi"],"镓":["jia"],"镔":["bin"],"镕":["rong"],"镖":["biao"],"镗":["tang"],"镘":["man"],"镚":["beng"],"镛":["yong"],"镜":["jing"],"镝":["di"],"镞":["zu"],"镠":["liu"],"镡":["chan","xin"],"镢":["jue"],"镣":["liao"],"镤":["pu"],"镥":["lu"],"镦":["dui"],"镧":["lan"],"镨":["pu"],"镩":["cuan"],"镪":["qiang"],"镫":["deng"],"镬":["huo"],"镭":["lei"],"镮":["huan"],"镯":["zhuo"],"镰":["lian"],"镱":["yi"],"镲":["cha"],"镳":["biao"],"镴":["la"],"镵":["chan"],"镶":["xiang"],"长":["zhang","chang"],"门":["men"],"闩":["shuan"],"闪":["shan"],"闫":["yan"],"闭":["bi"],"问":["wen"],"闯":["chuang"],"闰":["run"],"闱":["wei"],"闲":["xian"],"闳":["hong"],"间":["jian"],"闵":["min"],"闶":["kang"],"闷":["men"],"闸":["zha"],"闹":["nao"],"闺":["gui"],"闻":["wen"],"闼":["ta"],"闽":["min"],"闾":["lv"],"闿":["kai"],"阀":["fa"],"阁":["ge"],"阂":["he"],"阃":["kun"],"阄":["jiu"],"阅":["yue"],"阆":["lang"],"阇":["du"],"阈":["yu"],"阉":["yan"],"阊":["chang"],"阋":["xi"],"阌":["wen"],"阍":["hun"],"阎":["yan"],"阏":["e"],"阐":["chan"],"阑":["lan"],"阒":["qu"],"阔":["kuo"],"阕":["que"],"阖":["he"],"阗":["tian"],"阘":["da","ta"],"阙":["que"],"阚":["han","kan"],"阜":["fu"],"队":["dui"],"阡":["qian"],"阪":["ban"],"阮":["ruan"],"阱":["jing"],"防":["fang"],"阳":["yang"],"阴":["yin"],"阵":["zhen"],"阶":["jie"],"阻":["zu"],"阼":["zuo"],"阽":["dian"],"阿":["a"],"陀":["tuo"],"陂":["bei"],"附":["fu"],"际":["ji"],"陆":["lu"],"陇":["long"],"陈":["chen"],"陉":["xing"],"陋":["lou"],"陌":["mo"],"降":["jiang"],"陎":["shu"],"限":["xian"],"陑":["er"],"陔":["gai"],"陕":["shan"],"陛":["bi"],"陞":["sheng"],"陟":["zhi"],"陡":["dou"],"院":["yuan"],"除":["chu"],"陧":["nie"],"陨":["yun"],"险":["xian"],"陪":["pei"],"陬":["zou"],"陲":["chui"],"陴":["pi"],"陵":["ling"],"陶":["tao"],"陷":["xian"],"隃":["shu","yu"],"隅":["yu"],"隆":["long"],"隈":["wei"],"隋":["sui"],"隍":["huang"],"随":["sui"],"隐":["yin"],"隔":["ge"],"隗":["kui","wei"],"隘":["ai"],"隙":["xi"],"障":["zhang"],"隧":["sui"],"隩":["ao"],"隰":["xi"],"隳":["hui"],"隶":["li"],"隹":["zhui"],"隺":["hu"],"隼":["sun"],"隽":["juan","jun"],"难":["nan"],"雀":["que"],"雁":["yan"],"雄":["xiong"],"雅":["ya"],"集":["ji"],"雇":["gu"],"雉":["zhi"],"雊":["gou"],"雌":["ci"],"雍":["yong"],"雎":["ju"],"雏":["chu"],"雒":["luo"],"雕":["diao"],"雠":["chou"],"雨":["yu"],"雩":["yu"],"雪":["xue"],"雯":["wen"],"雱":["pang"],"雳":["li"],"零":["ling"],"雷":["lei"],"雹":["bao"],"雾":["wu"],"需":["xu"],"霁":["ji"],"霄":["xiao"],"霅":["zha"],"霆":["ting"],"震":["zhen"],"霈":["pei"],"霉":["mei"],"霍":["huo"],"霎":["sha"],"霏":["fei"],"霓":["ni"],"霖":["lin"],"霜":["shuang"],"霞":["xia"],"霨":["wei"],"霪":["yin"],"霭":["ai"],"霰":["xian"],"露":["lu","lou"],"霸":["ba"],"霹":["pi"],"霾":["mai"],"青":["qing"],"靓":["jing"],"靖":["jing"],"静":["jing"],"靛":["dian"],"非":["fei"],"靠":["kao"],"靡":["mi"],"面":["mian"],"靥":["ye"],"革":["ge"],"靬":["qian","jian"],"靰":["wu"],"靳":["jin"],"靴":["xue"],"靶":["ba"],"靸":["sa"],"靺":["mo"],"靼":["da"],"靽":["ban"],"靿":["yao"],"鞁":["bei"],"鞅":["yang"],"鞋":["xie"],"鞍":["an"],"鞑":["da"],"鞒":["qiao"],"鞔":["man"],"鞘":["qiao"],"鞠":["ju"],"鞡":["la"],"鞣":["rou"],"鞧":["qiu"],"鞨":["he"],"鞫":["ju"],"鞬":["jian"],"鞭":["bian"],"鞮":["di"],"鞯":["jian"],"鞲":["gou"],"鞳":["ta"],"鞴":["bei"],"韂":["chan"],"韦":["wei"],"韧":["ren"],"韨":["fu"],"韩":["han"],"韪":["wei"],"韫":["yun"],"韬":["tao"],"韭":["jiu"],"音":["yin"],"韵":["yun"],"韶":["shao"],"页":["ye"],"顶":["ding"],"顷":["qing"],"顸":["han"],"项":["xiang"],"顺":["shun"],"须":["xu"],"顼":["xu"],"顽":["wan"],"顾":["gu"],"顿":["dun"],"颀":["qi"],"颁":["ban"],"颂":["song"],"颃":["hang"],"预":["yu"],"颅":["lu"],"领":["ling"],"颇":["po"],"颈":["jing"],"颉":["jie"],"颊":["jia"],"颋":["ting"],"颌":["he"],"颍":["ying"],"颎":["jiong"],"颏":["ke"],"颐":["yi"],"频":["pin"],"颓":["tui"],"颔":["han"],"颖":["ying"],"颗":["ke"],"题":["ti"],"颙":["yong"],"颚":["e"],"颛":["zhuan"],"颜":["yan"],"额":["e"],"颞":["nie"],"颟":["man"],"颠":["dian"],"颡":["sang"],"颢":["hao"],"颤":["chan"],"颥":["ru"],"颦":["pin"],"颧":["quan"],"风":["feng"],"飏":["yang"],"飐":["zhan"],"飑":["biao"],"飒":["sa"],"飓":["ju"],"飔":["si"],"飕":["sou"],"飗":["liu"],"飘":["piao"],"飙":["biao"],"飞":["fei"],"食":["shi"],"飧":["sun"],"飨":["xiang"],"餍":["yan"],"餐":["can"],"餮":["tie"],"饔":["yong"],"饕":["tao"],"饥":["ji"],"饧":["tang","xing"],"饨":["tun"],"饩":["xi"],"饪":["ren"],"饫":["yu"],"饬":["chi"],"饭":["fan"],"饮":["yin"],"饯":["jian"],"饰":["shi"],"饱":["bao"],"饲":["si"],"饳":["duo"],"饴":["yi"],"饵":["er"],"饶":["rao"],"饷":["xiang"],"饸":["he"],"饹":["le","ge"],"饺":["jiao"],"饻":["xi"],"饼":["bing"],"饽":["bo"],"饿":["e"],"馁":["nei"],"馃":["guo"],"馄":["hun"],"馅":["xian"],"馆":["guan"],"馇":["cha"],"馈":["kui"],"馉":["gu"],"馊":["sou"],"馋":["chan"],"馌":["ye"],"馍":["mo"],"馏":["liu"],"馐":["xiu"],"馑":["jin"],"馒":["man"],"馓":["san"],"馔":["zhuan"],"馕":["nang"],"首":["shou"],"馗":["kui"],"馘":["guo"],"香":["xiang"],"馝":["bi"],"馞":["bo"],"馥":["fu"],"馧":["yun"],"馨":["xin"],"马":["ma"],"驭":["yu"],"驮":["tuo"],"驯":["xun"],"驰":["chi"],"驱":["qu"],"驲":["ri"],"驳":["bo"],"驴":["lv"],"驵":["zang"],"驶":["shi"],"驷":["si"],"驸":["fu"],"驹":["ju"],"驺":["zou"],"驻":["zhu"],"驼":["tuo"],"驽":["nu"],"驾":["jia"],"驿":["yi"],"骀":["dai","tai"],"骁":["xiao"],"骂":["ma"],"骃":["yin"],"骄":["jiao"],"骅":["hua"],"骆":["luo"],"骇":["hai"],"骈":["pian"],"骉":["biao"],"骊":["li"],"骋":["cheng"],"验":["yan"],"骍":["xing"],"骎":["qin"],"骏":["jun"],"骐":["qi"],"骑":["qi"],"骒":["ke"],"骓":["zhui"],"骕":["su"],"骖":["can"],"骗":["pian"],"骘":["zhi"],"骙":["kui"],"骚":["sao"],"骛":["wu"],"骜":["ao"],"骝":["liu"],"骞":["qian"],"骟":["shan"],"骠":["biao","piao"],"骡":["luo"],"骢":["cong"],"骣":["chan"],"骤":["zhou"],"骥":["ji"],"骦":["shuang"],"骧":["xiang"],"骨":["gu"],"骰":["tou"],"骱":["jie"],"骶":["di"],"骷":["ku"],"骸":["hai"],"骺":["hou"],"骼":["ge"],"髀":["bi"],"髁":["ke"],"髂":["qia"],"髃":["yu"],"髅":["lou"],"髋":["kuan"],"髌":["bin"],"髎":["liao"],"髑":["du"],"髓":["sui"],"高":["gao"],"髡":["kun"],"髢":["di"],"髦":["mao"],"髫":["tiao"],"髭":["zi"],"髯":["ran"],"髹":["xiu"],"髻":["ji"],"髽":["zhua"],"鬃":["zong"],"鬈":["quan"],"鬏":["jiu"],"鬒":["zhen"],"鬓":["bin"],"鬘":["man"],"鬟":["huan"],"鬣":["lie"],"鬯":["chang"],"鬲":["ge"],"鬶":["gui"],"鬷":["zong"],"鬻":["yu"],"鬼":["gui"],"魁":["kui"],"魂":["hun"],"魃":["ba"],"魄":["po"],"魅":["mei"],"魆":["xu"],"魇":["yan"],"魈":["xiao"],"魉":["liang"],"魋":["tui"],"魍":["wang"],"魏":["wei"],"魑":["chi"],"魔":["mo"],"鱼":["yu"],"鱽":["dao"],"鱾":["ji"],"鱿":["you"],"鲀":["tun"],"鲁":["lu"],"鲂":["fang"],"鲃":["ba"],"鲅":["ba"],"鲆":["ping"],"鲇":["nian"],"鲈":["lu"],"鲉":["you"],"鲊":["zha"],"鲋":["fu"],"鲌":["ba","bo"],"鲍":["bao"],"鲎":["hou"],"鲏":["pi"],"鲐":["tai"],"鲑":["gui"],"鲒":["jie"],"鲔":["wei"],"鲕":["er"],"鲖":["tong"],"鲗":["zei"],"鲘":["hou"],"鲙":["kuai"],"鲚":["ji"],"鲛":["jiao"],"鲜":["xian"],"鲝":["zha"],"鲞":["xiang"],"鲟":["xun"],"鲠":["geng"],"鲡":["li"],"鲢":["lian"],"鲣":["jian"],"鲤":["li"],"鲥":["shi"],"鲦":["tiao"],"鲧":["gun"],"鲨":["sha"],"鲩":["huan"],"鲪":["jun"],"鲫":["ji"],"鲬":["yong"],"鲭":["qing"],"鲮":["ling"],"鲯":["qi"],"鲰":["zou"],"鲱":["fei"],"鲲":["kun"],"鲳":["chang"],"鲴":["gu"],"鲵":["ni"],"鲷":["diao"],"鲸":["jing"],"鲹":["shen"],"鲺":["shi"],"鲻":["zi"],"鲼":["fen"],"鲽":["die"],"鲾":["bi"],"鲿":["chang"],"鳀":["ti"],"鳁":["wen"],"鳂":["wei"],"鳃":["sai"],"鳄":["e"],"鳅":["qiu"],"鳇":["huang"],"鳈":["quan"],"鳉":["jiang"],"鳊":["bian"],"鳌":["ao"],"鳍":["qi"],"鳎":["ta"],"鳏":["guan"],"鳐":["yao"],"鳑":["pang"],"鳒":["jian"],"鳓":["le"],"鳔":["biao"],"鳕":["xue"],"鳖":["bie"],"鳗":["man"],"鳘":["min"],"鳙":["yong"],"鳚":["wei"],"鳛":["xi"],"鳜":["gui"],"鳝":["shan"],"鳞":["lin"],"鳟":["zun"],"鳠":["hu"],"鳡":["gan"],"鳢":["li"],"鳣":["zhan"],"鳤":["guan"],"鸟":["niao"],"鸠":["jiu"],"鸡":["ji"],"鸢":["yuan"],"鸣":["ming"],"鸤":["shi"],"鸥":["ou"],"鸦":["ya"],"鸧":["cang"],"鸨":["bao"],"鸩":["zhen"],"鸪":["gu"],"鸫":["dong"],"鸬":["lu"],"鸭":["ya"],"鸮":["xiao"],"鸯":["yang"],"鸰":["ling"],"鸱":["chi"],"鸲":["qu"],"鸳":["yuan"],"鸵":["tuo"],"鸶":["si"],"鸷":["zhi"],"鸸":["er"],"鸹":["gua"],"鸺":["xiu"],"鸻":["heng"],"鸼":["zhou"],"鸽":["ge"],"鸾":["luan"],"鸿":["hong"],"鹀":["wu"],"鹁":["bo"],"鹂":["li"],"鹃":["juan"],"鹄":["gu","hu"],"鹅":["e"],"鹆":["yu"],"鹇":["xian"],"鹈":["ti"],"鹉":["wu"],"鹊":["que"],"鹋":["miao"],"鹌":["an"],"鹍":["kun"],"鹎":["bei"],"鹏":["peng"],"鹐":["qian"],"鹑":["chun"],"鹒":["geng"],"鹔":["su"],"鹕":["hu"],"鹖":["he"],"鹗":["e"],"鹘":["gu"],"鹙":["qiu"],"鹚":["ci"],"鹛":["mei"],"鹜":["wu"],"鹝":["yi"],"鹞":["yao"],"鹟":["weng"],"鹠":["liu"],"鹡":["ji"],"鹢":["yi"],"鹣":["jian"],"鹤":["he"],"鹦":["ying"],"鹧":["zhe"],"鹨":["liu"],"鹩":["liao"],"鹪":["jiao"],"鹫":["jiu"],"鹬":["yu"],"鹭":["lu"],"鹮":["huan"],"鹯":["zhan"],"鹰":["ying"],"鹱":["hu"],"鹲":["meng"],"鹳":["guan"],"鹴":["shuang"],"鹾":["cuo"],"鹿":["lu"],"麀":["you"],"麂":["ji"],"麇":["jun"],"麈":["zhu"],"麋":["mi"],"麑":["ni"],"麒":["qi"],"麓":["lu"],"麖":["jing"],"麝":["she"],"麟":["lin"],"麦":["mai"],"麸":["fu"],"麹":["qu"],"麻":["ma"],"麽":["mo"],"麾":["hui"],"黄":["huang"],"黇":["tian"],"黉":["hong"],"黍":["shu"],"黎":["li"],"黏":["nian"],"黑":["hei"],"黔":["qian"],"默":["mo"],"黛":["dai"],"黜":["chu"],"黝":["you"],"黟":["yi"],"黠":["xia"],"黡":["yan"],"黢":["qu"],"黥":["qing"],"黧":["li"],"黩":["du"],"黪":["can"],"黯":["an"],"黹":["zhi"],"黻":["fu"],"黼":["fu"],"黾":["min"],"鼋":["yuan"],"鼍":["tuo"],"鼎":["ding"],"鼐":["nai"],"鼒":["zi"],"鼓":["gu"],"鼗":["tao"],"鼙":["pi"],"鼠":["shu"],"鼢":["fen"],"鼩":["qu"],"鼫":["shi"],"鼬":["you"],"鼯":["wu"],"鼱":["jing"],"鼷":["xi"],"鼹":["yan"],"鼻":["bi"],"鼽":["qiu"],"鼾":["han"],"齁":["hou"],"齇":["zha"],"齉":["nang"],"齐":["qi"],"齑":["ji"],"齿":["chi"],"龀":["chen"],"龁":["he"],"龂":["yin"],"龃":["ju"],"龄":["ling"],"龅":["bao"],"龆":["tiao"],"龇":["zi"],"龈":["ken","yin"],"龉":["yu"],"龊":["chuo"],"龋":["qu"],"龌":["wo"],"龙":["long"],"龚":["gong"],"龛":["kan"],"龟":["gui"],"龠":["yue"],"龢":["he"],"鿍":["gang"],"鿎":["ta"],"鿏":["mai"],"㑇":["zhou"],"㑊":["yi"],"㕮":["fu"],"㘎":["han"],"㙍":["duo"],"㙘":["yao"],"㙦":["xie"],"㛃":["jie"],"㛚":["tong"],"㛹":["pian"],"㟃":["si"],"㠇":["jiu"],"㠓":["meng"],"㤘":["zhou","chu"],"㥄":["ling"],"㧐":["song"],"㧑":["hui","wei"],"㧟":["kuai"],"㫰":["lang"],"㬊":["huan"],"㬎":["xian"],"㬚":["che"],"㭎":["gang"],"㭕":["qu"],"㮾":["lang"],"㰀":["li"],"㳇":["fu"],"㳘":["chong"],"㳚":["xu","yu"],"㴔":["xi","se"],"㵐":["jue"],"㶲":["yong"],"㸆":["kao"],"㸌":["huo"],"㺄":["yu"],"㻬":["tu"],"㽏":["gan"],"㿠":["huang"],"䁖":["lou"],"䂮":["lüe"],"䃅":["di"],"䃎":["zha"],"䅟":["can"],"䌹":["jiong"],"䎃":["ran"],"䎖":["zeng"],"䏝":["zhuan","chun"],"䏡":["shi"],"䏲":["die"],"䐃":["jun","jiong"],"䓖":["qiong"],"䓛":["qu","fu"],"䓨":["ying"],"䓫":["qi","ji"],"䓬":["zhuo"],"䗖":["di"],"䗛":["xiu"],"䗪":["zhe"],"䗴":["ting"],"䜣":["xin","xi"],"䝙":["chu"],"䢺":["chu"],"䢼":["gong"],"䣘":["tang"],"䥽":["po"],"䦃":["zhuo"],"䲟":["yin"],"䲠":["chun"],"䲢":["teng"],"䴓":["shi"],"䴔":["jiao"],"䴕":["lie"],"䴖":["jing"],"䴗":["ju"],"䴘":["ti"],"䴙":["pi"],"䶮":["yan"],"𠅤":["xi"],"𠙶":["ou"],"𠳐":["bang"],"𡎚":["pian"],"𡐓":["kang"],"𣗋":["dang"],"𣲗":["wei"],"𣲘":["wu"],"𣸣":["fen"],"𤧛":["di"],"𤩽":["huan"],"𤫉":["xie"],"𥔲":["e"],"𥕢":["cao"],"𥖨":["zao"],"𥻗":["cha"],"𦈡":["xu"],"𦒍":["tong"],"𦙶":["gu"],"𦝼":["lv"],"𦭜":["zhi"],"𦰡":["na"],"𧿹":["mu"],"𨐈":["guang"],"𨙸":["qi"],"𨚕":["bian"],"𨟠":["quan","que"],"𨭉":["ban"],"𨱇":["qiu"],"𨱏":["da"],"𨱑":["huang"],"𨱔":["zun"],"𨺙":["ni"],"𩽾":["an","ān"],"𩾃":["mian"],"𩾌":["kang","kāng"],"𪟝":["ji"],"𪣻":["lou"],"𪤗":["liao"],"𪨰":["qu"],"𪨶":["she"],"𪩘":["yan"],"𪾢":["xian"],"𫄧":["yan"],"𫄨":["chi"],"𫄷":["yi"],"𫄸":["xun"],"𫇭":["wei"],"𫌀":["ji"],"𫍣":["tong"],"𫍯":["xian"],"𫍲":["xiao"],"𫍽":["xuan"],"𫐄":["yue"],"𫐐":["ni"],"𫐓":["bu"],"𫑡":["meng"],"𫓧":["fu"],"𫓯":["ji"],"𫓶":["xuan"],"𫓹":["ji"],"𫔍":["fan"],"𫔎":["jue"],"𫔶":["nie"],"𫖮":["yi"],"𫖯":["fu"],"𫖳":["yun"],"𫗧":["su"],"𫗴":["zhan"],"𫘜":["wen"],"𫘝":["jue"],"𫘦":["tao"],"𫘧":["lu"],"𫘨":["ti"],"𫘪":["yuan"],"𫘬":["xi"],"𫚕":["shi"],"𫚖":["ci","ji"],"𫚭":["lie"],"𫛭":["kuang"],"𫞩":["men"],"𫟅":["liang"],"𫟦":["sui"],"𫟹":["hong"],"𫟼":["da"],"𫠆":["kui"],"𫠊":["xuan"],"𫠜":["ni"],"𫢸":["dan"],"𫫇":["e"],"𫭟":["ou","qiu"],"𫭢":["lun"],"𫭼":["lao"],"𫮃":["shan"],"𫰛":["xing"],"𫵷":["li"],"𫶇":["die","di"],"𫷷":["xin"],"𫸩":["kou"],"𬀩":["wei"],"𬀪":["xian"],"𬂩":["jia"],"𬃊":["zhi"],"𬇕":["wan","man"],"𬇙":["pei"],"𬇹":["guo"],"𬉼":["ou"],"𬊈":["xun"],"𬊤":["chan","dan"],"𬌗":["he"],"𬍛":["li"],"𬍡":["dang"],"𬍤":["xun"],"𬒈":["que","hu"],"𬒔":["geng"],"𬒗":["lan"],"𬕂":["gong","gan","long"],"𬘓":["xun"],"𬘘":["dan"],"𬘡":["yin"],"𬘩":["ting"],"𬘫":["huan","wan"],"𬘬":["qian","qing","zheng"],"𬘭":["lin","chen"],"𬘯":["zhun"],"𬙂":["yan","yin"],"𬙊":["mo"],"𬙋":["xiang","rang"],"𬜬":["man"],"𬜯":["liang"],"𬞟":["pin","ping"],"𬟁":["yi"],"𬟽":["dong"],"𬣙":["xu"],"𬣞":["zhu"],"𬣡":["jian"],"𬣳":["hen"],"𬤇":["yin"],"𬤊":["shi","di"],"𬤝":["hui"],"𬨂":["qi"],"𬨎":["you"],"𬩽":["xun"],"𬪩":["nong"],"𬬩":["yi"],"𬬭":["lun"],"𬬮":["chang"],"𬬱":["jin"],"𬬸":["shu"],"𬬹":["shen"],"𬬻":["lu"],"𬬿":["zhao"],"𬭁":["mu"],"𬭊":["du"],"𬭎":["hong"],"𬭚":["chun"],"𬭛":["bo"],"𬭤":["hou"],"𬭩":["weng"],"𬭬":["wei"],"𬭯":["pie"],"𬭳":["xi"],"𬭶":["hei"],"𬭸":["lin"],"𬭼":["sui"],"𬮱":["yin"],"𬮿":["qi","gai","ai"],"𬯀":["ji"],"𬯎":["tui"],"𬱖":["di"],"𬱟":["wei"],"𬳵":["pi"],"𬳶":["jiong"],"𬳽":["shen"],"𬳿":["tu"],"𬴂":["fei"],"𬴃":["huo"],"𬴊":["lin"],"𬶋":["ju"],"𬶍":["tuo"],"𬶏":["wei"],"𬶐":["zhao"],"𬶟":["la"],"𬶠":["lian"],"𬶨":["ji"],"𬶭":["ji"],"𬶮":["xi"],"𬷕":["bu"],"𬸘":["yan"],"𬸚":["yue"],"𬸣":["xian"],"𬸦":["zhuo"],"𬸪":["fan"],"𬹼":["xie"],"𬺈":["yi"],"𬺓":["chu"]} \ No newline at end of file diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/renderer.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/renderer.ts new file mode 100644 index 000000000..6e1fd18cf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/renderer.ts @@ -0,0 +1,348 @@ + +const easeInOutQuad = (t: number, b: number, c: number, d: number): number => { + t /= d / 2 + if (t < 1) return (c / 2) * t * t + b + t-- + return (-c / 2) * (t * (t - 2) - 1) + b +} + +type Noop = () => void +const noop: Noop = () => {} +type ScrollElement = { + lx_scrollLockKey?: number + lx_scrollNextParams?: [ScrollElement, number, number, Noop] + lx_scrollTimeout?: number + lx_scrollDelayTimeout?: number +} & T + +const handleScrollY = (element: ScrollElement, to: number, duration = 300, fn = noop): Noop => { + if (!element) { + fn() + return noop + } + const clean = () => { + element.lx_scrollLockKey = undefined + element.lx_scrollNextParams = undefined + if (element.lx_scrollTimeout) window.clearTimeout(element.lx_scrollTimeout) + element.lx_scrollTimeout = undefined + } + if (element.lx_scrollLockKey) { + element.lx_scrollNextParams = [element, to, duration, fn] + element.lx_scrollLockKey = -1 + return clean + } + // @ts-expect-error + const start = element.scrollTop ?? element.scrollY ?? 0 + if (to > start) { + let maxScrollTop = element.scrollHeight - element.clientHeight + if (to > maxScrollTop) to = maxScrollTop + } else if (to < start) { + if (to < 0) to = 0 + } else { + fn() + return noop + } + const change = to - start + const increment = 10 + if (!change) { + fn() + return noop + } + + let currentTime = 0 + let val: number + let key = Math.random() + + const animateScroll = () => { + element.lx_scrollTimeout = undefined + // if (element.lx_scrollLockKey != key) { + if (element.lx_scrollNextParams && currentTime > duration * 0.75) { + const [_element, to, duration, fn] = element.lx_scrollNextParams + clean() + handleScrollY(_element, to, duration, fn) + return + } + + currentTime += increment + val = Math.trunc(easeInOutQuad(currentTime, start, change, duration)) + if (element.scrollTo) { + element.scrollTo(0, val) + } else { + element.scrollTop = val + } + if (currentTime < duration) { + element.lx_scrollTimeout = window.setTimeout(animateScroll, increment) + } else { + if (element.lx_scrollNextParams) { + const [_element, to, duration, fn] = element.lx_scrollNextParams + clean() + handleScrollY(_element, to, duration, fn) + } else { + clean() + fn() + } + } + } + + element.lx_scrollLockKey = key + animateScroll() + + return clean +} +/** + * 设置滚动条位置 + * @param {*} element 要设置滚动的容器 dom + * @param {*} to 滚动的目标位置 + * @param {*} duration 滚动完成时间 ms + * @param {*} fn 滚动完成后的回调 + * @param {*} delay 延迟执行时间 + */ +export const scrollTo = (element: ScrollElement, to: number, duration = 300, fn = () => {}, delay = 0): () => void => { + let cancelFn: () => void + if (element.lx_scrollDelayTimeout != null) { + window.clearTimeout(element.lx_scrollDelayTimeout) + element.lx_scrollDelayTimeout = undefined + } + if (delay) { + let scrollCancelFn: Noop + cancelFn = () => { + if (element.lx_scrollDelayTimeout == null) { + scrollCancelFn?.() + } else { + window.clearTimeout(element.lx_scrollDelayTimeout) + element.lx_scrollDelayTimeout = undefined + } + } + element.lx_scrollDelayTimeout = window.setTimeout(() => { + element.lx_scrollDelayTimeout = undefined + scrollCancelFn = handleScrollY(element, to, duration, fn) + }, delay) + } else { + cancelFn = handleScrollY(element, to, duration, fn) ?? noop + } + return cancelFn +} +const handleScrollX = (element: ScrollElement, to: number, duration = 300, fn = () => {}): () => void => { + if (!element) { + fn() + return noop + } + const clean = () => { + element.lx_scrollLockKey = undefined + element.lx_scrollNextParams = undefined + if (element.lx_scrollTimeout) window.clearTimeout(element.lx_scrollTimeout) + element.lx_scrollTimeout = undefined + } + if (element.lx_scrollLockKey) { + element.lx_scrollNextParams = [element, to, duration, fn] + element.lx_scrollLockKey = -1 + return clean + } + // @ts-expect-error + const start = element.scrollLeft || element.scrollX || 0 + if (to > start) { + let maxScrollLeft = element.scrollWidth - element.clientWidth + if (to > maxScrollLeft) to = maxScrollLeft + } else if (to < start) { + if (to < 0) to = 0 + } else { + fn() + return noop + } + const change = to - start + const increment = 10 + if (!change) { + fn() + return noop + } + + let currentTime = 0 + let val: number + let key = Math.random() + + const animateScroll = () => { + element.lx_scrollTimeout = undefined + if (element.lx_scrollNextParams && currentTime > duration * 0.75) { + const [_element, to, duration, fn] = element.lx_scrollNextParams + clean() + handleScrollY(_element, to, duration, fn) + return + } + + currentTime += increment + val = Math.trunc(easeInOutQuad(currentTime, start, change, duration)) + if (element.scrollTo) { + element.scrollTo(val, 0) + } else { + element.scrollLeft = val + } + if (currentTime < duration) { + element.lx_scrollTimeout = window.setTimeout(animateScroll, increment) + } else { + if (element.lx_scrollNextParams) { + const [_element, to, duration, fn] = element.lx_scrollNextParams + clean() + handleScrollY(_element, to, duration, fn) + } else { + clean() + fn() + } + } + } + element.lx_scrollLockKey = key + animateScroll() + return clean +} +/** + * 设置滚动条位置 + * @param {*} element 要设置滚动的容器 dom + * @param {*} to 滚动的目标位置 + * @param {*} duration 滚动完成时间 ms + * @param {*} fn 滚动完成后的回调 + * @param {*} delay 延迟执行时间 + */ +export const scrollXTo = (element: ScrollElement, to: number, duration = 300, fn = () => {}, delay = 0): () => void => { + let cancelFn: Noop + if (element.lx_scrollDelayTimeout != null) { + window.clearTimeout(element.lx_scrollDelayTimeout) + element.lx_scrollDelayTimeout = undefined + } + if (delay) { + let scrollCancelFn: Noop + cancelFn = () => { + if (element.lx_scrollDelayTimeout == null) { + scrollCancelFn?.() + } else { + window.clearTimeout(element.lx_scrollDelayTimeout) + element.lx_scrollDelayTimeout = undefined + } + } + element.lx_scrollDelayTimeout = window.setTimeout(() => { + element.lx_scrollDelayTimeout = undefined + scrollCancelFn = handleScrollX(element, to, duration, fn) + }, delay) + } else { + cancelFn = handleScrollX(element, to, duration, fn) + } + return cancelFn +} + +const handleScrollXR = (element: ScrollElement, to: number, duration = 300, fn = () => {}): () => void => { + if (!element) { + fn() + return noop + } + const clean = () => { + element.lx_scrollLockKey = undefined + element.lx_scrollNextParams = undefined + if (element.lx_scrollTimeout) window.clearTimeout(element.lx_scrollTimeout) + element.lx_scrollTimeout = undefined + } + if (element.lx_scrollLockKey) { + element.lx_scrollNextParams = [element, to, duration, fn] + element.lx_scrollLockKey = -1 + return clean + } + // @ts-expect-error + const start = element.scrollLeft || element.scrollX as number || 0 + if (to < start) { + let maxScrollLeft = -element.scrollWidth + element.clientWidth + if (to < maxScrollLeft) to = maxScrollLeft + } else if (to > start) { + if (to > 0) to = 0 + } else { + fn() + return noop + } + + const change = to - start + const increment = 10 + if (!change) { + fn() + return noop + } + + let currentTime = 0 + let val: number + let key = Math.random() + + const animateScroll = () => { + element.lx_scrollTimeout = undefined + if (element.lx_scrollNextParams && currentTime > duration * 0.75) { + const [_element, to, duration, fn] = element.lx_scrollNextParams + clean() + handleScrollY(_element, to, duration, fn) + return + } + + currentTime += increment + val = Math.trunc(easeInOutQuad(currentTime, start, change, duration)) + + if (element.scrollTo) { + element.scrollTo(val, 0) + } else { + element.scrollLeft = val + } + if (currentTime < duration) { + element.lx_scrollTimeout = window.setTimeout(animateScroll, increment) + } else { + if (element.lx_scrollNextParams) { + const [_element, to, duration, fn] = element.lx_scrollNextParams + clean() + handleScrollY(_element, to, duration, fn) + } else { + clean() + fn() + } + } + } + + element.lx_scrollLockKey = key + animateScroll() + + return clean +} +/** + * 设置滚动条位置 (writing-mode: vertical-rl 专用) + * @param element 要设置滚动的容器 dom + * @param to 滚动的目标位置 + * @param duration 滚动完成时间 ms + * @param fn 滚动完成后的回调 + * @param delay 延迟执行时间 + */ +export const scrollXRTo = (element: ScrollElement, to: number, duration = 300, fn = () => {}, delay = 0): () => void => { + let cancelFn: Noop + if (element.lx_scrollDelayTimeout != null) { + window.clearTimeout(element.lx_scrollDelayTimeout) + element.lx_scrollDelayTimeout = undefined + } + if (delay) { + let scrollCancelFn: Noop + cancelFn = () => { + if (element.lx_scrollDelayTimeout == null) { + scrollCancelFn?.() + } else { + window.clearTimeout(element.lx_scrollDelayTimeout) + element.lx_scrollDelayTimeout = undefined + } + } + element.lx_scrollDelayTimeout = window.setTimeout(() => { + element.lx_scrollDelayTimeout = undefined + scrollCancelFn = handleScrollXR(element, to, duration, fn) + }, delay) + } else { + cancelFn = handleScrollXR(element, to, duration, fn) + } + return cancelFn +} + + +/** + * 设置标题 + */ +let dom_title = document.getElementsByTagName('title')[0] +export const setTitle = (title: string | null) => { + title ||= 'LX Music' + dom_title.innerText = title +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/request.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/request.ts new file mode 100644 index 000000000..2c224232d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/request.ts @@ -0,0 +1,274 @@ +import qs from 'node:querystring' +import { + FormData, + getGlobalDispatcher, + interceptors, + request as nodeRrequest, + ProxyAgent, + setGlobalDispatcher, + type Dispatcher, +} from 'undici' + +const defaultOptions: Options = { + timeout: 15000, + 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', + }, + maxRedirect: 5, +} as const +const redirectDispatcher = interceptors.redirect({ maxRedirections: defaultOptions.maxRedirect }) +const dispatchers = [ + interceptors.retry({ + maxRetries: 3, + minTimeout: 1000, + maxTimeout: 10000, + timeoutFactor: 2, + retryAfter: true, + }), + // interceptors.responseError(), +] as const +let proxyAgent: ProxyAgent | null = null +let globalDispatcher = getGlobalDispatcher() +const buildDispatcher = (redirectDispatcher: Dispatcher.DispatcherComposeInterceptor | null, retryNum = 3) => { + const otherInterceptors = + retryNum == 3 + ? dispatchers + : [ + interceptors.retry({ + maxRetries: retryNum, + minTimeout: 1000, + maxTimeout: 6000, + timeoutFactor: 2, + retryAfter: true, + }), + ] + if (redirectDispatcher) { + return (proxyAgent ?? globalDispatcher).compose(redirectDispatcher, ...otherInterceptors) + } + return (proxyAgent ?? globalDispatcher).compose(...otherInterceptors) +} + +setGlobalDispatcher(buildDispatcher(redirectDispatcher)) + +export const setProxy = (url?: string) => { + proxyAgent = url ? new ProxyAgent(url) : null + setGlobalDispatcher(buildDispatcher(redirectDispatcher)) +} +export const setProxyByHost = (host?: string, port?: string) => { + setProxy(host ? `http://${host}:${port}` : undefined) +} +const CONTENT_TYPE = { + json: 'application/json', + form: 'application/x-www-form-urlencoded', + text: 'text/plain', + formdata: 'multipart/form-data', + xml: 'application/xml', + binary: 'application/octet-stream', +} +type ParamsData = Record +export interface Options { + method?: + | 'GET' + | 'HEAD' + | 'POST' + | 'PUT' + | 'DELETE' + | 'OPTIONS' + | 'PATCH' + | 'PROPFIND' + | 'COPY' + | 'MOVE' + | 'MKCOL' + | 'PROPPATCH' + | 'QUOTA' + query?: ParamsData + headers?: Record + timeout?: number + maxRedirect?: number + signal?: AbortController['signal'] + json?: Record + form?: ParamsData + binary?: Buffer | Uint8Array + text?: string + formdata?: Record + xml?: string + needBody?: boolean + needRaw?: boolean + retryNum?: number +} +export interface Response { + headers: { + accept?: string | undefined + 'accept-language'?: string | undefined + 'accept-patch'?: string | undefined + 'accept-ranges'?: string | undefined + 'access-control-allow-credentials'?: string | undefined + 'access-control-allow-headers'?: string | undefined + 'access-control-allow-methods'?: string | undefined + 'access-control-allow-origin'?: string | undefined + 'access-control-expose-headers'?: string | undefined + 'access-control-max-age'?: string | undefined + 'access-control-request-headers'?: string | undefined + 'access-control-request-method'?: string | undefined + age?: string | undefined + allow?: string | undefined + 'alt-svc'?: string | undefined + authorization?: string | undefined + 'cache-control'?: string | undefined + connection?: string | undefined + 'content-disposition'?: string | undefined + 'content-encoding'?: string | undefined + 'content-language'?: string | undefined + 'content-length'?: string | undefined + 'content-location'?: string | undefined + 'content-range'?: string | undefined + 'content-type'?: string | undefined + cookie?: string | undefined + date?: string | undefined + etag?: string | undefined + expect?: string | undefined + expires?: string | undefined + forwarded?: string | undefined + from?: string | undefined + host?: string | undefined + 'if-match'?: string | undefined + 'if-modified-since'?: string | undefined + 'if-none-match'?: string | undefined + 'if-unmodified-since'?: string | undefined + 'last-modified'?: string | undefined + location?: string | undefined + origin?: string | undefined + pragma?: string | undefined + 'proxy-authenticate'?: string | undefined + 'proxy-authorization'?: string | undefined + 'public-key-pins'?: string | undefined + range?: string | undefined + referer?: string | undefined + 'retry-after'?: string | undefined + 'sec-websocket-accept'?: string | undefined + 'sec-websocket-extensions'?: string | undefined + 'sec-websocket-key'?: string | undefined + 'sec-websocket-protocol'?: string | undefined + 'sec-websocket-version'?: string | undefined + 'set-cookie'?: string[] | undefined + 'strict-transport-security'?: string | undefined + tk?: string | undefined + trailer?: string | undefined + 'transfer-encoding'?: string | undefined + upgrade?: string | undefined + 'user-agent'?: string | undefined + vary?: string | undefined + via?: string | undefined + warning?: string | undefined + 'www-authenticate'?: string | undefined + } + body: Res + raw: Uint8Array + statusCode?: number + statusMessage?: string +} + +const buildRequestBody = (options: Options) => { + let contentType: string | undefined + let body: string | Buffer | Uint8Array | FormData | undefined + if (options.json != null) { + contentType = CONTENT_TYPE.json + body = JSON.stringify(options.json) + } else if (options.form != null) { + contentType = CONTENT_TYPE.form + body = qs.stringify(options.form) + } else if (options.binary != null) { + contentType = CONTENT_TYPE.binary + body = options.binary + } else if (options.formdata != null) { + const formdata = new FormData() + // eslint-disable-next-line guard-for-in + for (const key in options.formdata) { + formdata.append(key, options.formdata[key]) + } + contentType = CONTENT_TYPE.formdata + body = formdata + } else if (options.xml != null) { + contentType = CONTENT_TYPE.xml + body = options.xml + } else if (options.text != null) { + contentType = CONTENT_TYPE.text + body = options.text + } + const headers = options.headers ?? {} + if (headers['Content-Type']) { + contentType = headers['Content-Type'] as string + } + const finalHeaders: NonNullable = { ...defaultOptions.headers, ...headers } + if (contentType) finalHeaders['Content-Type'] = contentType + return [finalHeaders, body] as const +} + +const buildRequestDispatcher = (options: Options) => { + let dispatcher: Dispatcher.ComposedDispatcher | undefined + + if (options.maxRedirect != null) { + if (options.maxRedirect != defaultOptions.maxRedirect) { + if (options.maxRedirect) { + dispatcher = buildDispatcher(interceptors.redirect({ maxRedirections: options.maxRedirect }), options.retryNum) + } else { + dispatcher = buildDispatcher(null, options.retryNum) + } + } + } + return dispatcher +} + +export const request = async (url: string, options: Options = {}): Promise> => { + const method = options.method?.toUpperCase() ?? 'GET' + const timeout = options.timeout ?? defaultOptions.timeout + const [headers, body] = buildRequestBody(options) + // console.log(url, { + // method, + // bodyTimeout: timeout, + // headersTimeout: timeout, + // headers, + // query: options.query, + // body, + // signal: options.signal, + // dispatcher: buildRequestDispatcher(options), + // }) + return nodeRrequest(url, { + method, + bodyTimeout: timeout, + headersTimeout: timeout, + headers, + query: options.query, + body, + signal: options.signal, + dispatcher: buildRequestDispatcher(options), + }).then(async(response) => { + if (options.needBody) { + return { + headers: response.headers, + statusCode: response.statusCode, + body: response.body as unknown as T, + } satisfies Omit, 'raw'> as Response + } + if (options.needRaw) { + return { + headers: response.headers, + statusCode: response.statusCode, + raw: await response.body.bytes(), + } satisfies Omit, 'body'> as Response + } + // console.log(response) + let body = (await response.body.text()) as T + if (!headers['Content-Type'] || headers['Content-Type'].includes(CONTENT_TYPE.json)) { + try { + body = JSON.parse(body as string) as T + } catch {} + } + return { + body, + headers: response.headers, + statusCode: response.statusCode, + } satisfies Omit, 'raw'> as Response + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/request_node16.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/request_node16.ts new file mode 100644 index 000000000..048de78d0 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/request_node16.ts @@ -0,0 +1,238 @@ +import qs from 'node:querystring' +import { + FormData, + getGlobalDispatcher, + request as nodeRrequest, + ProxyAgent, + setGlobalDispatcher, + type Dispatcher, +} from 'undici' + +const defaultOptions: Options = { + timeout: 15000, + 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', + }, + maxRedirect: 5, +} as const +let proxyAgent: ProxyAgent | null = null +let globalDispatcher = getGlobalDispatcher() +const buildDispatcher = () => { + return proxyAgent ?? globalDispatcher +} + +setGlobalDispatcher(buildDispatcher()) + +export const setProxy = (url?: string) => { + proxyAgent = url ? new ProxyAgent(url) : null + setGlobalDispatcher(buildDispatcher()) +} +export const setProxyByHost = (host?: string, port?: string) => { + console.log(host) + setProxy(host ? `http://${host}:${port}` : undefined) +} +const CONTENT_TYPE = { + json: 'application/json', + form: 'application/x-www-form-urlencoded', + text: 'text/plain', + formdata: 'multipart/form-data', + xml: 'application/xml', + binary: 'application/octet-stream', +} +type ParamsData = Record +export interface Options { + method?: + | 'GET' + | 'HEAD' + | 'POST' + | 'PUT' + | 'DELETE' + | 'OPTIONS' + | 'PATCH' + | 'PROPFIND' + | 'COPY' + | 'MOVE' + | 'MKCOL' + | 'PROPPATCH' + | 'QUOTA' + query?: ParamsData + headers?: Record + timeout?: number + maxRedirect?: number + signal?: AbortController['signal'] + json?: Record + form?: ParamsData + binary?: Buffer | Uint8Array + text?: string + formdata?: Record + xml?: string + needBody?: boolean + needRaw?: boolean + retryNum?: number +} +export interface Response { + headers: { + accept?: string | undefined + 'accept-language'?: string | undefined + 'accept-patch'?: string | undefined + 'accept-ranges'?: string | undefined + 'access-control-allow-credentials'?: string | undefined + 'access-control-allow-headers'?: string | undefined + 'access-control-allow-methods'?: string | undefined + 'access-control-allow-origin'?: string | undefined + 'access-control-expose-headers'?: string | undefined + 'access-control-max-age'?: string | undefined + 'access-control-request-headers'?: string | undefined + 'access-control-request-method'?: string | undefined + age?: string | undefined + allow?: string | undefined + 'alt-svc'?: string | undefined + authorization?: string | undefined + 'cache-control'?: string | undefined + connection?: string | undefined + 'content-disposition'?: string | undefined + 'content-encoding'?: string | undefined + 'content-language'?: string | undefined + 'content-length'?: string | undefined + 'content-location'?: string | undefined + 'content-range'?: string | undefined + 'content-type'?: string | undefined + cookie?: string | undefined + date?: string | undefined + etag?: string | undefined + expect?: string | undefined + expires?: string | undefined + forwarded?: string | undefined + from?: string | undefined + host?: string | undefined + 'if-match'?: string | undefined + 'if-modified-since'?: string | undefined + 'if-none-match'?: string | undefined + 'if-unmodified-since'?: string | undefined + 'last-modified'?: string | undefined + location?: string | undefined + origin?: string | undefined + pragma?: string | undefined + 'proxy-authenticate'?: string | undefined + 'proxy-authorization'?: string | undefined + 'public-key-pins'?: string | undefined + range?: string | undefined + referer?: string | undefined + 'retry-after'?: string | undefined + 'sec-websocket-accept'?: string | undefined + 'sec-websocket-extensions'?: string | undefined + 'sec-websocket-key'?: string | undefined + 'sec-websocket-protocol'?: string | undefined + 'sec-websocket-version'?: string | undefined + 'set-cookie'?: string[] | undefined + 'strict-transport-security'?: string | undefined + tk?: string | undefined + trailer?: string | undefined + 'transfer-encoding'?: string | undefined + upgrade?: string | undefined + 'user-agent'?: string | undefined + vary?: string | undefined + via?: string | undefined + warning?: string | undefined + 'www-authenticate'?: string | undefined + } + body: Res + raw: Uint8Array + statusCode?: number + statusMessage?: string +} + +const buildRequestBody = (options: Options) => { + let contentType: string | undefined + let body: string | Buffer | Uint8Array | FormData | undefined + if (options.json != null) { + contentType = CONTENT_TYPE.json + body = JSON.stringify(options.json) + } else if (options.form != null) { + contentType = CONTENT_TYPE.form + body = qs.stringify(options.form) + } else if (options.binary != null) { + contentType = CONTENT_TYPE.binary + body = options.binary + } else if (options.formdata != null) { + const formdata = new FormData() + // eslint-disable-next-line guard-for-in + for (const key in options.formdata) { + formdata.append(key, options.formdata[key]) + } + contentType = CONTENT_TYPE.formdata + body = formdata + } else if (options.xml != null) { + contentType = CONTENT_TYPE.xml + body = options.xml + } else if (options.text != null) { + contentType = CONTENT_TYPE.text + body = options.text + } + const headers = options.headers ?? {} + if (headers['Content-Type']) { + contentType = headers['Content-Type'] as string + } + const finalHeaders: NonNullable = { ...defaultOptions.headers, ...headers } + if (contentType) finalHeaders['Content-Type'] = contentType + return [finalHeaders, body] as const +} + +const buildRequestDispatcher = (options: Options) => { + return buildDispatcher() +} + +export const request = async (url: string, options: Options = {}): Promise> => { + const method = (options.method?.toUpperCase() ?? 'GET') as Dispatcher.RequestOptions['method'] + const timeout = options.timeout ?? defaultOptions.timeout + const [headers, body] = buildRequestBody(options) + // console.log(url, { + // method, + // bodyTimeout: timeout, + // headersTimeout: timeout, + // headers, + // query: options.query, + // body, + // signal: options.signal, + // dispatcher: buildRequestDispatcher(options), + // }) + return nodeRrequest(url, { + method, + bodyTimeout: timeout, + headersTimeout: timeout, + headers, + query: options.query, + body, + signal: options.signal, + dispatcher: buildRequestDispatcher(options), + maxRedirections: options.maxRedirect ?? defaultOptions.maxRedirect, + }).then(async(response) => { + if (options.needBody) { + return { + headers: response.headers, + statusCode: response.statusCode, + body: response.body as unknown as T, + } satisfies Omit, 'raw'> as Response + } + if (options.needRaw) { + return { + headers: response.headers, + statusCode: response.statusCode, + raw: new Uint8Array(await response.body.arrayBuffer()), + } satisfies Omit, 'body'> as unknown as Response + } + // console.log(response) + let body = (await response.body.text()) as T + if (!headers['Content-Type'] || headers['Content-Type'].includes(CONTENT_TYPE.json)) { + try { + body = JSON.parse(body as string) as T + } catch {} + } + return { + body, + headers: response.headers, + statusCode: response.statusCode, + } satisfies Omit, 'raw'> as Response + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/tools.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/tools.ts new file mode 100644 index 000000000..287b395cf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/tools.ts @@ -0,0 +1,156 @@ +// 业务工具方法 + +export const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfo => { + const meta: Record = { + songId: oldMusicInfo.songmid, // 歌曲ID,local为文件路径 + albumName: oldMusicInfo.albumName, // 歌曲专辑名称 + picUrl: oldMusicInfo.img, // 歌曲图片链接 + } + const newInfo = { + id: `${oldMusicInfo.source}_${oldMusicInfo.songmid}`, + name: oldMusicInfo.name, + singer: oldMusicInfo.singer, + source: oldMusicInfo.source, + interval: oldMusicInfo.interval, + meta: meta as LX.Music.MusicInfoOnline['meta'], + } + + if (oldMusicInfo.source == 'local') { + meta.filePath = oldMusicInfo.filePath ?? oldMusicInfo.songmid ?? '' + meta.ext = oldMusicInfo.ext ?? /\.(\w+)$/.exec(meta.filePath)?.[1] ?? '' + } else { + meta.qualitys = oldMusicInfo.types + meta._qualitys = oldMusicInfo._types + meta.albumId = oldMusicInfo.albumId + if (meta._qualitys.flac32bit && !meta._qualitys.flac24bit) { + meta._qualitys.flac24bit = meta._qualitys.flac32bit + delete meta._qualitys.flac32bit + + meta.qualitys = (meta.qualitys as any[]).map(quality => { + if (quality.type == 'flac32bit') quality.type = 'flac24bit' + return quality + }) + } + + switch (oldMusicInfo.source) { + case 'kg': + meta.hash = oldMusicInfo.hash + meta.albumAudioId = oldMusicInfo.albumAudioId + newInfo.id = oldMusicInfo.songmid + '_' + oldMusicInfo.hash + break + case 'tx': + meta.strMediaMid = oldMusicInfo.strMediaMid + meta.id = oldMusicInfo.songId + meta.albumMid = oldMusicInfo.albumMid + break + case 'mg': + meta.copyrightId = oldMusicInfo.copyrightId + meta.lrcUrl = oldMusicInfo.lrcUrl + meta.mrcUrl = oldMusicInfo.mrcUrl + meta.trcUrl = oldMusicInfo.trcUrl + break + } + } + + return newInfo +} + +export const toOldMusicInfo = (minfo: LX.Music.MusicInfo) => { + const oInfo: Record = { + name: minfo.name, + singer: minfo.singer, + source: minfo.source, + songmid: minfo.meta.songId, + interval: minfo.interval, + albumName: minfo.meta.albumName, + img: minfo.meta.picUrl ?? '', + typeUrl: {}, + } + if (minfo.source == 'local') { + oInfo.filePath = minfo.meta.filePath + oInfo.ext = minfo.meta.ext + oInfo.albumId = '' + oInfo.types = [] + oInfo._types = {} + } else { + oInfo.albumId = minfo.meta.albumId + oInfo.types = minfo.meta.qualitys + oInfo._types = minfo.meta._qualitys + + switch (minfo.source) { + case 'kg': + oInfo.hash = minfo.meta.hash + oInfo.albumAudioId = minfo.meta.albumAudioId + break + case 'tx': + oInfo.strMediaMid = minfo.meta.strMediaMid + oInfo.albumMid = minfo.meta.albumMid + oInfo.songId = minfo.meta.id + break + case 'mg': + oInfo.copyrightId = minfo.meta.copyrightId + oInfo.lrcUrl = minfo.meta.lrcUrl + oInfo.mrcUrl = minfo.meta.mrcUrl + oInfo.trcUrl = minfo.meta.trcUrl + break + } + } + + return oInfo +} + +/** + * 修复2.0.0-dev.8之前的新列表数据音质 + * @param musicInfo + */ +export const fixNewMusicInfoQuality = (musicInfo: LX.Music.MusicInfo) => { + if (musicInfo.source == 'local') return musicInfo + + // @ts-expect-error + if (musicInfo.meta._qualitys.flac32bit && !musicInfo.meta._qualitys.flac24bit) { + // @ts-expect-error + musicInfo.meta._qualitys.flac24bit = musicInfo.meta._qualitys.flac32bit + // @ts-expect-error + delete musicInfo.meta._qualitys.flac32bit + + musicInfo.meta.qualitys = musicInfo.meta.qualitys.map(quality => { + // @ts-expect-error + if (quality.type == 'flac32bit') quality.type = 'flac24bit' + return quality + }) + } + + return musicInfo +} + +export const filterMusicList = (list: T[]): T[] => { + const ids = new Set() + return list.filter(s => { + if (!s.id || ids.has(s.id) || !s.name) return false + if (s.singer == null) s.singer = '' + ids.add(s.id) + return true + }) +} + + +const MAX_NAME_LENGTH = 80 +const MAX_FILE_NAME_LENGTH = 150 +export const clipNameLength = (name: string) => { + if (name.length <= MAX_NAME_LENGTH || !name.includes('、')) return name + const names = name.split('、') + let newName = names.shift()! + for (const name of names) { + if (newName.length + name.length > MAX_NAME_LENGTH) break + newName = newName + '、' + name + } + return newName +} +export const clipFileNameLength = (name: string) => { + return name.length > MAX_FILE_NAME_LENGTH ? name.substring(0, MAX_FILE_NAME_LENGTH) : name +} + +export const formatMusicName = (format: string, name: string, singer: string) => { + // return format.replace(/歌名|歌手/g, match => match === '歌名' ? name : singer) + return format.replace('歌手', singer).replace('歌名', name) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/vueRouter.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/vueRouter.ts new file mode 100644 index 000000000..2a44942c5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/vueRouter.ts @@ -0,0 +1,2 @@ +export { useRouter, useRoute, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router' + diff --git a/plugins/lx-music-ztools/vendor/lx/src/common/utils/vueTools.ts b/plugins/lx-music-ztools/vendor/lx/src/common/utils/vueTools.ts new file mode 100644 index 000000000..c08b4a1f4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/common/utils/vueTools.ts @@ -0,0 +1,82 @@ +import { + ref, + reactive, + computed, + watch, + watchEffect, + nextTick, + onMounted, + onBeforeUnmount, + toRaw, + useCssModule, + toRef, + toRefs, + shallowRef, + unref, + markRaw, + type ComputedRef, + type Ref, + type ShallowRef, + shallowReactive, + withDefaults, +} from 'vue' +// import { useStore } from 'vuex' + +// export const useState = name => { +// const store = useStore() +// return store.state[name] +// } +// export const useGetter = (...names) => { +// const store = useStore() +// return store.getters[names.join('/')] +// } +// export const useRefGetter = (...names) => { +// const store = useStore() +// return computed(() => store.getters[names.join('/')]) +// } + +// export const useAction = (...names) => { +// const store = useStore() +// return params => { +// return store.dispatch(names.join('/'), params) +// } +// } +// export const useCommit = (...names) => { +// const store = useStore() +// return params => { +// return store.commit(names.join('/'), params) +// } +// } + +export const markRawList = (list: T) => { + for (const item of list) { + markRaw(item) + } + return list +} + +export { + nextTick, + onBeforeUnmount, + ref, + toRaw, + reactive, + watch, + watchEffect, + computed, + useCssModule, + toRef, + toRefs, + shallowRef, + unref, + onMounted, + markRaw, + shallowReactive, + withDefaults, +} + +export type { + ComputedRef, + Ref, + ShallowRef, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/lang/.eslintrc.cjs b/plugins/lx-music-ztools/vendor/lx/src/lang/.eslintrc.cjs new file mode 100644 index 000000000..b86028ebf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/lang/.eslintrc.cjs @@ -0,0 +1,15 @@ +/* eslint-env node */ +const { base, typescript } = require('../../.eslintrc.base.cjs') + +module.exports = { + root: true, + ...base, + overrides: [ + { + ...typescript, + parserOptions: { + project: './tsconfig.json', + }, + }, + ], +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/lang/Readme.md b/plugins/lx-music-ztools/vendor/lx/src/lang/Readme.md new file mode 100644 index 000000000..8a63ed0de --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/lang/Readme.md @@ -0,0 +1,39 @@ +新增语言时创建的语言文件夹需要与以下列表对应: + +- `ar-sa` - Arabic Saudi Arabia +- `cs-cz` - Czech Czech Republic +- `da-dk` - Danish Denmark +- `de-de` - German Germany +- `el-gr` - Modern Greek Greece +- `en-au` - English Australia +- `en-gb` - English United Kingdom +- `en-ie` - English Ireland +- `en-us` - English United States +- `en-za` - English South Africa +- `es-es` - Spanish Spain +- `es-mx` - Spanish Mexico +- `fi-fi` - Finnish Finland +- `fr-ca` - French Canada +- `fr-fr` - French France +- `he-il` - Hebrew Israel +- `hi-in` - Hindi India +- `hu-hu` - Hungarian Hungary +- `id-id` - Indonesian Indonesia +- `it-it` - Italian Italy +- `ja-jp` - Japanese Japan +- `ko-kr` - Korean Republic of Korea +- `nl-be` - Dutch Belgium +- `nl-nl` - Dutch Netherlands +- `no-no` - Norwegian Norway +- `pl-pl` - Polish Poland +- `pt-br` - Portuguese Brazil +- `pt-pt` - Portuguese Portugal +- `ro-ro` - Romanian Romania +- `ru-ru` - Russian Russian Federation +- `sk-sk` - Slovak Slovakia +- `sv-se` - Swedish Sweden +- `th-th` - Thai Thailand +- `tr-tr` - Turkish Turkey +- `zh-cn` - Chinese China +- `zh-hk` - Chinese Hong Kong +- `zh-tw` - Chinese Taiwan diff --git a/plugins/lx-music-ztools/vendor/lx/src/lang/en-us.json b/plugins/lx-music-ztools/vendor/lx/src/lang/en-us.json new file mode 100644 index 000000000..bf9484a9f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/lang/en-us.json @@ -0,0 +1,748 @@ +{ + "action": "Actions", + "agree": "Accept", + "alert_button_text": "All right", + "audio_visualization": "Audio Visualization (Experimental)", + "back": "Back", + "btn_cancel": "Cancel", + "btn_close": "Close", + "btn_confirm": "Confirm", + "btn_save": "Save", + "cancel_button_text": "Cancel", + "cancel_button_text_2": "No, no, wrong click", + "close": "Close", + "comment__hot_load_error": "Failed to load top comments. Click to try to reload.", + "comment__hot_loading": "Top comments are loading...", + "comment__hot_title": "Top comments", + "comment__location": "From {location}", + "comment__new_load_error": "Failed to load latest comments. Click to try to reload.", + "comment__new_loading": "Latest comments are loading...", + "comment__new_title": "Latest comments", + "comment__no_content": "No comments yet", + "comment__refresh": "Refresh comments", + "comment__show": "Song comments", + "comment__title": "Comments for \"{name}\"", + "comment__unavailable": "Unable to get comments for this song.", + "confirm_button_text": "Yes", + "copy_tip": " (Click to copy)", + "date_format_hour": "{num} hours ago", + "date_format_minute": "{num} minutes ago", + "date_format_second": "{num} seconds ago", + "deep_link__handle_error_tip": "Failed to call: {message}", + "default": "Default", + "default_list": "Default", + "desktop_lyric__back": "Back", + "desktop_lyric__close": "Close", + "desktop_lyric__font_decrease": "Decrease font size", + "desktop_lyric__font_increase": "Increase font size", + "desktop_lyric__lock": "Lock", + "desktop_lyric__lrc_active_zoom_off": "Unzoom currently playing lyrics", + "desktop_lyric__lrc_active_zoom_on": "Zoom currently playing lyrics", + "desktop_lyric__opacity_decrease": "Increase transparency\n(Right-click to fine-tune)", + "desktop_lyric__opacity_increase": "Decrease transparency\n(Right-click to fine-tune)", + "desktop_lyric__theme": "Theme Color", + "desktop_lyric__unlock": "Unlock", + "desktop_lyric__win_top_off": "Stay always-on-top", + "desktop_lyric__win_top_on": "Un-top", + "download": "Downloads", + "download___status_completed": "Download is complete", + "download___status_error": "Error", + "download___status_paused": "Paused", + "download___status_running": "Downloading", + "download___status_waiting": "Waiting to download", + "download__all": "All Tasks", + "download__error": "Error", + "download__finished": "Completed", + "download__high_quality": "High Quality", + "download__lossless": "Lossless", + "download__multiple_tip": "{len} song(s) selected", + "download__multiple_tip2": "Choose preferred download quality", + "download__normal": "Normal", + "download__not_available_tip": "The audio quality is not available.", + "download__paused": "Paused", + "download__progress": "Progress", + "download__quality": "Quality", + "download__running": "Downloading", + "download__status": "Status", + "download_status_error_check_path": "There is an error in checking the download path. Please check whether the set download directory is normal", + "download_status_error_check_path_exist": "A file with the same name exists. The download has been skipped", + "download_status_error_refresh_url": "The link is dead, refreshing...", + "download_status_error_response": "Failed to download: ", + "download_status_error_url_failed": "Failed to get song link", + "download_status_error_write": "The download location is occupied or does not have write permission. Please try to change the download directory or restart the app/device. Error details: ", + "download_status_start": "Start download", + "download_status_url_getting": "Getting song link...", + "download_status_write_queue": "Writing data in progress ({num})", + "duplicate_list_tip": "You have collected this list \"{name}\" before. Do you need to update the songs in it?", + "export": "Export", + "fullscreen_exit": "Exit fullscreen", + "history_clear": "Clear history", + "history_remove": "Right-click to remove this entry", + "history_search": "Search History", + "import": "Import", + "leaderboard": "Charts", + "list__add_to": "Add to ...", + "list__collect": "Collect", + "list__copy_name": "Copy Name", + "list__dislike": "Dislike", + "list__download": "Download", + "list__export_part_desc": "Choose where to save the list file", + "list__file": "Reveal File", + "list__import_part_button_cancel": "Don't", + "list__import_part_button_confirm": "Overwrite", + "list__import_part_confirm": "The imported list ({importName}) has the same ID as the local list ({localName}). Do you want to overwrite the local list?", + "list__import_part_desc": "Choose list file", + "list__load_failed": "Ah, the loading failed 😭", + "list__loading": "List loading...⏳", + "list__move_to": "Move to ...", + "list__movedown": "Move down", + "list__moveup": "Move up", + "list__name_default": "Default", + "list__name_love": "Loved", + "list__new_list_btn": "New List", + "list__new_list_input": "New list...", + "list__pause": "Stop", + "list__play": "Play", + "list__play_later": "Play Later", + "list__remove": "Remove", + "list__remove_tip": "Do you really want to remove \"{name}\"?", + "list__remove_tip_button": "Yes, that's right", + "list__rename": "Rename", + "list__search": "Search", + "list__sort": "Adjust Position", + "list__source_detail": "Song Detail", + "list__start": "Resume", + "list__sync": "Update", + "list__toggle_source": "Change Source", + "list_add__btn_title": "Add the song(s) to \"{name}\"", + "list_add__multiple_btn_title": "Add these song(s) to \"{name}\"", + "list_add__multiple_title_add": "Add the selected {num} songs to ...", + "list_add__multiple_title_move": "Move the selected {num} songs to ...", + "list_add__title_first_add": "Add", + "list_add__title_first_move": "Move", + "list_add__title_last": "to ...", + "list_duplicate_tip": "A list with the same name already exists. Do you want to continue creating it?", + "list_import_tip__alldata": "Failed to import. This is an \"All Data\" backup file. You need to go here to import: \n\nSettings → Backup & Restore → All Data → Import", + "list_import_tip__playlist": "Failed to import. This is a \"List\" backup file. You need to go here to import: \n\nSettings → Backup & Restore → Partial Data → Import lists", + "list_import_tip__playlist_part": "Failed to import. This is a \"List-only\" backup file. You need to go here to import: \n\nYour Library → Right-click on any list name → Select 'Import' in the menu", + "list_import_tip__setting": "Failed to import. This is a \"Settings\" backup file. You need to go here to import: \n\nSettings → Backup & Restore → Partial Data → Import settings", + "list_import_tip__unknown": "Failed to import. Unknown file type. Please try to upgrade the app to the latest version and try again.", + "list_sort_modal_by_album": "Album", + "list_sort_modal_by_down": "Descending", + "list_sort_modal_by_field": "Sort Field", + "list_sort_modal_by_name": "Title", + "list_sort_modal_by_random": "Random", + "list_sort_modal_by_singer": "Artist", + "list_sort_modal_by_source": "Music Service", + "list_sort_modal_by_time": "Length", + "list_sort_modal_by_type": "Sort Category", + "list_sort_modal_by_up": "Ascending", + "list_sort_modal_tip_confirm": "Are you sure you want to do this?", + "list_update_modal__auto_update": "Auto Update", + "list_update_modal__tips": "💡 The list with \"Auto Update\" checked will be automatically updated each time the app is started.", + "list_update_modal__title": "List Update Management", + "list_update_modal__update": "Sync", + "lists__add_local_file_desc": "Choose song file", + "lists__dislike_music_singer_tip": "Do you really dislike {singer}'s \"{name}\"?", + "lists__dislike_music_tip": "Do you really dislike \"{name}\"?", + "lists__duplicate": "Duplicate Songs", + "lists__export": "Export", + "lists__export_part_desc": "Choose where to save the list file", + "lists__import": "Import", + "lists__import_part_button_cancel": "No", + "lists__import_part_button_confirm": "Overwrite", + "lists__import_part_confirm": "The imported list ({importName}) has the same ID as the local list ({localName}). Do you overwrite the local list?", + "lists__import_part_desc": "Select list file", + "lists__new_list_btn": "Create list", + "lists__new_list_input": "New list...", + "lists__remove": "Remove", + "lists__remove_music_tip": "Do you really want to remove the selected {len} songs?", + "lists__remove_tip": "Do you really want to remove \"{name}\"?", + "lists__remove_tip_button": "Yes, that's right", + "lists__rename": "Rename", + "lists__select_local_file": "Add Local Songs", + "lists__sort_list": "Sort Songs", + "lists__source_detail": "Playlist Page", + "lists__sync": "Update", + "lists__sync_confirm_tip": "This will replace the songs in \"{name}\" with the songs in the online list. Are you sure you want to update?", + "load_list_file_error_detail": "We have helped you back up the old list file to {path}\nIt is stored in JSON format. You can try to repair and restore it manually.\n\nError details: {detail}", + "load_list_file_error_title": "Error loading playlist data", + "loading": "Loading...", + "love_list": "Favorites", + "lyric__load_error": "Failed to get lyrics", + "lyric__select": "Allow selection of lyrics text", + "lyric_menu__align": "Lyric alignment", + "lyric_menu__align_center": "Center", + "lyric_menu__align_left": "Left", + "lyric_menu__lrc_size": "Font Size [{size}]", + "lyric_menu__offset": "Offset [{offset}ms]", + "lyric_menu__offset_add_10": "Increase 10ms", + "lyric_menu__offset_add_100": "Increase 100ms", + "lyric_menu__offset_dec_10": "Decrease 10ms", + "lyric_menu__offset_dec_100": "Decrease 100ms", + "lyric_menu__offset_reset": "Reset", + "lyric_menu__size_add": "Increase font size\n(Right-click to fine-tune)", + "lyric_menu__size_dec": "Decrease font size\n(Right-click to fine-tune)", + "lyric_menu__size_reset": "Reset", + "media_device__empty_device_tip": "The audio output device is empty. If there is no playback, please check whether the sound card driver is installed. \nOr uninstall and reinstall the software, select \"install for me only\" during installation, and keep the default installation path.", + "min": "Minimize", + "music_album": "Album", + "music_duplicate": "Duplicate song", + "music_name": "Title", + "music_singer": "Artist", + "music_sort__input_tip": "Please enter which position you want to adjust to", + "music_sort__title": "Adjust the position of \"{name}\" to: ", + "music_sort__title_multiple": "Adjust the position of the selected {num} songs to: ", + "music_time": "Length", + "music_toggle_clean": "Cancel Change", + "music_toggle_confirm": "Confirm", + "music_toggle_duplicate_tip": "The same song already exists in the list, should I remove it and continue?", + "my_list": "Your Library", + "no_item": "Nothing's here...", + "not_agree": "Decline", + "ok": "OK", + "pagination__next": "Next page", + "pagination__page": "Page {num}", + "pagination__prev": "Previous page", + "play_timeout": "Timed Pause", + "play_timeout_close": "Close", + "play_timeout_confirm": "Confirm", + "play_timeout_end": "Wait for the song to finish before pausing", + "play_timeout_stop": "Cancel timer", + "play_timeout_tip": "Pause after {time}", + "play_timeout_unit": "Minute(s)", + "play_timeout_update": "Update timing", + "player__add_music_to": "Add the current song to ...", + "player__buffering": "Buffering...", + "player__desktop_lyric_lock": "Right-click to lock lyric window", + "player__desktop_lyric_off": "Hide lyric window", + "player__desktop_lyric_on": "Show lyric window", + "player__desktop_lyric_unlock": "Right-click to unlock lyric window", + "player__end": "Stopped", + "player__error": "Error loading music. Switch to the next song after 5 seconds", + "player__getting_url": "Getting music link...", + "player__getting_url_delay_retry": "The server is busy. Try again in {time} seconds...", + "player__hide_detail_tip": "Hide detail page\n(Right-click in the app window to quickly hide the detail page)", + "player__loading": "Music loading...", + "player__music_album": "Album: ", + "player__music_name": "Title: ", + "player__music_singer": "Artist: ", + "player__next": "Next", + "player__pause": "Pause", + "player__pic_tip": "Playback detail page\n(Right-click to locate the currently playing song in \"Your Library\")", + "player__play": "Play", + "player__play_toggle_mode_list": "In Order", + "player__play_toggle_mode_list_loop": "Repeat Playlist", + "player__play_toggle_mode_off": "Disable", + "player__play_toggle_mode_random": "Shuffle", + "player__play_toggle_mode_single_loop": "Repeat", + "player__playback_preserves_pitch": "Pitch compensation", + "player__playback_rate": "Current playback rate: ", + "player__playback_rate_reset_btn": "Reset", + "player__playing": "Now playing...", + "player__prev": "Prev", + "player__refresh_url": "Music URL expired, refreshing...", + "player__sound_effect": "Sound options (EXPERIMENTAL)", + "player__sound_effect_biquad_filter": "Equalizer", + "player__sound_effect_biquad_filter_preset_classical": "Classical", + "player__sound_effect_biquad_filter_preset_dance": "Dance", + "player__sound_effect_biquad_filter_preset_electronic": "Electronic", + "player__sound_effect_biquad_filter_preset_pop": "Pop", + "player__sound_effect_biquad_filter_preset_rock": "Rock", + "player__sound_effect_biquad_filter_preset_slow": "Slow", + "player__sound_effect_biquad_filter_preset_soft": "Soft", + "player__sound_effect_biquad_filter_preset_subwoofer": "Subwoofer", + "player__sound_effect_biquad_filter_preset_vocal": "Vocal", + "player__sound_effect_biquad_filter_reset_btn": "Reset", + "player__sound_effect_biquad_filter_save_btn": "Save Preset As", + "player__sound_effect_biquad_filter_save_input": "New presets...", + "player__sound_effect_convolution": "Ambient Reverb Sound Effect", + "player__sound_effect_convolution_file_bright_hall": "Hall", + "player__sound_effect_convolution_file_cardiod_35_10_spread": "Heart-shaped Diffusion", + "player__sound_effect_convolution_file_cinema_diningroom": "Cinema", + "player__sound_effect_convolution_file_dining_living_true_stereo": "Dining Room", + "player__sound_effect_convolution_file_feedback_spring": "Feedback Spring", + "player__sound_effect_convolution_file_living_bedroom_leveled": "Bathroom", + "player__sound_effect_convolution_file_matrix_1": "Matrix (1)", + "player__sound_effect_convolution_file_matrix_2": "Matrix (2)", + "player__sound_effect_convolution_file_s2_r4_bd": "Church", + "player__sound_effect_convolution_file_s3_r1_bd": "Stereo", + "player__sound_effect_convolution_file_spreader50_65ms": "Indoor", + "player__sound_effect_convolution_file_telephone": "Telephone", + "player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "Magnetic Stereo", + "player__sound_effect_convolution_main_gain": "Original Audio Gain", + "player__sound_effect_convolution_send_gain": "Ambient Sound Effect Gain", + "player__sound_effect_features_tip": "Tip: The sound effect options conflict with the custom audio output device. After enabling the sound effect options, the audio output device will be reset to the default output device. This problem cannot be resolved at present.", + "player__sound_effect_panner": "3D Stereo Surround (Need to use headphones)", + "player__sound_effect_panner_enabled": "Enabled", + "player__sound_effect_panner_sound_r": "Sound Distance", + "player__sound_effect_panner_sound_speed": "Surround Speed", + "player__sound_effect_pitch_shifter": "Pitch Adjustment", + "player__sound_effect_pitch_shifter_preset_semitones": "{num} semitones", + "player__sound_effect_pitch_shifter_reset_btn": "Reset", + "player__sound_effect_pitch_shifter_tip": "This results in additional CPU usage, as raising/lowering the pitch requires real-time processing of audio data.\n\nKnown issues:\nInsufficient CPU resources will cause processing tasks to pile up, and a sound exception will occur.\nAt this time, it is necessary to pause the playback for a period of time and wait for the accumulated tasks to be processed before playing.", + "player__stop": "Paused", + "player__volume": "Volume: ", + "player__volume_mute_label": "Mute", + "player__volume_muted": "Muted", + "search": "Search", + "search__hot_search": "Top Searches", + "search__type_music": "Song", + "search__type_songlist": "Playlist", + "search__welcome": "Search what I want~~ 😉", + "setting": "Settings", + "setting__about": "About LX Music", + "setting__backup": "Backup & Restore", + "setting__backup_all": "All Data (\"List\" data and \"Setting\" data)", + "setting__backup_all_export": "Export", + "setting__backup_all_export_desc": "Save the backup to ...", + "setting__backup_all_import": "Import", + "setting__backup_all_import_desc": "Choose a backup file", + "setting__backup_other": "Other Backup Formats (Restoring such backup files is not currently supported)", + "setting__backup_other_export_dir": "Choose where to save the file", + "setting__backup_other_export_list_csv": "Export lists in CSV format", + "setting__backup_other_export_list_text": "Export lists in TXT format", + "setting__backup_other_export_list_text_confirm": "Do you want to merge all the lists into one file?", + "setting__backup_part": "Partial Data (\"List\" data includes \"Default\", \"Loved\", and user-created lists. \"Setting\" data does not include \"Shortcuts\" settings)", + "setting__backup_part_export_list": "Export lists", + "setting__backup_part_export_list_desc": "Save the lists to ...", + "setting__backup_part_export_setting": "Export settings", + "setting__backup_part_export_setting_desc": "Save the settings to ...", + "setting__backup_part_import_list": "Import lists", + "setting__backup_part_import_list_confirm": "If the list in the backup file has the same ID as the existing list, the songs in the existing list will be overwritten. Do you want to continue?", + "setting__backup_part_import_list_desc": "Choose a list backup file", + "setting__backup_part_import_setting": "Import settings", + "setting__backup_part_import_setting_desc": "Choose a setting backup file", + "setting__basic": "General", + "setting__basic_animation": "Randomize pop-up animation", + "setting__basic_control_btn_position": "Control Button Position", + "setting__basic_control_btn_position_left": "Left", + "setting__basic_control_btn_position_right": "Right", + "setting__basic_font": "Font", + "setting__basic_font_size": "Font Size", + "setting__basic_font_size_14px": "Smaller", + "setting__basic_font_size_15px": "Small", + "setting__basic_font_size_16px": "Standard", + "setting__basic_font_size_17px": "Big", + "setting__basic_font_size_18px": "Larger", + "setting__basic_font_size_19px": "Oversize", + "setting__basic_lang": "Language", + "setting__basic_lang_title": "The language displayed in the app", + "setting__basic_playbar_progress_style": "Playbar Progress Bar Style", + "setting__basic_playbar_progress_style_full": "Full-width", + "setting__basic_playbar_progress_style_middle": "Medium", + "setting__basic_playbar_progress_style_mini": "Mini", + "setting__basic_show_animation": "Show animation", + "setting__basic_source": "Music API", + "setting__basic_source_status_failed": "Failed to initialize", + "setting__basic_source_status_initing": "Initializing...", + "setting__basic_source_status_success": "Successfully initialized", + "setting__basic_source_temp": "Temporary API (Some features not available. Workaround if Test API is unavailable)", + "setting__basic_source_test": "Test API (Available for most app features)", + "setting__basic_source_user_api_btn": "Music API Management", + "setting__basic_sourcename": "Music Streaming Service Name", + "setting__basic_sourcename_alias": "Alias", + "setting__basic_sourcename_real": "Original", + "setting__basic_sourcename_title": "Choose the music streaming service name", + "setting__basic_start_in_fullscreen": "Start in fullscreen mode", + "setting__basic_theme": "Theme", + "setting__basic_theme_auto_tip": "This is a dynamic theme. You can preset a light theme and a dark theme, and then it will automatically switch to the corresponding theme you preset according to the system's light and dark theme colors.\n\nNOTE: Right-click this theme item to open the light and dark theme setting window.", + "setting__basic_to_tray": "Minimize the app window to the system tray when closing it", + "setting__basic_window_size": "Window Size", + "setting__basic_window_size_big": "Big", + "setting__basic_window_size_huge": "Huge", + "setting__basic_window_size_larger": "Larger", + "setting__basic_window_size_medium": "Medium", + "setting__basic_window_size_oversized": "Oversize", + "setting__basic_window_size_small": "Small", + "setting__basic_window_size_smaller": "Smaller", + "setting__basic_window_size_title": "Set the window size", + "setting__click_copy": "Click to copy", + "setting__click_open": "Click to open", + "setting__desktop_lyric": "Desktop Lyric", + "setting__desktop_lyric_align": "Lyric Alignment", + "setting__desktop_lyric_align_center": "Center", + "setting__desktop_lyric_align_left": "Left", + "setting__desktop_lyric_align_right": "Right", + "setting__desktop_lyric_always_on_top": "Stay always-on-top", + "setting__desktop_lyric_always_on_top_loop": "Refresh lyric window repeatedly when \"Stay always-on-top\" is enabled", + "setting__desktop_lyric_always_on_top_loop_tip": "Try to enable this option when the window are still covered by some programs.", + "setting__desktop_lyric_audio_visualization": "Enable audio visualization (EXPERIMENTAL)", + "setting__desktop_lyric_color": "Lyric Font Color", + "setting__desktop_lyric_color_reset": "Reset color", + "setting__desktop_lyric_delay_scroll": "Delay scrolling lyrics", + "setting__desktop_lyric_direction": "Lyric Display Direction", + "setting__desktop_lyric_direction_horizontal": "Horizontal", + "setting__desktop_lyric_direction_vertical": "Vertical", + "setting__desktop_lyric_ellipsis": "Do not wrap lyrics", + "setting__desktop_lyric_enable": "Show lyric window", + "setting__desktop_lyric_font": "Lyric Font", + "setting__desktop_lyric_font_default": "Default", + "setting__desktop_lyric_font_weight": "Bold Font", + "setting__desktop_lyric_font_weight_extended": "Translated & romanized lyrics", + "setting__desktop_lyric_font_weight_font": "Verbatim lyrics", + "setting__desktop_lyric_font_weight_line": "Progressive lyrics", + "setting__desktop_lyric_fullscreen_hide": "Hide lyric window when in fullscreen", + "setting__desktop_lyric_hover_hide": "Increase lyric window transparency when the mouse moves into the lyric window", + "setting__desktop_lyric_hover_hide_tip": "This feature has platform compatibility issues.", + "setting__desktop_lyric_line_gap": "Lyric Spacing ({num})", + "setting__desktop_lyric_line_gap_add": "Increase spacing", + "setting__desktop_lyric_line_gap_dec": "Decrease spacing", + "setting__desktop_lyric_lock": "Lock lyric window", + "setting__desktop_lyric_lock_screen": "Do not allow lyric window to be dragged out of main screen", + "setting__desktop_lyric_pause_hide": "Increase lyric window transparency when pausing music", + "setting__desktop_lyric_played_color": "Played", + "setting__desktop_lyric_reset": "Reset", + "setting__desktop_lyric_reset_window": "Reset Desktop Lyric Options", + "setting__desktop_lyric_scroll_align": "Position of Lyrics Scrolling While Playing", + "setting__desktop_lyric_scroll_align_center": "Center", + "setting__desktop_lyric_scroll_align_top": "Top", + "setting__desktop_lyric_shadow_color": "Shadow", + "setting__desktop_lyric_show_taskbar": "Show lyric window process on the taskbar", + "setting__desktop_lyric_show_taskbar_tip": "This option is used as a workaround when the screen recording program cannot capture the lyric window.", + "setting__desktop_lyric_unplay_color": "Not Played", + "setting__dislike_list_input_tip": "song_name@artist_name\nsong_name\n@artist_name", + "setting__dislike_list_save_btn": "Save", + "setting__dislike_list_tips": "1. One line per entry. If there is an \"@\" symbol in the name of the song or artist, it needs to be replaced with \"#\"\n2. Specify a song by a certain artist: song_name@artist_name\n3. Specify a song: song_name\n4. Specify an artist: @artist_name", + "setting__dislike_list_title": "Disliked Song Rule List", + "setting__download": "Download", + "setting__download_data_embed": "Embed the Following into Audio File", + "setting__download_embed_lxlyric": "Embed LX Music lyrics at the same time (if any, verbatim lyrics can be supported when playing with LX Music)", + "setting__download_embed_lyric": "Embed lyrics", + "setting__download_embed_pic": "Embed cover", + "setting__download_embed_rlyric": "Also embed romanized lyrics if available", + "setting__download_embed_tlyric": "Also embed translated lyrics if available", + "setting__download_enable": "Enable Download", + "setting__download_lxlyric": "Also write LX Music lyrics into the lyrics file (if any, verbatim lyrics can be supported when playing with LX Music)", + "setting__download_lyric": "Lyric Download", + "setting__download_lyric_format": "Encoding Format of Downloaded Lyric Files", + "setting__download_lyric_format_gbk": "GBK", + "setting__download_lyric_format_tip": "Try to select GBK format when Chinese garbled characters appear on some devices.", + "setting__download_lyric_format_utf8": "UTF-8", + "setting__download_lyric_title": "Select whether to download the lyrics file", + "setting__download_max_num": "Number of Simultaneous Downloads", + "setting__download_max_num_tip": "An excessively large number of simultaneous downloads may cause your IP to be blocked by the Music API. Do you confirm the modification?", + "setting__download_max_num_tooltip": "Set too high may result in IP being blocked, depending on the Music API.", + "setting__download_name": "Music File Naming", + "setting__download_name1": "\"Title - Artist\"", + "setting__download_name2": "\"Artist - Title\"", + "setting__download_name3": "Title only", + "setting__download_name_title": "Select the music naming style for downloading", + "setting__download_path": "Download Path", + "setting__download_path_change_btn": "Change", + "setting__download_path_label": "Current: ", + "setting__download_path_open_label": "Click to open this path", + "setting__download_path_title": "Choose the path to downloading", + "setting__download_rlyric": "Also write romanized lyrics to the lyric file if available", + "setting__download_select_save_path": "Select the save path", + "setting__download_skip_exist_file": "Skip the download task if a file with the same name exists in the download directory", + "setting__download_tlyric": "Also write translated lyrics to the lyric file if available", + "setting__download_use_other_source": "Automatic Switch Source Download", + "setting__download_use_other_source_tip": "When the song cannot be downloaded from the original source, try to switch to another source to download.\nNOTE: This does not 100% guarantee that the version of the song after changing the source is consistent with the original version.", + "setting__hot_key": "Shortcuts", + "setting__hot_key_common_focus_search_input": "Focus Search Box", + "setting__hot_key_common_min": "Minimize App", + "setting__hot_key_common_toggle_close": "Exit App", + "setting__hot_key_common_toggle_hide": "Show/Hide App", + "setting__hot_key_common_toggle_min": "Minimize/Restore App", + "setting__hot_key_desktop_lyric_toggle_always_top": "Top/Un-top Desktop Lyric", + "setting__hot_key_desktop_lyric_toggle_lock": "Lock/Unlock Desktop Lyric", + "setting__hot_key_desktop_lyric_toggle_visible": "Show/Hide Desktop Lyric", + "setting__hot_key_global_title": "Global Keyboard Shortcuts", + "setting__hot_key_local_title": "In-app Keyboard Shortcuts", + "setting__hot_key_player_music_dislike": "Dislike Song", + "setting__hot_key_player_music_love": "Love Song", + "setting__hot_key_player_music_unlove": "Unlove Song", + "setting__hot_key_player_next": "Next Song", + "setting__hot_key_player_prev": "Previous Song", + "setting__hot_key_player_seekbackward": "Seek Backward 5s", + "setting__hot_key_player_seekforward": "Seek Forward 5s", + "setting__hot_key_player_toggle_play": "Play/Pause Control", + "setting__hot_key_player_volume_down": "Decrease Volume", + "setting__hot_key_player_volume_mute": "Toggle Mute", + "setting__hot_key_player_volume_up": "Increase Volume", + "setting__hot_key_tip_input": "Please enter a new key", + "setting__hot_key_unset_input": "Not set", + "setting__is_enable": "Enable", + "setting__is_show": "Show", + "setting__list": "List", + "setting__list_action_btn": "Show list action buttons", + "setting__list_add_music_location_type": "Position When Adding a Song to the List", + "setting__list_add_music_location_type_bottom": "Bottom", + "setting__list_add_music_location_type_top": "Top", + "setting__list_click_action": "Automatically switch to current list when double-clicking a song in the list (Only valid for \"Playlists\" and \"Charts\" page)", + "setting__list_scroll": "Remember position of scroll bar of playlist (Only valid for \"Your Library\" page)", + "setting__list_source": "Show which music streaming service the song is from (Only valid for \"Your Library\" page)", + "setting__network": "Network", + "setting__network_proxy_host": "Host", + "setting__network_proxy_password": "Password", + "setting__network_proxy_port": "Port", + "setting__network_proxy_title": "HTTP Proxy (Incorrect settings will prevent the app from being networked)", + "setting__network_proxy_username": "Username", + "setting__odc": "Auto Empty", + "setting__odc_clear_search_input": "Empty search box when you are not searching", + "setting__odc_clear_search_list": "Empty search list when you are not searching", + "setting__open_api": "Open API", + "setting__open_api_address": "Service Address: ", + "setting__open_api_bind_lan": "Allow access from LAN", + "setting__open_api_enable": "Enable Open API service", + "setting__open_api_port": "Service Port", + "setting__open_api_port_tip": "Please enter the Open API service port", + "setting__open_api_tip": "This feature is used to provide third-party programs with the ability to call LX Music. You can see the currently available features: ", + "setting__open_api_tip_link": "Visit Documents", + "setting__other": "Extras", + "setting__other_dislike_list": "Dislike Song Rule", + "setting__other_dislike_list_label": "Number of rules: ", + "setting__other_dislike_list_show_btn": "Edit Rule", + "setting__other_listdata": "List Data Cleanup", + "setting__other_listdata_clear_btn": "Clear \"Your Library\" Data", + "setting__other_listdata_clear_tip_confirm": "This will clear all lists you have created and all songs your have loved. Do you really want to continue?", + "setting__other_lyric_edited_cache": "Lyric Management with Adjusted Offset", + "setting__other_lyric_edited_clear_btn": "Clear Time-adjusted Lyrics", + "setting__other_lyric_edited_clear_tip_confirm": "This will clear all the lyrics that you have adjusted the offset time before. Confirm to clear? \n(Hand shaking to confirm 🤪)", + "setting__other_lyric_edited_label": "Number of lyrics: ", + "setting__other_lyric_raw_clear_btn": "Clear Lyric Cache", + "setting__other_lyric_raw_label": "Number of lyrics: ", + "setting__other_music_url_clear_btn": "Clear Song URL Cache", + "setting__other_music_url_label": "Number of song URLs: ", + "setting__other_other_cache": "Other Cache Management", + "setting__other_other_source_clear_btn": "Clear Song Cache of Changed Source", + "setting__other_other_source_label": "Number of songs information that changed source: ", + "setting__other_resource_cache": "Resource Cache Management", + "setting__other_resource_cache_clear_btn": "Clear Resource Cache", + "setting__other_resource_cache_confirm": "I want to clear", + "setting__other_resource_cache_label": "The app has used cache size: ", + "setting__other_resource_cache_tip": "Includes pictures, audios, and other caches. After cleaning, these resources will need to be downloaded again. It is not recommended to clean up. The app will dynamically manage the cache size according to the disk space.", + "setting__other_resource_cache_tip_confirm": "Involving the cache of pictures, audios, etc., these resources will need to be downloaded again after cleaning. It is not recommended to clean up. The app will dynamically manage the cache size according to the disk space. Do you still need to clean up?", + "setting__other_transparent_window": "Use built-in rounded corners and shadows for the main window", + "setting__other_transparent_window_tip": "Turning off this option will use the system's native window style. Requires a restart of the software for the change to take effect.", + "setting__other_tray_theme": "Tray Icon Style", + "setting__other_tray_theme_auto": "Follow System", + "setting__other_tray_theme_black": "Black", + "setting__other_tray_theme_native": "White", + "setting__other_tray_theme_origin": "Default", + "setting__play": "Playback", + "setting__play_advanced_audio_features_tip": "A custom audio output device conflicts with this feature. After enabling this feature, the audio output device will be reset to the default output device. This problem cannot be solved at the moment. Do you still want to enable it?", + "setting__play_auto_clean_played_list": "Empty played list when playing the same list as the current playlist", + "setting__play_auto_clean_played_list_tip": "All songs in the list in shuffle mode will participate in the random again.", + "setting__play_auto_skip_on_error": "Automatically switch songs when playback error occurs", + "setting__play_detail": "Playback Detail Page", + "setting__play_detail_align": "Lyric Alignment", + "setting__play_detail_align_center": "Center", + "setting__play_detail_align_left": "Left", + "setting__play_detail_align_right": "Right", + "setting__play_detail_font_size": "Lyrics font size (you can use the keyboard \"+\" & \"-\" to adjust the font size on the playback detail page)", + "setting__play_detail_font_size_current": "Current font size: {size}", + "setting__play_detail_font_size_reset": "Reset", + "setting__play_detail_font_zoom": "Zoom currently playing lyrics", + "setting__play_detail_lyric_delay_scroll": "Delay scrolling lyrics", + "setting__play_detail_lyric_progress": "Allow to adjust playback progress by drag-and-drop lyrics", + "setting__play_lyric_lxlrc": "Playback with karaoke-style lyrics if available", + "setting__play_lyric_lxlrc_tip": "This feature requires higher performance and is not recommended to be turned on for low performance devices!", + "setting__play_lyric_roma": "Show romanized lyrics if available", + "setting__play_lyric_s2t": "Convert Chinese lyrics that are playing and downloading to traditional", + "setting__play_lyric_transition": "Show translated lyrics if available", + "setting__play_max_output_channel_count": "Output audio using the maximum number of channels the device can handle", + "setting__play_mediaDevice": "Audio Output", + "setting__play_mediaDevice_remove_stop_play": "Pause playback when current audio output device is changed", + "setting__play_mediaDevice_title": "Select a media device for audio output", + "setting__play_media_device_error_tip": "This feature conflicts with advanced audio features (audio visualization, sound effect options, audio output using the maximum number of channels that the device can handle). You have enabled these features when you started the app this time. This option is temporarily unavailable. Please turn off these features and change this option again after restarting the app.", + "setting__play_media_device_tip": "This feature conflicts with the audio visualization feature and cannot be enabled at the same time. Do you want to turn audio visualization off and apply the selected audio output options?", + "setting__play_playQuality": "Prioritize Sound Quality for Playback If Available", + "setting__play_power_save_blocker": "Prevent device from hibernating while playing songs", + "setting__play_save_play_time": "Remember playback progress", + "setting__play_startup_auto_play": "Automatically play music on startup", + "setting__play_statusbar_lyric": "Show lyrics on status menus", + "setting__play_statusbar_lyric_tip": "Requires \"Minimize the app window to the system tray when closing it\" to be enabled in Settings → General", + "setting__play_task_bar": "Show playback progress on the taskbar", + "setting__play_timeout": "Timed Pause", + "setting__player_audio_visualization_tip": "The custom audio output device feature conflicts with the audio visualization feature. After the audio visualization is enabled, the audio output device will be reset to the default. At present, this problem cannot be resolved. Do you still want to enable it?", + "setting__player_swap_lyric_trans_roma": "Swap the position of the translated and romanized lyrics", + "setting__search": "Search", + "setting__search_focus_search_box": "Automatically focus search box on startup", + "setting__search_history": "Enable Search History", + "setting__search_hot": "Enable Top Searches", + "setting__sync": "Data Sync", + "setting__sync_client_address": "Current device address: {address}", + "setting__sync_client_host": "Sync service address", + "setting__sync_client_host_tip": "http://:", + "setting__sync_client_mode": "Client Mode", + "setting__sync_client_status": "Status: {status}", + "setting__sync_code_blocked_ip": "The IP of the current device has been blocked by the server!", + "setting__sync_code_fail": "Invalid connection code", + "setting__sync_enable": "Enable Sync", + "setting__sync_mode": "Sync Mode", + "setting__sync_mode_client": "Client", + "setting__sync_mode_server": "Server", + "setting__sync_server_address": "Sync service address: {address}", + "setting__sync_server_auth_code": "Connection code: {code}", + "setting__sync_server_device": "Connected devices: {devices}", + "setting__sync_server_device_list_btn_remove": "Remove", + "setting__sync_server_device_list_noitem": "Nothing here ┗( ▔, ▔ )┛", + "setting__sync_server_device_list_time": "Last connection time: {time}", + "setting__sync_server_device_list_tips": "💡 After the device is removed, you need to re-enter the connection code when reconnecting", + "setting__sync_server_device_list_title": "Certified device", + "setting__sync_server_mode": "Server Mode (Please use it on a trusted network as data is transmitted in plaintext)", + "setting__sync_server_port": "Sync Port", + "setting__sync_server_port_tip": "Please enter a port number", + "setting__sync_server_refresh_code": "Refresh connection code", + "setting__sync_server_show_device_list": "List of certified devices", + "setting__sync_tip": "For how to use it, please see the \"Sync feature\" section of the FAQ", + "setting__update": "Update", + "setting__update_checking": "Checking for updates...", + "setting__update_commit_date": "Commit date: ", + "setting__update_commit_id": "Commit hash: ", + "setting__update_current_label": "Current version: ", + "setting__update_downloading": "Update is found and being downloaded...⏳", + "setting__update_init": "Updating...", + "setting__update_latest": "The app is up-to-date. Enjoy yourself!🥂", + "setting__update_latest_label": "Latest version: ", + "setting__update_new_version": "Found a new version. Hurry up and update~🚀🚀", + "setting__update_open_version_modal_btn": "Open Update Window", + "setting__update_progress": "Status: ", + "setting__update_show_change_log": "Show changelog on first startup after update", + "setting__update_try_auto_update": "Automatically attempt to download updates when a new version is found", + "setting__update_unknown": "unknown", + "setting__update_unknown_tip": "❓ Failed to fetch the latest version information. it is recommended to go to the About page to open the project release address to check whether the current version is the latest", + "setting_download_save_group_list_name": "Save files to a subdirectory named after the corresponding list", + "setting_sync_status_enabled": "connected", + "song_list": "Playlists", + "songlist__import_input_btn_confirm": "Open", + "songlist__import_input_show_btn": "Open Playlist", + "songlist__import_input_tip": "Enter a playlist link/ID", + "songlist__import_input_tip_1": "Cross-service playlists are not supported. Please confirm whether the playlist to be opened corresponds to the current chosen service", + "songlist__import_input_tip_2": "If you encounter a playlist link that cannot be opened. Please send us your feedback", + "songlist__import_input_tip_3": "Unable to open Kugou playlist with playlist ID or link from lite edition, but support to open it with Kugou code or link from main edition", + "songlist__import_input_tip_4": "NetEase’s “Private Radar” playlists require a token to access. Tencent’s personal playlists require your profile be public to access. For details, see ", + "songlist__import_input_title": "Open shared playlist", + "songlist__open_list": "Open \"{name}\" playlist", + "songlist__tag_info_hot_tag": "Top Tags", + "source_alias_all": "Aggregated", + "source_alias_bd": "BD Music", + "source_alias_kg": "KG Music", + "source_alias_kw": "KW Music", + "source_alias_mg": "MG Music", + "source_alias_tx": "TX Music", + "source_alias_wy": "WY Music", + "source_alias_xm": "XM Music", + "source_all": "Aggregated", + "source_bd": "Baidu", + "source_kg": "Kugou", + "source_kw": "Kuwo", + "source_mg": "Migu", + "source_tx": "Tencent", + "source_wy": "NetEase", + "source_xm": "Xiami", + "sync__auth_code_input_tip": "Please enter the connection code", + "sync__auth_code_title": "Need to enter the connection code", + "sync__dislike_merge_tip_desc": "Merge the content of the two lists and remove the duplicates.", + "sync__dislike_other_tip_desc": "\"Cancel Sync\" will not sync the \"dislike song\" list.", + "sync__dislike_overwrite_tip_desc": "The list of overwritten parties will be replaced with the list of overwriting parties.", + "sync__dislike_title": "Choose how to sync with {name}'s \"dislike song\" list", + "sync__list_merge_tip_desc": "Merge the two lists together. The same song will be removed (the song of the merged person is removed), and different songs will be added.", + "sync__list_other_tip_desc": "\"Cancel Sync\" will not sync the list.", + "sync__list_overwrite_tip_desc": "Lists with the same ID as the overwritten list and the overwritten list will be deleted and replaced with the overrider's list (lists with different list IDs will be merged together). If \"Full Overwrite\" is checked, all lists of the covered one will be moved. Remove and replace with a list of overrides.", + "sync__list_title": "Choose how to synchronize the list with \"{name}\"", + "sync__merge_btn_local_remote": "\"Local List\" Merge \"Remote List\"", + "sync__merge_btn_remote_local": "\"Remote List\" Merge \"Local List\"", + "sync__merge_label": "Merge", + "sync__merge_tip": "Merge: ", + "sync__other_label": "Other", + "sync__other_tip": "Other: ", + "sync__overwrite": "Full Overwrite", + "sync__overwrite_btn_cancel": "Cancel Sync", + "sync__overwrite_btn_local_remote": "\"Local List\" Overwrite \"Remote List\"", + "sync__overwrite_btn_none": "Only use real-time synchronization", + "sync__overwrite_btn_remote_local": "\"Remote List\" Overwrite \"Local List\"", + "sync__overwrite_label": "Overwrite", + "sync__overwrite_tip": "Overwrite: ", + "sync_status_disabled": "not connected", + "tag__high_quality": "HQ", + "tag__lossless": "SQ", + "tag__lossless_24bit": "24bit", + "theme_add": "Add theme", + "theme_auto": "Auto", + "theme_auto_tip": "Right-click to open the light and dark theme options window", + "theme_black": "Black", + "theme_blue": "Blue", + "theme_blue2": "Purple Blue", + "theme_blue_plus": "Blue Plus", + "theme_china_ink": "China Ink", + "theme_edit_modal__app_bg": "App Background Color", + "theme_edit_modal__aside_color": "Sidebar Button Color", + "theme_edit_modal__badge": "Label Color", + "theme_edit_modal__badge_primary": "Main Color", + "theme_edit_modal__badge_secondary": "2nd Color", + "theme_edit_modal__badge_tertiary": "3rd Color", + "theme_edit_modal__bg_image": "Background Image", + "theme_edit_modal__bg_image_add": "Add background image", + "theme_edit_modal__bg_image_change": "Change background image", + "theme_edit_modal__bg_image_remove": "Remove background image", + "theme_edit_modal__close_btn": "Close", + "theme_edit_modal__control_btn": "Left Control Button Color", + "theme_edit_modal__copy": "Copy Theme", + "theme_edit_modal__dark": "Dark Theme", + "theme_edit_modal__dark_font": "Dark Font", + "theme_edit_modal__font": "Font Color", + "theme_edit_modal__hide_btn": "Hide Detail Page", + "theme_edit_modal__main_bg": "Content Area Background Color", + "theme_edit_modal__min_btn": "Minimize", + "theme_edit_modal__pick_cancel": "Reset", + "theme_edit_modal__pick_color": "Choose color", + "theme_edit_modal__pick_last_color": "Use previous color", + "theme_edit_modal__pick_save": "Confirm", + "theme_edit_modal__preview": "Preview", + "theme_edit_modal__primary": "Theme Color", + "theme_edit_modal__remove": "Remove", + "theme_edit_modal__remove_tip": "Do you really want to remove this theme?", + "theme_edit_modal__save_new": "Copy", + "theme_edit_modal__select_bg_file": "Choose a background image", + "theme_edit_modal__title_add": "Add Theme", + "theme_edit_modal__title_edit": "Edit Theme", + "theme_green": "Green", + "theme_grey": "Grey", + "theme_happy_new_year": "New Year", + "theme_max_tip": "You can only add up to 10 themes. Remove some and add more 😜", + "theme_mid_autumn": "Mid-Autumn", + "theme_ming": "Ming", + "theme_more_btn_show": "More themes", + "theme_naruto": "Naruto", + "theme_orange": "Orange", + "theme_pink": "Pink", + "theme_purple": "Purple", + "theme_red": "Red", + "theme_selector_modal__dark_title": "Dark Theme", + "theme_selector_modal__light_title": "Light Theme", + "theme_selector_modal__theme_name": "Theme name", + "theme_selector_modal__title": "Follow System Theme Options", + "theme_selector_modal__title_tip": "NOTE: You can set a light theme and a dark theme in advance, and then it will automatically switch to the corresponding theme you set in advance according to the light and dark theme colors of the system.", + "toggle_source_failed": "Failed to change the source. Please try to manually search for the song on the search page by specifying another service.", + "toggle_source_try": "Try switching to another source...", + "update__downgrade_tip": "We found that you have downgraded the version ({ver}). If you encounter problems when using the new version, please read the FAQ first. If the problem you encounter is not documented in the FAQ or cannot be resolved, you can give us feedback through the feedback channels mentioned in the document😘!\n\nNOTE: When downgrading from the new version to the old version, it is recommended to backup the playlist first. If there is an exception, it can be resolved by cleaning the data. The data directory path can be found in the document.", + "update__error_top": "Failed to download the new version. You can try downloading it again or going to GitHub to download it.\n\nThe address of the new version is included in the update pop-up window. Download the new version and install it directly. If the installation fails, please see the FAQ.\n\nNOTE: Currently only Windows Setup variants can be updated automatically. AppImage and deb variants for Linux seem to work but have not been tested. Users installing with other variants should download and update manually.", + "update__ignore_cancel": "I don't want to update 🤨", + "update__ignore_confirm": "Ok, let's update it ❤️", + "update__ignore_confirm_tip": "Currently only Windows Setup variants can be updated automatically. AppImage and deb variants for Linux seem to work but have not been tested. Users installing with other variants should download and update manually.\n\nThe address of the new version is included in the update pop-up window. Download the new version and install it directly. If the installation fails, please see the FAQ.", + "update__ignore_confirm_tip_confirm": "OK, I understood", + "update__ignore_tip": "The version you are using now is behind the latest version by {num} versions🤪. For a better user experience, it is recommended to update to the latest version~!\nNOTE: If you encounter problems when using the new version, please read the FAQ first. If the problem you encounter is not documented in the FAQ or cannot be resolved, you can give us feedback through the feedback channels mentioned in the document😘!", + "update__timeout_top": "Download time is too long prompt\n\nYour current network access to GitHub is slow, and the new version has been downloaded for an hour and has not been completed yet😳. You can still choose to continue waiting, but it is highly recommended to manually update the version!", + "user_api__allow_show_update_alert": "Allow update popup to show", + "user_api__btn_export": "Export", + "user_api__btn_import": "Import from Local File", + "user_api__btn_import_online": "Import from Network", + "user_api__btn_remove": "Remove", + "user_api__import_file": "Choose Music API script file", + "user_api__init_failed_alert": "Failed to initialize Music API \"{name}\": ", + "user_api__max_tip": "There can only be a maximum of 20 APIs at the same time🤪.\n\nIf you want to continue importing, please remove some unnecessary APIs to make room.", + "user_api__noitem": "There is nothing here...😲", + "user_api__note": "TIP: Although we have isolated the API script's running environment as much as possible, importing API scripts containing malicious behaviors may still affect your system. Please import them with caution.", + "user_api__readme": "API writing instructions: ", + "user_api__title": "Music API Management", + "user_api__update_alert": "Music API \"{name}\" found new version: ", + "user_api__update_alert_open_url": "Open update address", + "user_api_import__failed": "Failed to import Music API:\n\n{message}", + "user_api_import_online__input_confirm": "Import", + "user_api_import_online__input_loading": "Importing...", + "user_api_import_online__input_tip": "Please enter an HTTP link", + "user_api_import_online__title": "Import Music API from network." +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/lang/i18n.ts b/plugins/lx-music-ztools/vendor/lx/src/lang/i18n.ts new file mode 100644 index 000000000..ab04b022a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/lang/i18n.ts @@ -0,0 +1,91 @@ +import { type App, ref } from 'vue' +import { messages } from './index' +import type { Messages, Message } from './index' + +type TranslateValues = Record + +type Langs = keyof Messages + +export declare interface I18n { + locale: Langs + fallbackLocale: Langs + availableLocales: Langs[] + messages: Messages + message: Message + setLanguage: (locale: Langs) => void + fillMessage: (message: string, val: TranslateValues) => string + getMessage: (key: keyof Message, val?: TranslateValues) => string + t: (key: keyof Message, val?: TranslateValues) => string +} + +const locale = ref('zh-cn') + +let i18n: I18n + + +const trackReactivityValues = (): any => { + return locale.value +} + +const i18nPlugin = { + install: (app: App) => { + // inject a globally available $translate() method + app.config.globalProperties.$t = (key: keyof Message, val?: TranslateValues): string => { + // retrieve a nested property in `options` + // using `key` as the path + // return key.split('.').reduce((o, i) => { + // if (o) return o[i] + // }, options) + trackReactivityValues() + return i18n.getMessage(key, val) + } + }, +} + +const useI18n = () => { + return (key: keyof Message, val?: TranslateValues): string => { + trackReactivityValues() + return i18n.getMessage(key, val) + } +} + +const setLanguage = (lang: Langs) => { + i18n.setLanguage(lang) +} + +const createI18n = (): I18n => { + return i18n = { + locale: locale.value, + fallbackLocale: 'zh-cn', + availableLocales: Object.keys(messages) as Langs[], + messages, + message: messages[locale.value], + setLanguage(_locale: Langs) { + this.locale = _locale + this.message = messages[_locale] + locale.value = _locale + }, + fillMessage(message: string, vals: TranslateValues): string { + for (const [key, val] of Object.entries(vals)) { + message = message.replaceAll(`{${key}}`, String(val)) + } + return message + }, + getMessage(key: keyof Message, val?: TranslateValues): string { + let targetMessage = this.message[key] ?? this.messages[this.fallbackLocale][key] ?? key + return val ? this.fillMessage(targetMessage, val) : targetMessage + }, + t(key: keyof Message, val?: TranslateValues): string { + trackReactivityValues() + return this.getMessage(key, val) + }, + } +} + + +export { + i18nPlugin, + setLanguage, + useI18n, + createI18n, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/lang/index.ts b/plugins/lx-music-ztools/vendor/lx/src/lang/index.ts new file mode 100644 index 000000000..cf79ce5ac --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/lang/index.ts @@ -0,0 +1,69 @@ +import zh_cn from './zh-cn.json' +import zh_tw from './zh-tw.json' +import en_us from './en-us.json' +import ko_kr from './ko-kr.json' + +type Message = Record +| Record +| Record +| Record + +type Messages = Record<(typeof langs)[number]['locale'], Message> + +const langs = [ + { + name: 'English', + locale: 'en-us', + country: 'us', + message: en_us, + }, + { + name: '한국어', + locale: 'ko-kr', + country: 'kr', + message: ko_kr, + }, + { + name: '简体中文', + locale: 'zh-cn', + // alternate: 'zh-hans', + country: 'cn', + fallback: true, + message: zh_cn, + }, + { + name: '繁體中文', + locale: 'zh-tw', + // alternate: 'zh-hk', + country: 'cn', + message: zh_tw, + }, +] as const + +const langList: Array<{ + name: string + locale: keyof Messages + alternate?: string +}> = [] +// @ts-expect-error +const messages: Messages = {} +langs.forEach(item => { + langList.push({ + name: item.name, + locale: item.locale, + // alternate: item.alternate, + }) + messages[item.locale] = item.message +}) + +export { + langList, + messages, +} + +export type { + Messages, + Message, +} + +export * from './i18n' diff --git a/plugins/lx-music-ztools/vendor/lx/src/lang/ko-kr.json b/plugins/lx-music-ztools/vendor/lx/src/lang/ko-kr.json new file mode 100644 index 000000000..eb6b6aec8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/lang/ko-kr.json @@ -0,0 +1,748 @@ +{ + "action": "동작", + "agree": "동의", + "alert_button_text": "확인", + "audio_visualization": "오디오 시각화 (실험적 기능)", + "back": "뒤로", + "btn_cancel": "취소", + "btn_close": "닫기", + "btn_confirm": "확인", + "btn_save": "저장", + "cancel_button_text": "취소", + "cancel_button_text_2": "아니요, 잘못 눌렀어요", + "close": "닫기", + "comment__hot_load_error": "인기 댓글을 불러오지 못했습니다. 클릭하여 다시 시도하세요.", + "comment__hot_loading": "인기 댓글을 불러오는 중...", + "comment__hot_title": "인기 댓글", + "comment__location": "{location}에서", + "comment__new_load_error": "최신 댓글을 불러오지 못했습니다. 클릭하여 다시 시도하세요.", + "comment__new_loading": "최신 댓글을 불러오는 중...", + "comment__new_title": "최신 댓글", + "comment__no_content": "아직 댓글이 없습니다", + "comment__refresh": "댓글 새로고침", + "comment__show": "노래 댓글", + "comment__title": "\"{name}\"의 댓글", + "comment__unavailable": "이 노래의 댓글을 가져올 수 없습니다.", + "confirm_button_text": "예", + "copy_tip": " (클릭하여 복사)", + "date_format_hour": "{num}시간 전", + "date_format_minute": "{num}분 전", + "date_format_second": "{num}초 전", + "deep_link__handle_error_tip": "호출 실패: {message}", + "default": "기본값", + "default_list": "기본 목록", + "desktop_lyric__back": "뒤로", + "desktop_lyric__close": "닫기", + "desktop_lyric__font_decrease": "글자 크기 줄이기", + "desktop_lyric__font_increase": "글자 크기 늘리기", + "desktop_lyric__lock": "잠금", + "desktop_lyric__lrc_active_zoom_off": "재생 중인 가사 확대 해제", + "desktop_lyric__lrc_active_zoom_on": "재생 중인 가사 확대", + "desktop_lyric__opacity_decrease": "투명도 높이기\n(우클릭하여 세밀 조정)", + "desktop_lyric__opacity_increase": "투명도 낮추기\n(우클릭하여 세밀 조정)", + "desktop_lyric__theme": "테마 색상", + "desktop_lyric__unlock": "잠금 해제", + "desktop_lyric__win_top_off": "항상 위 유지", + "desktop_lyric__win_top_on": "항상 위 해제", + "download": "다운로드", + "download___status_completed": "다운로드 완료", + "download___status_error": "오류", + "download___status_paused": "일시정지됨", + "download___status_running": "다운로드 중", + "download___status_waiting": "다운로드 대기 중", + "download__all": "전체 작업", + "download__error": "오류", + "download__finished": "완료", + "download__high_quality": "고음질", + "download__lossless": "무손실", + "download__multiple_tip": "{len}곡 선택됨", + "download__multiple_tip2": "원하는 다운로드 음질을 선택하세요", + "download__normal": "일반", + "download__not_available_tip": "해당 음질을 사용할 수 없습니다.", + "download__paused": "일시정지됨", + "download__progress": "진행률", + "download__quality": "음질", + "download__running": "다운로드 중", + "download__status": "상태", + "download_status_error_check_path": "다운로드 경로 확인 중 오류가 발생했습니다. 설정한 다운로드 폴더가 정상인지 확인해 주세요", + "download_status_error_check_path_exist": "동일한 이름의 파일이 존재하여 다운로드를 건너뛰었습니다", + "download_status_error_refresh_url": "링크가 만료되어 새로고침 중...", + "download_status_error_response": "다운로드 실패: ", + "download_status_error_url_failed": "노래 링크를 가져오지 못했습니다", + "download_status_error_write": "다운로드 위치가 사용 중이거나 쓰기 권한이 없습니다. 다운로드 폴더를 변경하거나 앱/기기를 재시작해 보세요. 오류 상세: ", + "download_status_start": "다운로드 시작", + "download_status_url_getting": "노래 링크를 가져오는 중...", + "download_status_write_queue": "데이터 쓰는 중 ({num})", + "duplicate_list_tip": "이미 \"{name}\" 목록을 담은 적이 있습니다. 목록의 곡을 업데이트하시겠습니까?", + "export": "내보내기", + "fullscreen_exit": "전체화면 종료", + "history_clear": "기록 지우기", + "history_remove": "우클릭하여 이 항목 삭제", + "history_search": "검색 기록", + "import": "가져오기", + "leaderboard": "차트", + "list__add_to": "추가할 위치...", + "list__collect": "담기", + "list__copy_name": "이름 복사", + "list__dislike": "싫어요", + "list__download": "다운로드", + "list__export_part_desc": "목록 파일을 저장할 위치를 선택하세요", + "list__file": "파일 위치 열기", + "list__import_part_button_cancel": "취소", + "list__import_part_button_confirm": "덮어쓰기", + "list__import_part_confirm": "가져온 목록({importName})의 ID가 로컬 목록({localName})과 동일합니다. 로컬 목록을 덮어쓰시겠습니까?", + "list__import_part_desc": "목록 파일을 선택하세요", + "list__load_failed": "이런, 불러오기에 실패했습니다 😭", + "list__loading": "목록을 불러오는 중...⏳", + "list__move_to": "이동할 위치...", + "list__movedown": "아래로 이동", + "list__moveup": "위로 이동", + "list__name_default": "기본 목록", + "list__name_love": "좋아요", + "list__new_list_btn": "새 목록", + "list__new_list_input": "새 목록...", + "list__pause": "정지", + "list__play": "재생", + "list__play_later": "다음에 재생", + "list__remove": "삭제", + "list__remove_tip": "\"{name}\"을(를) 정말 삭제하시겠습니까?", + "list__remove_tip_button": "예, 맞습니다", + "list__rename": "이름 변경", + "list__search": "검색", + "list__sort": "순서 조정", + "list__source_detail": "노래 상세정보", + "list__start": "재개", + "list__sync": "업데이트", + "list__toggle_source": "소스 변경", + "list_add__btn_title": "\"{name}\"에 곡 추가", + "list_add__multiple_btn_title": "이 곡들을 \"{name}\"에 추가", + "list_add__multiple_title_add": "선택한 {num}곡을 다음 위치에 추가...", + "list_add__multiple_title_move": "선택한 {num}곡을 다음 위치로 이동...", + "list_add__title_first_add": "추가", + "list_add__title_first_move": "이동", + "list_add__title_last": "위치로...", + "list_duplicate_tip": "동일한 이름의 목록이 이미 존재합니다. 계속 생성하시겠습니까?", + "list_import_tip__alldata": "가져오기에 실패했습니다. 이 파일은 \"전체 데이터\" 백업 파일입니다. 다음 위치에서 가져와야 합니다: \n\n설정 → 백업 및 복원 → 전체 데이터 → 가져오기", + "list_import_tip__playlist": "가져오기에 실패했습니다. 이 파일은 \"목록\" 백업 파일입니다. 다음 위치에서 가져와야 합니다: \n\n설정 → 백업 및 복원 → 일부 데이터 → 목록 가져오기", + "list_import_tip__playlist_part": "가져오기에 실패했습니다. 이 파일은 \"목록 전용\" 백업 파일입니다. 다음 위치에서 가져와야 합니다: \n\n내 서재 → 아무 목록 이름에서 우클릭 → 메뉴에서 '가져오기' 선택", + "list_import_tip__setting": "가져오기에 실패했습니다. 이 파일은 \"설정\" 백업 파일입니다. 다음 위치에서 가져와야 합니다: \n\n설정 → 백업 및 복원 → 일부 데이터 → 설정 가져오기", + "list_import_tip__unknown": "가져오기에 실패했습니다. 알 수 없는 파일 형식입니다. 앱을 최신 버전으로 업데이트한 후 다시 시도해 주세요.", + "list_sort_modal_by_album": "앨범", + "list_sort_modal_by_down": "내림차순", + "list_sort_modal_by_field": "정렬 기준", + "list_sort_modal_by_name": "제목", + "list_sort_modal_by_random": "무작위", + "list_sort_modal_by_singer": "아티스트", + "list_sort_modal_by_source": "음악 서비스", + "list_sort_modal_by_time": "길이", + "list_sort_modal_by_type": "정렬 항목", + "list_sort_modal_by_up": "오름차순", + "list_sort_modal_tip_confirm": "정말로 실행하시겠습니까?", + "list_update_modal__auto_update": "자동 업데이트", + "list_update_modal__tips": "💡 \"자동 업데이트\"가 체크된 목록은 앱을 시작할 때마다 자동으로 업데이트됩니다.", + "list_update_modal__title": "목록 업데이트 관리", + "list_update_modal__update": "동기화", + "lists__add_local_file_desc": "노래 파일을 선택하세요", + "lists__dislike_music_singer_tip": "{singer}의 \"{name}\"을(를) 정말 싫어요 처리하시겠습니까?", + "lists__dislike_music_tip": "\"{name}\"을(를) 정말 싫어요 처리하시겠습니까?", + "lists__duplicate": "중복된 곡", + "lists__export": "내보내기", + "lists__export_part_desc": "목록 파일을 저장할 위치를 선택하세요", + "lists__import": "가져오기", + "lists__import_part_button_cancel": "아니요", + "lists__import_part_button_confirm": "덮어쓰기", + "lists__import_part_confirm": "가져온 목록({importName})의 ID가 로컬 목록({localName})과 동일합니다. 로컬 목록을 덮어쓰시겠습니까?", + "lists__import_part_desc": "목록 파일을 선택하세요", + "lists__new_list_btn": "목록 만들기", + "lists__new_list_input": "새 목록...", + "lists__remove": "삭제", + "lists__remove_music_tip": "선택한 {len}곡을 정말 삭제하시겠습니까?", + "lists__remove_tip": "\"{name}\"을(를) 정말 삭제하시겠습니까?", + "lists__remove_tip_button": "예, 맞습니다", + "lists__rename": "이름 변경", + "lists__select_local_file": "로컬 곡 추가", + "lists__sort_list": "곡 정렬", + "lists__source_detail": "재생목록 페이지", + "lists__sync": "업데이트", + "lists__sync_confirm_tip": "\"{name}\"의 곡을 온라인 목록의 곡으로 교체합니다. 업데이트하시겠습니까?", + "load_list_file_error_detail": "이전 목록 파일을 {path}에 백업해 두었습니다.\nJSON 형식으로 저장되어 있으며, 수동으로 복구를 시도할 수 있습니다.\n\n오류 상세: {detail}", + "load_list_file_error_title": "재생목록 데이터 로드 오류", + "loading": "불러오는 중...", + "love_list": "즐겨찾기", + "lyric__load_error": "가사를 가져오지 못했습니다", + "lyric__select": "가사 텍스트 선택 허용", + "lyric_menu__align": "가사 정렬", + "lyric_menu__align_center": "가운데", + "lyric_menu__align_left": "왼쪽", + "lyric_menu__lrc_size": "글자 크기 [{size}]", + "lyric_menu__offset": "오프셋 [{offset}ms]", + "lyric_menu__offset_add_10": "10ms 늘리기", + "lyric_menu__offset_add_100": "100ms 늘리기", + "lyric_menu__offset_dec_10": "10ms 줄이기", + "lyric_menu__offset_dec_100": "100ms 줄이기", + "lyric_menu__offset_reset": "초기화", + "lyric_menu__size_add": "글자 크기 늘리기\n(우클릭하여 세밀 조정)", + "lyric_menu__size_dec": "글자 크기 줄이기\n(우클릭하여 세밀 조정)", + "lyric_menu__size_reset": "초기화", + "media_device__empty_device_tip": "오디오 출력 장치가 비어 있습니다. 재생이 되지 않는다면 사운드 카드 드라이버가 설치되어 있는지 확인해 주세요. \n또는 소프트웨어를 제거한 후 재설치할 때 \"나만 사용\"을 선택하고 기본 설치 경로를 유지해 보세요.", + "min": "최소화", + "music_album": "앨범", + "music_duplicate": "중복된 곡", + "music_name": "제목", + "music_singer": "아티스트", + "music_sort__input_tip": "이동할 위치를 입력하세요", + "music_sort__title": "\"{name}\"의 위치를 다음으로 조정: ", + "music_sort__title_multiple": "선택한 {num}곡의 위치를 다음으로 조정: ", + "music_time": "길이", + "music_toggle_clean": "변경 취소", + "music_toggle_confirm": "확인", + "music_toggle_duplicate_tip": "목록에 동일한 곡이 이미 존재합니다. 삭제하고 계속할까요?", + "my_list": "내 서재", + "no_item": "아무것도 없습니다...", + "not_agree": "거부", + "ok": "확인", + "pagination__next": "다음 페이지", + "pagination__page": "{num} 페이지", + "pagination__prev": "이전 페이지", + "play_timeout": "타이머 정지", + "play_timeout_close": "닫기", + "play_timeout_confirm": "확인", + "play_timeout_end": "곡이 끝난 후 정지", + "play_timeout_stop": "타이머 취소", + "play_timeout_tip": "{time} 후 정지", + "play_timeout_unit": "분", + "play_timeout_update": "시간 업데이트", + "player__add_music_to": "현재 곡을 다음 위치에 추가...", + "player__buffering": "버퍼링 중...", + "player__desktop_lyric_lock": "우클릭하여 가사창 잠금", + "player__desktop_lyric_off": "가사창 숨기기", + "player__desktop_lyric_on": "가사창 표시", + "player__desktop_lyric_unlock": "우클릭하여 가사창 잠금 해제", + "player__end": "정지됨", + "player__error": "음악을 불러오는 중 오류가 발생했습니다. 5초 후 다음 곡으로 전환합니다", + "player__getting_url": "음악 링크를 가져오는 중...", + "player__getting_url_delay_retry": "서버가 혼잡합니다. {time}초 후 다시 시도합니다...", + "player__hide_detail_tip": "상세 페이지 숨기기\n(앱 창에서 우클릭하면 상세 페이지를 빠르게 숨길 수 있습니다)", + "player__loading": "음악을 불러오는 중...", + "player__music_album": "앨범: ", + "player__music_name": "제목: ", + "player__music_singer": "아티스트: ", + "player__next": "다음 곡", + "player__pause": "일시정지", + "player__pic_tip": "재생 상세 페이지\n(우클릭하면 \"내 서재\"에서 현재 재생 중인 곡을 찾을 수 있습니다)", + "player__play": "재생", + "player__play_toggle_mode_list": "순서대로", + "player__play_toggle_mode_list_loop": "목록 반복", + "player__play_toggle_mode_off": "사용 안 함", + "player__play_toggle_mode_random": "무작위 재생", + "player__play_toggle_mode_single_loop": "한 곡 반복", + "player__playback_preserves_pitch": "음정 보정", + "player__playback_rate": "현재 재생 속도: ", + "player__playback_rate_reset_btn": "초기화", + "player__playing": "재생 중...", + "player__prev": "이전 곡", + "player__refresh_url": "음악 URL이 만료되어 새로고침 중...", + "player__sound_effect": "음향 옵션 (실험적 기능)", + "player__sound_effect_biquad_filter": "이퀄라이저", + "player__sound_effect_biquad_filter_preset_classical": "클래식", + "player__sound_effect_biquad_filter_preset_dance": "댄스", + "player__sound_effect_biquad_filter_preset_electronic": "일렉트로닉", + "player__sound_effect_biquad_filter_preset_pop": "팝", + "player__sound_effect_biquad_filter_preset_rock": "록", + "player__sound_effect_biquad_filter_preset_slow": "슬로우", + "player__sound_effect_biquad_filter_preset_soft": "소프트", + "player__sound_effect_biquad_filter_preset_subwoofer": "서브우퍼", + "player__sound_effect_biquad_filter_preset_vocal": "보컬", + "player__sound_effect_biquad_filter_reset_btn": "초기화", + "player__sound_effect_biquad_filter_save_btn": "프리셋 다른 이름으로 저장", + "player__sound_effect_biquad_filter_save_input": "새 프리셋...", + "player__sound_effect_convolution": "환경 리버브 음향 효과", + "player__sound_effect_convolution_file_bright_hall": "홀", + "player__sound_effect_convolution_file_cardiod_35_10_spread": "하트형 확산", + "player__sound_effect_convolution_file_cinema_diningroom": "영화관", + "player__sound_effect_convolution_file_dining_living_true_stereo": "식당", + "player__sound_effect_convolution_file_feedback_spring": "피드백 스프링", + "player__sound_effect_convolution_file_living_bedroom_leveled": "욕실", + "player__sound_effect_convolution_file_matrix_1": "매트릭스 (1)", + "player__sound_effect_convolution_file_matrix_2": "매트릭스 (2)", + "player__sound_effect_convolution_file_s2_r4_bd": "교회", + "player__sound_effect_convolution_file_s3_r1_bd": "스테레오", + "player__sound_effect_convolution_file_spreader50_65ms": "실내", + "player__sound_effect_convolution_file_telephone": "전화기", + "player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "마그네틱 스테레오", + "player__sound_effect_convolution_main_gain": "원음 게인", + "player__sound_effect_convolution_send_gain": "환경 음향 효과 게인", + "player__sound_effect_features_tip": "안내: 음향 옵션은 사용자 지정 오디오 출력 장치와 충돌합니다. 음향 옵션을 활성화하면 오디오 출력 장치가 기본 출력 장치로 재설정됩니다. 현재로서는 이 문제를 해결할 수 없습니다.", + "player__sound_effect_panner": "3D 입체 서라운드 (헤드폰 사용 필요)", + "player__sound_effect_panner_enabled": "사용", + "player__sound_effect_panner_sound_r": "음원 거리", + "player__sound_effect_panner_sound_speed": "서라운드 속도", + "player__sound_effect_pitch_shifter": "음정 조정", + "player__sound_effect_pitch_shifter_preset_semitones": "{num}반음", + "player__sound_effect_pitch_shifter_reset_btn": "초기화", + "player__sound_effect_pitch_shifter_tip": "음정을 높이거나 낮추려면 오디오 데이터를 실시간으로 처리해야 하므로 CPU 사용량이 추가로 발생합니다.\n\n알려진 문제:\nCPU 자원이 부족하면 처리 작업이 쌓여 소리에 이상이 발생할 수 있습니다.\n이 경우 재생을 잠시 멈추고 쌓인 작업이 처리될 때까지 기다린 후 재생해야 합니다.", + "player__stop": "일시정지됨", + "player__volume": "볼륨: ", + "player__volume_mute_label": "음소거", + "player__volume_muted": "음소거됨", + "search": "검색", + "search__hot_search": "인기 검색어", + "search__type_music": "노래", + "search__type_songlist": "재생목록", + "search__welcome": "원하는 것을 검색해보세요~~ 😉", + "setting": "설정", + "setting__about": "LX Music 정보", + "setting__backup": "백업 및 복원", + "setting__backup_all": "전체 데이터 (\"목록\" 데이터와 \"설정\" 데이터)", + "setting__backup_all_export": "내보내기", + "setting__backup_all_export_desc": "백업을 저장할 위치...", + "setting__backup_all_import": "가져오기", + "setting__backup_all_import_desc": "백업 파일을 선택하세요", + "setting__backup_other": "기타 백업 형식 (해당 백업 파일의 복원은 현재 지원하지 않습니다)", + "setting__backup_other_export_dir": "파일을 저장할 위치를 선택하세요", + "setting__backup_other_export_list_csv": "CSV 형식으로 목록 내보내기", + "setting__backup_other_export_list_text": "TXT 형식으로 목록 내보내기", + "setting__backup_other_export_list_text_confirm": "모든 목록을 하나의 파일로 합치시겠습니까?", + "setting__backup_part": "일부 데이터 (\"목록\" 데이터에는 \"기본\", \"좋아요\" 및 사용자가 만든 목록이 포함됩니다. \"설정\" 데이터에는 \"단축키\" 설정이 포함되지 않습니다)", + "setting__backup_part_export_list": "목록 내보내기", + "setting__backup_part_export_list_desc": "목록을 저장할 위치...", + "setting__backup_part_export_setting": "설정 내보내기", + "setting__backup_part_export_setting_desc": "설정을 저장할 위치...", + "setting__backup_part_import_list": "목록 가져오기", + "setting__backup_part_import_list_confirm": "백업 파일의 목록이 기존 목록과 동일한 ID를 가지고 있으면 기존 목록의 곡이 덮어써집니다. 계속하시겠습니까?", + "setting__backup_part_import_list_desc": "목록 백업 파일을 선택하세요", + "setting__backup_part_import_setting": "설정 가져오기", + "setting__backup_part_import_setting_desc": "설정 백업 파일을 선택하세요", + "setting__basic": "일반", + "setting__basic_animation": "팝업 애니메이션 무작위 적용", + "setting__basic_control_btn_position": "컨트롤 버튼 위치", + "setting__basic_control_btn_position_left": "왼쪽", + "setting__basic_control_btn_position_right": "오른쪽", + "setting__basic_font": "글꼴", + "setting__basic_font_size": "글자 크기", + "setting__basic_font_size_14px": "더 작게", + "setting__basic_font_size_15px": "작게", + "setting__basic_font_size_16px": "표준", + "setting__basic_font_size_17px": "크게", + "setting__basic_font_size_18px": "더 크게", + "setting__basic_font_size_19px": "매우 크게", + "setting__basic_lang": "언어", + "setting__basic_lang_title": "앱에 표시되는 언어", + "setting__basic_playbar_progress_style": "재생바 진행바 스타일", + "setting__basic_playbar_progress_style_full": "전체 너비", + "setting__basic_playbar_progress_style_middle": "중간", + "setting__basic_playbar_progress_style_mini": "미니", + "setting__basic_show_animation": "애니메이션 표시", + "setting__basic_source": "음악 API", + "setting__basic_source_status_failed": "초기화 실패", + "setting__basic_source_status_initing": "초기화 중...", + "setting__basic_source_status_success": "초기화 성공", + "setting__basic_source_temp": "임시 API (일부 기능 사용 불가. 테스트 API를 사용할 수 없을 때의 대안)", + "setting__basic_source_test": "테스트 API (대부분의 앱 기능 사용 가능)", + "setting__basic_source_user_api_btn": "음악 API 관리", + "setting__basic_sourcename": "음악 스트리밍 서비스 이름", + "setting__basic_sourcename_alias": "별칭", + "setting__basic_sourcename_real": "원래 이름", + "setting__basic_sourcename_title": "음악 스트리밍 서비스 이름 표시 방식을 선택하세요", + "setting__basic_start_in_fullscreen": "전체화면 모드로 시작", + "setting__basic_theme": "테마", + "setting__basic_theme_auto_tip": "이것은 동적 테마입니다. 라이트 테마와 다크 테마를 미리 설정해 두면 시스템의 라이트/다크 모드에 따라 자동으로 해당 테마로 전환됩니다.\n\n참고: 이 테마 항목을 우클릭하면 라이트/다크 테마 설정 창이 열립니다.", + "setting__basic_to_tray": "앱 창을 닫을 때 시스템 트레이로 최소화", + "setting__basic_window_size": "창 크기", + "setting__basic_window_size_big": "크게", + "setting__basic_window_size_huge": "아주 크게", + "setting__basic_window_size_larger": "더 크게", + "setting__basic_window_size_medium": "중간", + "setting__basic_window_size_oversized": "매우 크게", + "setting__basic_window_size_small": "작게", + "setting__basic_window_size_smaller": "더 작게", + "setting__basic_window_size_title": "창 크기를 설정하세요", + "setting__click_copy": "클릭하여 복사", + "setting__click_open": "클릭하여 열기", + "setting__desktop_lyric": "데스크톱 가사", + "setting__desktop_lyric_align": "가사 정렬", + "setting__desktop_lyric_align_center": "가운데", + "setting__desktop_lyric_align_left": "왼쪽", + "setting__desktop_lyric_align_right": "오른쪽", + "setting__desktop_lyric_always_on_top": "항상 위 유지", + "setting__desktop_lyric_always_on_top_loop": "\"항상 위 유지\"가 활성화된 경우 가사창을 반복적으로 새로고침", + "setting__desktop_lyric_always_on_top_loop_tip": "다른 프로그램에 가사창이 계속 가려질 경우 이 옵션을 활성화해 보세요.", + "setting__desktop_lyric_audio_visualization": "오디오 시각화 활성화 (실험적 기능)", + "setting__desktop_lyric_color": "가사 글자 색상", + "setting__desktop_lyric_color_reset": "색상 초기화", + "setting__desktop_lyric_delay_scroll": "가사 스크롤 지연", + "setting__desktop_lyric_direction": "가사 표시 방향", + "setting__desktop_lyric_direction_horizontal": "가로", + "setting__desktop_lyric_direction_vertical": "세로", + "setting__desktop_lyric_ellipsis": "가사 줄바꿈 안 함", + "setting__desktop_lyric_enable": "가사창 표시", + "setting__desktop_lyric_font": "가사 글꼴", + "setting__desktop_lyric_font_default": "기본값", + "setting__desktop_lyric_font_weight": "굵은 글꼴", + "setting__desktop_lyric_font_weight_extended": "번역 및 로마자 가사", + "setting__desktop_lyric_font_weight_font": "글자 단위 가사", + "setting__desktop_lyric_font_weight_line": "줄 단위 가사", + "setting__desktop_lyric_fullscreen_hide": "전체화면일 때 가사창 숨기기", + "setting__desktop_lyric_hover_hide": "마우스가 가사창 위로 이동하면 가사창 투명도 높이기", + "setting__desktop_lyric_hover_hide_tip": "이 기능은 플랫폼 호환성 문제가 있을 수 있습니다.", + "setting__desktop_lyric_line_gap": "가사 간격 ({num})", + "setting__desktop_lyric_line_gap_add": "간격 늘리기", + "setting__desktop_lyric_line_gap_dec": "간격 줄이기", + "setting__desktop_lyric_lock": "가사창 잠금", + "setting__desktop_lyric_lock_screen": "가사창을 메인 화면 밖으로 드래그하지 못하도록 제한", + "setting__desktop_lyric_pause_hide": "음악을 일시정지하면 가사창 투명도 높이기", + "setting__desktop_lyric_played_color": "재생된 부분", + "setting__desktop_lyric_reset": "초기화", + "setting__desktop_lyric_reset_window": "데스크톱 가사 옵션 초기화", + "setting__desktop_lyric_scroll_align": "재생 중 가사 스크롤 위치", + "setting__desktop_lyric_scroll_align_center": "가운데", + "setting__desktop_lyric_scroll_align_top": "위쪽", + "setting__desktop_lyric_shadow_color": "그림자", + "setting__desktop_lyric_show_taskbar": "작업 표시줄에 가사창 프로세스 표시", + "setting__desktop_lyric_show_taskbar_tip": "화면 녹화 프로그램이 가사창을 캡처하지 못할 때 대안으로 사용하는 옵션입니다.", + "setting__desktop_lyric_unplay_color": "재생 안 된 부분", + "setting__dislike_list_input_tip": "곡명@아티스트명\n곡명\n@아티스트명", + "setting__dislike_list_save_btn": "저장", + "setting__dislike_list_tips": "1. 한 줄에 하나의 항목을 입력합니다. 곡명이나 아티스트명에 \"@\" 기호가 있으면 \"#\"로 대체해야 합니다\n2. 특정 아티스트의 곡 지정: 곡명@아티스트명\n3. 곡 지정: 곡명\n4. 아티스트 지정: @아티스트명", + "setting__dislike_list_title": "싫어요 곡 규칙 목록", + "setting__download": "다운로드", + "setting__download_data_embed": "다음 항목을 오디오 파일에 삽입", + "setting__download_embed_lxlyric": "LX Music 가사도 함께 삽입 (있는 경우, LX Music으로 재생 시 글자 단위 가사를 지원합니다)", + "setting__download_embed_lyric": "가사 삽입", + "setting__download_embed_pic": "커버 이미지 삽입", + "setting__download_embed_rlyric": "가능한 경우 로마자 가사도 함께 삽입", + "setting__download_embed_tlyric": "가능한 경우 번역 가사도 함께 삽입", + "setting__download_enable": "다운로드 활성화", + "setting__download_lxlyric": "LX Music 가사도 가사 파일에 함께 기록 (있는 경우, LX Music으로 재생 시 글자 단위 가사를 지원합니다)", + "setting__download_lyric": "가사 다운로드", + "setting__download_lyric_format": "다운로드한 가사 파일의 인코딩 형식", + "setting__download_lyric_format_gbk": "GBK", + "setting__download_lyric_format_tip": "일부 기기에서 중국어가 깨져 보인다면 GBK 형식을 선택해 보세요.", + "setting__download_lyric_format_utf8": "UTF-8", + "setting__download_lyric_title": "가사 파일 다운로드 여부를 선택하세요", + "setting__download_max_num": "동시 다운로드 개수", + "setting__download_max_num_tip": "동시 다운로드 개수가 너무 많으면 음악 API에서 IP가 차단될 수 있습니다. 변경하시겠습니까?", + "setting__download_max_num_tooltip": "값을 너무 높게 설정하면 음악 API에 따라 IP가 차단될 수 있습니다.", + "setting__download_name": "음악 파일 이름 규칙", + "setting__download_name1": "\"제목 - 아티스트\"", + "setting__download_name2": "\"아티스트 - 제목\"", + "setting__download_name3": "제목만", + "setting__download_name_title": "다운로드 시 음악 파일 이름 형식을 선택하세요", + "setting__download_path": "다운로드 경로", + "setting__download_path_change_btn": "변경", + "setting__download_path_label": "현재: ", + "setting__download_path_open_label": "클릭하여 이 경로 열기", + "setting__download_path_title": "다운로드 경로를 선택하세요", + "setting__download_rlyric": "가능한 경우 로마자 가사도 가사 파일에 함께 기록", + "setting__download_select_save_path": "저장 경로를 선택하세요", + "setting__download_skip_exist_file": "다운로드 폴더에 동일한 이름의 파일이 있으면 다운로드 작업 건너뛰기", + "setting__download_tlyric": "가능한 경우 번역 가사도 가사 파일에 함께 기록", + "setting__download_use_other_source": "다운로드 시 자동 소스 전환", + "setting__download_use_other_source_tip": "원래 소스에서 곡을 다운로드할 수 없을 때 다른 소스로 전환하여 다운로드를 시도합니다.\n참고: 소스를 변경한 후의 곡 버전이 원래 버전과 100% 일치한다고 보장하지는 않습니다.", + "setting__hot_key": "단축키", + "setting__hot_key_common_focus_search_input": "검색창 포커스", + "setting__hot_key_common_min": "앱 최소화", + "setting__hot_key_common_toggle_close": "앱 종료", + "setting__hot_key_common_toggle_hide": "앱 표시/숨기기", + "setting__hot_key_common_toggle_min": "앱 최소화/복원", + "setting__hot_key_desktop_lyric_toggle_always_top": "데스크톱 가사 항상 위/해제", + "setting__hot_key_desktop_lyric_toggle_lock": "데스크톱 가사 잠금/잠금 해제", + "setting__hot_key_desktop_lyric_toggle_visible": "데스크톱 가사 표시/숨기기", + "setting__hot_key_global_title": "전역 단축키", + "setting__hot_key_local_title": "앱 내 단축키", + "setting__hot_key_player_music_dislike": "곡 싫어요", + "setting__hot_key_player_music_love": "곡 좋아요", + "setting__hot_key_player_music_unlove": "곡 좋아요 취소", + "setting__hot_key_player_next": "다음 곡", + "setting__hot_key_player_prev": "이전 곡", + "setting__hot_key_player_seekbackward": "5초 되감기", + "setting__hot_key_player_seekforward": "5초 빨리감기", + "setting__hot_key_player_toggle_play": "재생/일시정지", + "setting__hot_key_player_volume_down": "볼륨 낮추기", + "setting__hot_key_player_volume_mute": "음소거 전환", + "setting__hot_key_player_volume_up": "볼륨 높이기", + "setting__hot_key_tip_input": "새 키를 입력하세요", + "setting__hot_key_unset_input": "설정 안 됨", + "setting__is_enable": "사용", + "setting__is_show": "표시", + "setting__list": "목록", + "setting__list_action_btn": "목록 작업 버튼 표시", + "setting__list_add_music_location_type": "목록에 곡 추가 시 위치", + "setting__list_add_music_location_type_bottom": "맨 아래", + "setting__list_add_music_location_type_top": "맨 위", + "setting__list_click_action": "목록에서 곡을 더블클릭하면 자동으로 현재 목록으로 전환 (\"재생목록\" 및 \"차트\" 페이지에서만 적용)", + "setting__list_scroll": "재생목록 스크롤 위치 기억 (\"내 서재\" 페이지에서만 적용)", + "setting__list_source": "곡이 어느 음악 스트리밍 서비스에서 왔는지 표시 (\"내 서재\" 페이지에서만 적용)", + "setting__network": "네트워크", + "setting__network_proxy_host": "호스트", + "setting__network_proxy_password": "비밀번호", + "setting__network_proxy_port": "포트", + "setting__network_proxy_title": "HTTP 프록시 (설정이 잘못되면 앱이 네트워크에 연결되지 않습니다)", + "setting__network_proxy_username": "사용자 이름", + "setting__odc": "자동 비우기", + "setting__odc_clear_search_input": "검색하지 않을 때 검색창 비우기", + "setting__odc_clear_search_list": "검색하지 않을 때 검색 목록 비우기", + "setting__open_api": "오픈 API", + "setting__open_api_address": "서비스 주소: ", + "setting__open_api_bind_lan": "LAN에서 접속 허용", + "setting__open_api_enable": "오픈 API 서비스 활성화", + "setting__open_api_port": "서비스 포트", + "setting__open_api_port_tip": "오픈 API 서비스 포트를 입력하세요", + "setting__open_api_tip": "이 기능은 타사 프로그램이 LX Music을 호출할 수 있도록 제공합니다. 현재 사용 가능한 기능을 확인하세요: ", + "setting__open_api_tip_link": "문서 보기", + "setting__other": "기타", + "setting__other_dislike_list": "싫어요 곡 규칙", + "setting__other_dislike_list_label": "규칙 개수: ", + "setting__other_dislike_list_show_btn": "규칙 편집", + "setting__other_listdata": "목록 데이터 정리", + "setting__other_listdata_clear_btn": "\"내 서재\" 데이터 지우기", + "setting__other_listdata_clear_tip_confirm": "생성한 모든 목록과 좋아요한 모든 곡이 지워집니다. 정말 계속하시겠습니까?", + "setting__other_lyric_edited_cache": "오프셋 조정된 가사 관리", + "setting__other_lyric_edited_clear_btn": "시간 조정된 가사 지우기", + "setting__other_lyric_edited_clear_tip_confirm": "이전에 오프셋을 조정한 모든 가사가 지워집니다. 지우시겠습니까? \n(손 떨림으로 확인 🤪)", + "setting__other_lyric_edited_label": "가사 개수: ", + "setting__other_lyric_raw_clear_btn": "가사 캐시 지우기", + "setting__other_lyric_raw_label": "가사 개수: ", + "setting__other_music_url_clear_btn": "곡 URL 캐시 지우기", + "setting__other_music_url_label": "곡 URL 개수: ", + "setting__other_other_cache": "기타 캐시 관리", + "setting__other_other_source_clear_btn": "소스 변경된 곡 캐시 지우기", + "setting__other_other_source_label": "소스가 변경된 곡 정보 개수: ", + "setting__other_resource_cache": "리소스 캐시 관리", + "setting__other_resource_cache_clear_btn": "리소스 캐시 지우기", + "setting__other_resource_cache_confirm": "지우겠습니다", + "setting__other_resource_cache_label": "앱이 사용 중인 캐시 크기: ", + "setting__other_resource_cache_tip": "이미지, 오디오 등의 캐시를 포함합니다. 정리 후에는 해당 리소스를 다시 다운로드해야 합니다. 정리하지 않는 것을 권장하며, 앱이 디스크 공간에 따라 캐시 크기를 동적으로 관리합니다.", + "setting__other_resource_cache_tip_confirm": "이미지, 오디오 등의 캐시를 포함하며, 정리 후에는 해당 리소스를 다시 다운로드해야 합니다. 정리하지 않는 것을 권장하며, 앱이 디스크 공간에 따라 캐시 크기를 동적으로 관리합니다. 그래도 정리하시겠습니까?", + "setting__other_transparent_window": "메인 창에 내장된 둥근 모서리와 그림자 사용", + "setting__other_transparent_window_tip": "이 옵션을 끄면 시스템 기본 창 스타일을 사용합니다. 변경 사항을 적용하려면 소프트웨어를 재시작해야 합니다.", + "setting__other_tray_theme": "트레이 아이콘 스타일", + "setting__other_tray_theme_auto": "시스템 따르기", + "setting__other_tray_theme_black": "검정", + "setting__other_tray_theme_native": "흰색", + "setting__other_tray_theme_origin": "기본값", + "setting__play": "재생", + "setting__play_advanced_audio_features_tip": "사용자 지정 오디오 출력 장치는 이 기능과 충돌합니다. 이 기능을 활성화하면 오디오 출력 장치가 기본 출력 장치로 재설정됩니다. 현재로서는 이 문제를 해결할 수 없습니다. 그래도 활성화하시겠습니까?", + "setting__play_auto_clean_played_list": "현재 재생목록과 동일한 목록을 재생할 때 재생 완료 목록 비우기", + "setting__play_auto_clean_played_list_tip": "무작위 재생 모드에서 목록의 모든 곡이 다시 무작위 대상에 포함됩니다.", + "setting__play_auto_skip_on_error": "재생 오류 발생 시 자동으로 곡 전환", + "setting__play_detail": "재생 상세 페이지", + "setting__play_detail_align": "가사 정렬", + "setting__play_detail_align_center": "가운데", + "setting__play_detail_align_left": "왼쪽", + "setting__play_detail_align_right": "오른쪽", + "setting__play_detail_font_size": "가사 글자 크기 (재생 상세 페이지에서 키보드 \"+\"와 \"-\"로 글자 크기를 조정할 수 있습니다)", + "setting__play_detail_font_size_current": "현재 글자 크기: {size}", + "setting__play_detail_font_size_reset": "초기화", + "setting__play_detail_font_zoom": "재생 중인 가사 확대", + "setting__play_detail_lyric_delay_scroll": "가사 스크롤 지연", + "setting__play_detail_lyric_progress": "가사를 드래그하여 재생 진행 위치 조정 허용", + "setting__play_lyric_lxlrc": "가능한 경우 노래방 스타일 가사로 재생", + "setting__play_lyric_lxlrc_tip": "이 기능은 높은 성능을 요구하므로 저사양 기기에서는 사용을 권장하지 않습니다!", + "setting__play_lyric_roma": "가능한 경우 로마자 가사 표시", + "setting__play_lyric_s2t": "재생 및 다운로드 중인 중국어 가사를 번체자로 변환", + "setting__play_lyric_transition": "가능한 경우 번역 가사 표시", + "setting__play_max_output_channel_count": "기기가 지원하는 최대 채널 수로 오디오 출력", + "setting__play_mediaDevice": "오디오 출력", + "setting__play_mediaDevice_remove_stop_play": "현재 오디오 출력 장치가 변경되면 재생 일시정지", + "setting__play_mediaDevice_title": "오디오 출력에 사용할 미디어 장치를 선택하세요", + "setting__play_media_device_error_tip": "이 기능은 고급 오디오 기능(오디오 시각화, 음향 옵션, 기기 최대 채널 수 오디오 출력)과 충돌합니다. 이번 앱 실행 시 해당 기능이 활성화되어 있어 이 옵션을 일시적으로 사용할 수 없습니다. 해당 기능을 끄고 앱을 재시작한 후 다시 변경해 주세요.", + "setting__play_media_device_tip": "이 기능은 오디오 시각화 기능과 충돌하여 동시에 사용할 수 없습니다. 오디오 시각화를 끄고 선택한 오디오 출력 옵션을 적용하시겠습니까?", + "setting__play_playQuality": "가능한 경우 재생 시 음질 우선", + "setting__play_power_save_blocker": "곡 재생 중 기기 절전 모드 방지", + "setting__play_save_play_time": "재생 진행 위치 기억", + "setting__play_startup_auto_play": "시작 시 자동으로 음악 재생", + "setting__play_statusbar_lyric": "상태 메뉴에 가사 표시", + "setting__play_statusbar_lyric_tip": "설정 → 일반에서 \"앱 창을 닫을 때 시스템 트레이로 최소화\"를 활성화해야 합니다", + "setting__play_task_bar": "작업 표시줄에 재생 진행률 표시", + "setting__play_timeout": "타이머 정지", + "setting__player_audio_visualization_tip": "사용자 지정 오디오 출력 장치 기능은 오디오 시각화 기능과 충돌합니다. 오디오 시각화를 활성화하면 오디오 출력 장치가 기본값으로 재설정됩니다. 현재로서는 이 문제를 해결할 수 없습니다. 그래도 활성화하시겠습니까?", + "setting__player_swap_lyric_trans_roma": "번역 가사와 로마자 가사의 위치 바꾸기", + "setting__search": "검색", + "setting__search_focus_search_box": "시작 시 검색창 자동 포커스", + "setting__search_history": "검색 기록 활성화", + "setting__search_hot": "인기 검색어 활성화", + "setting__sync": "데이터 동기화", + "setting__sync_client_address": "현재 기기 주소: {address}", + "setting__sync_client_host": "동기화 서비스 주소", + "setting__sync_client_host_tip": "http://<호스트>:<포트>", + "setting__sync_client_mode": "클라이언트 모드", + "setting__sync_client_status": "상태: {status}", + "setting__sync_code_blocked_ip": "현재 기기의 IP가 서버에 의해 차단되었습니다!", + "setting__sync_code_fail": "유효하지 않은 연결 코드", + "setting__sync_enable": "동기화 활성화", + "setting__sync_mode": "동기화 모드", + "setting__sync_mode_client": "클라이언트", + "setting__sync_mode_server": "서버", + "setting__sync_server_address": "동기화 서비스 주소: {address}", + "setting__sync_server_auth_code": "연결 코드: {code}", + "setting__sync_server_device": "연결된 기기: {devices}", + "setting__sync_server_device_list_btn_remove": "삭제", + "setting__sync_server_device_list_noitem": "아무것도 없습니다 ┗( ▔, ▔ )┛", + "setting__sync_server_device_list_time": "마지막 연결 시간: {time}", + "setting__sync_server_device_list_tips": "💡 기기를 삭제한 후 다시 연결하려면 연결 코드를 다시 입력해야 합니다", + "setting__sync_server_device_list_title": "인증된 기기", + "setting__sync_server_mode": "서버 모드 (데이터가 평문으로 전송되므로 신뢰할 수 있는 네트워크에서 사용하세요)", + "setting__sync_server_port": "동기화 포트", + "setting__sync_server_port_tip": "포트 번호를 입력하세요", + "setting__sync_server_refresh_code": "연결 코드 새로고침", + "setting__sync_server_show_device_list": "인증된 기기 목록", + "setting__sync_tip": "사용 방법은 FAQ의 \"동기화 기능\" 항목을 참고하세요", + "setting__update": "업데이트", + "setting__update_checking": "업데이트 확인 중...", + "setting__update_commit_date": "커밋 날짜: ", + "setting__update_commit_id": "커밋 해시: ", + "setting__update_current_label": "현재 버전: ", + "setting__update_downloading": "업데이트를 찾아 다운로드하는 중...⏳", + "setting__update_init": "업데이트 중...", + "setting__update_latest": "앱이 최신 버전입니다. 즐거운 시간 되세요!🥂", + "setting__update_latest_label": "최신 버전: ", + "setting__update_new_version": "새 버전을 찾았습니다. 어서 업데이트하세요~🚀🚀", + "setting__update_open_version_modal_btn": "업데이트 창 열기", + "setting__update_progress": "상태: ", + "setting__update_show_change_log": "업데이트 후 첫 실행 시 변경 로그 표시", + "setting__update_try_auto_update": "새 버전을 찾으면 자동으로 업데이트 다운로드 시도", + "setting__update_unknown": "알 수 없음", + "setting__update_unknown_tip": "❓ 최신 버전 정보를 가져오지 못했습니다. 정보 페이지에서 프로젝트 릴리스 주소를 열어 현재 버전이 최신인지 확인하는 것을 권장합니다", + "setting_download_save_group_list_name": "해당 목록 이름의 하위 폴더에 파일 저장", + "setting_sync_status_enabled": "연결됨", + "song_list": "재생목록", + "songlist__import_input_btn_confirm": "열기", + "songlist__import_input_show_btn": "재생목록 열기", + "songlist__import_input_tip": "재생목록 링크/ID를 입력하세요", + "songlist__import_input_tip_1": "다른 서비스 간 재생목록은 지원되지 않습니다. 열려는 재생목록이 현재 선택한 서비스에 해당하는지 확인해 주세요", + "songlist__import_input_tip_2": "열리지 않는 재생목록 링크가 있다면 피드백을 보내주세요", + "songlist__import_input_tip_3": "간편판의 재생목록 ID나 링크로는 쿠거우 재생목록을 열 수 없지만, 정식판의 쿠거우 코드나 링크로는 열 수 있습니다", + "songlist__import_input_tip_4": "넷이즈의 \"프라이빗 레이더\" 재생목록은 접근하려면 토큰이 필요합니다. 텐센트의 개인 재생목록은 프로필을 공개해야 접근할 수 있습니다. 자세한 내용은 다음을 참고하세요: ", + "songlist__import_input_title": "공유된 재생목록 열기", + "songlist__open_list": "\"{name}\" 재생목록 열기", + "songlist__tag_info_hot_tag": "인기 태그", + "source_alias_all": "통합", + "source_alias_bd": "BD 뮤직", + "source_alias_kg": "KG 뮤직", + "source_alias_kw": "KW 뮤직", + "source_alias_mg": "MG 뮤직", + "source_alias_tx": "TX 뮤직", + "source_alias_wy": "WY 뮤직", + "source_alias_xm": "XM 뮤직", + "source_all": "통합", + "source_bd": "바이두", + "source_kg": "쿠거우", + "source_kw": "쿠워", + "source_mg": "미구", + "source_tx": "텐센트", + "source_wy": "넷이즈", + "source_xm": "샤미", + "sync__auth_code_input_tip": "연결 코드를 입력하세요", + "sync__auth_code_title": "연결 코드 입력이 필요합니다", + "sync__dislike_merge_tip_desc": "두 목록의 내용을 병합하고 중복 항목을 제거합니다.", + "sync__dislike_other_tip_desc": "\"동기화 취소\"를 선택하면 \"싫어요 곡\" 목록을 동기화하지 않습니다.", + "sync__dislike_overwrite_tip_desc": "덮어써지는 쪽의 목록이 덮어쓰는 쪽의 목록으로 교체됩니다.", + "sync__dislike_title": "{name}의 \"싫어요 곡\" 목록과 동기화할 방법을 선택하세요", + "sync__list_merge_tip_desc": "두 목록을 병합합니다. 동일한 곡은 제거되고(병합 대상의 곡이 제거됨), 다른 곡은 추가됩니다.", + "sync__list_other_tip_desc": "\"동기화 취소\"를 선택하면 목록을 동기화하지 않습니다.", + "sync__list_overwrite_tip_desc": "덮어써지는 목록과 동일한 ID를 가진 목록은 삭제되고 덮어쓰는 쪽의 목록으로 교체됩니다(ID가 다른 목록은 병합됩니다). \"전체 덮어쓰기\"가 선택된 경우, 덮어써지는 쪽의 모든 목록이 제거되고 덮어쓰는 쪽의 목록으로 교체됩니다.", + "sync__list_title": "\"{name}\"과(와) 목록을 동기화할 방법을 선택하세요", + "sync__merge_btn_local_remote": "\"로컬 목록\"을 \"원격 목록\"과 병합", + "sync__merge_btn_remote_local": "\"원격 목록\"을 \"로컬 목록\"과 병합", + "sync__merge_label": "병합", + "sync__merge_tip": "병합: ", + "sync__other_label": "기타", + "sync__other_tip": "기타: ", + "sync__overwrite": "전체 덮어쓰기", + "sync__overwrite_btn_cancel": "동기화 취소", + "sync__overwrite_btn_local_remote": "\"로컬 목록\"으로 \"원격 목록\" 덮어쓰기", + "sync__overwrite_btn_none": "실시간 동기화만 사용", + "sync__overwrite_btn_remote_local": "\"원격 목록\"으로 \"로컬 목록\" 덮어쓰기", + "sync__overwrite_label": "덮어쓰기", + "sync__overwrite_tip": "덮어쓰기: ", + "sync_status_disabled": "연결 안 됨", + "tag__high_quality": "HQ", + "tag__lossless": "SQ", + "tag__lossless_24bit": "24bit", + "theme_add": "테마 추가", + "theme_auto": "자동", + "theme_auto_tip": "우클릭하여 라이트/다크 테마 옵션 창 열기", + "theme_black": "블랙", + "theme_blue": "블루", + "theme_blue2": "퍼플 블루", + "theme_blue_plus": "블루 플러스", + "theme_china_ink": "수묵", + "theme_edit_modal__app_bg": "앱 배경 색상", + "theme_edit_modal__aside_color": "사이드바 버튼 색상", + "theme_edit_modal__badge": "라벨 색상", + "theme_edit_modal__badge_primary": "메인 색상", + "theme_edit_modal__badge_secondary": "보조 색상", + "theme_edit_modal__badge_tertiary": "세 번째 색상", + "theme_edit_modal__bg_image": "배경 이미지", + "theme_edit_modal__bg_image_add": "배경 이미지 추가", + "theme_edit_modal__bg_image_change": "배경 이미지 변경", + "theme_edit_modal__bg_image_remove": "배경 이미지 제거", + "theme_edit_modal__close_btn": "닫기", + "theme_edit_modal__control_btn": "왼쪽 컨트롤 버튼 색상", + "theme_edit_modal__copy": "테마 복사", + "theme_edit_modal__dark": "다크 테마", + "theme_edit_modal__dark_font": "다크 글꼴", + "theme_edit_modal__font": "글꼴 색상", + "theme_edit_modal__hide_btn": "상세 페이지 숨기기", + "theme_edit_modal__main_bg": "콘텐츠 영역 배경 색상", + "theme_edit_modal__min_btn": "최소화", + "theme_edit_modal__pick_cancel": "초기화", + "theme_edit_modal__pick_color": "색상 선택", + "theme_edit_modal__pick_last_color": "이전 색상 사용", + "theme_edit_modal__pick_save": "확인", + "theme_edit_modal__preview": "미리보기", + "theme_edit_modal__primary": "테마 색상", + "theme_edit_modal__remove": "삭제", + "theme_edit_modal__remove_tip": "이 테마를 정말 삭제하시겠습니까?", + "theme_edit_modal__save_new": "복사", + "theme_edit_modal__select_bg_file": "배경 이미지를 선택하세요", + "theme_edit_modal__title_add": "테마 추가", + "theme_edit_modal__title_edit": "테마 편집", + "theme_green": "그린", + "theme_grey": "그레이", + "theme_happy_new_year": "새해", + "theme_max_tip": "테마는 최대 10개까지 추가할 수 있습니다. 일부를 삭제하고 다시 추가해 보세요 😜", + "theme_mid_autumn": "추석", + "theme_ming": "밍", + "theme_more_btn_show": "더 많은 테마", + "theme_naruto": "나루토", + "theme_orange": "오렌지", + "theme_pink": "핑크", + "theme_purple": "퍼플", + "theme_red": "레드", + "theme_selector_modal__dark_title": "다크 테마", + "theme_selector_modal__light_title": "라이트 테마", + "theme_selector_modal__theme_name": "테마 이름", + "theme_selector_modal__title": "시스템 테마 따르기 옵션", + "theme_selector_modal__title_tip": "참고: 라이트 테마와 다크 테마를 미리 설정해 두면 시스템의 라이트/다크 모드에 따라 자동으로 미리 설정한 테마로 전환됩니다.", + "toggle_source_failed": "소스 변경에 실패했습니다. 검색 페이지에서 다른 서비스를 지정하여 직접 곡을 검색해 보세요.", + "toggle_source_try": "다른 소스로 전환을 시도하는 중...", + "update__downgrade_tip": "버전을 다운그레이드({ver})하신 것을 확인했습니다. 새 버전을 사용하다가 문제가 발생하면 먼저 FAQ를 확인해 주세요. FAQ에 문서화되지 않았거나 해결되지 않는 문제는 문서에 안내된 피드백 채널을 통해 알려주세요😘!\n\n참고: 새 버전에서 이전 버전으로 다운그레이드할 때는 먼저 재생목록을 백업하는 것을 권장합니다. 문제가 발생하면 데이터를 정리하여 해결할 수 있습니다. 데이터 폴더 경로는 문서에서 확인할 수 있습니다.", + "update__error_top": "새 버전 다운로드에 실패했습니다. 다시 다운로드를 시도하거나 GitHub에서 직접 다운로드해 보세요.\n\n새 버전의 주소는 업데이트 팝업창에 포함되어 있습니다. 새 버전을 다운로드하여 바로 설치하세요. 설치에 실패하면 FAQ를 참고하세요.\n\n참고: 현재는 Windows Setup 버전만 자동 업데이트가 가능합니다. Linux의 AppImage 및 deb 버전도 동작하는 것으로 보이나 테스트되지 않았습니다. 다른 버전으로 설치한 사용자는 수동으로 다운로드하여 업데이트해야 합니다.", + "update__ignore_cancel": "업데이트하지 않겠습니다 🤨", + "update__ignore_confirm": "네, 업데이트할게요 ❤️", + "update__ignore_confirm_tip": "현재는 Windows Setup 버전만 자동 업데이트가 가능합니다. Linux의 AppImage 및 deb 버전도 동작하는 것으로 보이나 테스트되지 않았습니다. 다른 버전으로 설치한 사용자는 수동으로 다운로드하여 업데이트해야 합니다.\n\n새 버전의 주소는 업데이트 팝업창에 포함되어 있습니다. 새 버전을 다운로드하여 바로 설치하세요. 설치에 실패하면 FAQ를 참고하세요.", + "update__ignore_confirm_tip_confirm": "네, 이해했습니다", + "update__ignore_tip": "현재 사용 중인 버전이 최신 버전보다 {num}개 버전 뒤처져 있습니다🤪. 더 나은 사용 경험을 위해 최신 버전으로 업데이트하는 것을 권장합니다~!\n참고: 새 버전을 사용하다가 문제가 발생하면 먼저 FAQ를 확인해 주세요. FAQ에 문서화되지 않았거나 해결되지 않는 문제는 문서에 안내된 피드백 채널을 통해 알려주세요😘!", + "update__timeout_top": "다운로드 시간 초과 안내\n\n현재 GitHub 접속 속도가 느려 새 버전을 한 시간째 다운로드하고 있지만 아직 완료되지 않았습니다😳. 계속 기다릴 수도 있지만, 수동으로 업데이트하는 것을 강력히 권장합니다!", + "user_api__allow_show_update_alert": "업데이트 팝업 표시 허용", + "user_api__btn_export": "내보내기", + "user_api__btn_import": "로컬 파일에서 가져오기", + "user_api__btn_import_online": "네트워크에서 가져오기", + "user_api__btn_remove": "삭제", + "user_api__import_file": "음악 API 스크립트 파일을 선택하세요", + "user_api__init_failed_alert": "음악 API \"{name}\" 초기화 실패: ", + "user_api__max_tip": "동시에 최대 20개의 API만 사용할 수 있습니다🤪.\n\n계속 가져오려면 불필요한 API를 삭제하여 공간을 확보해 주세요.", + "user_api__noitem": "아무것도 없습니다...😲", + "user_api__note": "안내: API 스크립트의 실행 환경을 최대한 격리했지만, 악의적인 동작을 포함한 API 스크립트를 가져오면 시스템에 영향을 줄 수 있습니다. 신중하게 가져와 주세요.", + "user_api__readme": "API 작성 안내: ", + "user_api__title": "음악 API 관리", + "user_api__update_alert": "음악 API \"{name}\"의 새 버전을 찾았습니다: ", + "user_api__update_alert_open_url": "업데이트 주소 열기", + "user_api_import__failed": "음악 API 가져오기에 실패했습니다:\n\n{message}", + "user_api_import_online__input_confirm": "가져오기", + "user_api_import_online__input_loading": "가져오는 중...", + "user_api_import_online__input_tip": "HTTP 링크를 입력하세요", + "user_api_import_online__title": "네트워크에서 음악 API 가져오기" +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/lang/languages.json b/plugins/lx-music-ztools/vendor/lx/src/lang/languages.json new file mode 100644 index 000000000..bb4c113ef --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/lang/languages.json @@ -0,0 +1,25 @@ +[ + { + "name": "English", + "locale": "en-us", + "country": "us" + }, + { + "name": "한국어", + "locale": "ko-kr", + "country": "kr" + }, + { + "name": "简体中文", + "locale": "zh-cn", + "alternate": "zh-hans", + "country": "cn", + "fallback": true + }, + { + "name": "繁體中文", + "locale": "zh-tw", + "alternate": "zh-hk", + "country": "cn" + } +] diff --git a/plugins/lx-music-ztools/vendor/lx/src/lang/tsconfig.json b/plugins/lx-music-ztools/vendor/lx/src/lang/tsconfig.json new file mode 100644 index 000000000..cc53bd81f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/lang/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + // "typeRoots": [ + // "./types" + // ], + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/lang/zh-cn.json b/plugins/lx-music-ztools/vendor/lx/src/lang/zh-cn.json new file mode 100644 index 000000000..cc98c60ca --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/lang/zh-cn.json @@ -0,0 +1,748 @@ +{ + "action": "操作", + "agree": "接受", + "alert_button_text": "好吧", + "audio_visualization": "音频可视化(实验性)", + "back": "返回", + "btn_cancel": "取消", + "btn_close": "关闭", + "btn_confirm": "确定", + "btn_save": "保存", + "cancel_button_text": "我不", + "cancel_button_text_2": "不不不,点错了", + "close": "关闭", + "comment__hot_load_error": "热门评论加载失败,点击尝试重新加载", + "comment__hot_loading": "热门评论加载中...", + "comment__hot_title": "热门评论", + "comment__location": "来自{location}", + "comment__new_load_error": "最新评论加载失败,点击尝试重新加载", + "comment__new_loading": "最新评论加载中", + "comment__new_title": "最新评论", + "comment__no_content": "暂无评论", + "comment__refresh": "刷新评论", + "comment__show": "歌曲评论", + "comment__title": "「{name}」的评论", + "comment__unavailable": "此歌曲不支持获取评论", + "confirm_button_text": "是的", + "copy_tip": "(点击复制)", + "date_format_hour": "{num} 小时前", + "date_format_minute": "{num} 分钟前", + "date_format_second": "{num} 秒前", + "deep_link__handle_error_tip": "调用失败:{message}", + "default": "默认", + "default_list": "试听列表", + "desktop_lyric__back": "返回", + "desktop_lyric__close": "关闭", + "desktop_lyric__font_decrease": "减小字体大小", + "desktop_lyric__font_increase": "增加字体大小", + "desktop_lyric__lock": "锁定歌词", + "desktop_lyric__lrc_active_zoom_off": "取消缩放当前播放的歌词", + "desktop_lyric__lrc_active_zoom_on": "缩放当前播放的歌词", + "desktop_lyric__opacity_decrease": "增加透明度(右击可微调)", + "desktop_lyric__opacity_increase": "减小透明度(右击可微调)", + "desktop_lyric__theme": "主题配色", + "desktop_lyric__unlock": "解锁歌词", + "desktop_lyric__win_top_off": "取消置顶歌词界面", + "desktop_lyric__win_top_on": "置顶歌词界面", + "download": "下载", + "download___status_completed": "下载完成", + "download___status_error": "任务出错", + "download___status_paused": "暂停下载", + "download___status_running": "正在下载", + "download___status_waiting": "等待下载", + "download__all": "所有任务", + "download__error": "出错", + "download__finished": "下载完成", + "download__high_quality": "高清音质", + "download__lossless": "无损音质", + "download__multiple_tip": "已选择 {len} 首歌曲", + "download__multiple_tip2": "请选择要优先下载的音质", + "download__normal": "普通音质", + "download__not_available_tip": "该音质不可用", + "download__paused": "已暂停", + "download__progress": "进度", + "download__quality": "品质", + "download__running": "正在下载", + "download__status": "状态", + "download_status_error_check_path": "检查下载路径出错,请检查设置的下载目录是否正常", + "download_status_error_check_path_exist": "存在同名文件,跳过下载", + "download_status_error_refresh_url": "链接失效,正在刷新链接", + "download_status_error_response": "下载失败:", + "download_status_error_url_failed": "获取音乐链接失败", + "download_status_error_write": "歌曲保存位置被占用或没有写入权限,请尝试更改歌曲保存目录或重启软件或重启电脑,错误详情:", + "download_status_start": "开始下载", + "download_status_url_getting": "音乐链接获取中...", + "download_status_write_queue": "数据写入中({num})", + "duplicate_list_tip": "你之前已收藏过列表「{name}」,是否需要更新里面的歌曲?", + "export": "导出", + "fullscreen_exit": "退出全屏", + "history_clear": "清空搜索历史", + "history_remove": "右击移除该历史", + "history_search": "历史搜索", + "import": "导入", + "leaderboard": "排行榜", + "list__add_to": "添加到...", + "list__collect": "收藏", + "list__copy_name": "复制歌曲名", + "list__dislike": "不喜欢", + "list__download": "下载", + "list__export_part_desc": "选择列表文件保存位置", + "list__file": "定位文件", + "list__import_part_button_cancel": "不要啊", + "list__import_part_button_confirm": "覆盖掉", + "list__import_part_confirm": "导入的列表「{importName}」与本地列表「{localName}」的 ID 相同,是否覆盖本地列表?", + "list__import_part_desc": "选择列表文件", + "list__load_failed": "啊,加载失败了😭", + "list__loading": "列表加载中...⏳", + "list__move_to": "移动到...", + "list__movedown": "下移", + "list__moveup": "上移", + "list__name_default": "试听列表", + "list__name_love": "我的收藏", + "list__new_list_btn": "新建列表", + "list__new_list_input": "新列表...", + "list__pause": "暂停任务", + "list__play": "播放", + "list__play_later": "稍后播放", + "list__remove": "移除", + "list__remove_tip": "你真的想要移除「{name}」吗?", + "list__remove_tip_button": "是的,没错", + "list__rename": "重命名", + "list__search": "搜索", + "list__sort": "调整位置", + "list__source_detail": "歌曲详情页", + "list__start": "开始任务", + "list__sync": "更新", + "list__toggle_source": "歌曲换源", + "list_add__btn_title": "把该歌曲添加到「{name}」", + "list_add__multiple_btn_title": "把这些歌曲添加到「{name}」", + "list_add__multiple_title_add": "添加已选的 {num} 首歌曲到...", + "list_add__multiple_title_move": "移动已选的 {num} 首歌曲到...", + "list_add__title_first_add": "添加", + "list_add__title_first_move": "移动", + "list_add__title_last": "到...", + "list_duplicate_tip": "已存在同名列表,是否仍要继续创建?", + "list_import_tip__alldata": "导入失败,这是一个「所有数据」备份文件,你需要去这里导入:\n\n设置 → 备份与恢复 → 所有数据 → 导入", + "list_import_tip__playlist": "导入失败,这是一个「列表」备份文件,你需要去这里导入:\n\n设置 → 备份与恢复 → 部分数据 → 导入列表", + "list_import_tip__playlist_part": "导入失败,这是一个「单列表」文件,你需要去这里导入:\n\n我的列表 → 右击任意一个列表名 → 在弹出的菜单中选择「导入」", + "list_import_tip__setting": "导入失败,这是一个「设置」备份文件,你需要去这里导入:\n\n设置 → 备份与恢复 → 部分数据 → 导入设置", + "list_import_tip__unknown": "导入失败,未知的文件类型,请尝试升级到最新版本后再试", + "list_sort_modal_by_album": "专辑名", + "list_sort_modal_by_down": "降序", + "list_sort_modal_by_field": "排序字段", + "list_sort_modal_by_name": "歌曲名", + "list_sort_modal_by_random": "随机乱序", + "list_sort_modal_by_singer": "艺术家", + "list_sort_modal_by_source": "歌曲来源平台", + "list_sort_modal_by_time": "时长", + "list_sort_modal_by_type": "排序类别", + "list_sort_modal_by_up": "升序", + "list_sort_modal_tip_confirm": "你确定要这么做吗?", + "list_update_modal__auto_update": "自动更新", + "list_update_modal__tips": "💡 每次启动软件时将会自动更新已勾选「自动更新」的列表", + "list_update_modal__title": "列表更新管理", + "list_update_modal__update": "立即更新", + "lists__add_local_file_desc": "选择歌曲文件", + "lists__dislike_music_singer_tip": "你真的不喜欢「{singer}」的「{name}」吗?", + "lists__dislike_music_tip": "你真的不喜欢「{name}」吗?", + "lists__duplicate": "重复歌曲", + "lists__export": "导出", + "lists__export_part_desc": "选择列表文件保存位置", + "lists__import": "导入", + "lists__import_part_button_cancel": "不要啊", + "lists__import_part_button_confirm": "覆盖掉", + "lists__import_part_confirm": "导入的列表「{importName}」与本地列表「{localName}」的 ID 相同,是否覆盖本地列表?", + "lists__import_part_desc": "选择列表文件", + "lists__new_list_btn": "新建列表", + "lists__new_list_input": "新列表...", + "lists__remove": "移除", + "lists__remove_music_tip": "你真的要移除所选的 {len} 首歌曲吗?", + "lists__remove_tip": "你真的想要移除「{name}」吗?", + "lists__remove_tip_button": "是的,没错", + "lists__rename": "重命名", + "lists__select_local_file": "添加本地歌曲", + "lists__sort_list": "排序歌曲", + "lists__source_detail": "歌单详情页", + "lists__sync": "更新", + "lists__sync_confirm_tip": "这将会把「{name}」内的歌曲替换成在线列表的歌曲,你确认要更新吗?", + "load_list_file_error_detail": "我们已经帮你把旧的列表文件备份到:{path}\n\n它以 JSON 格式存储,你可以尝试手动修复并恢复它。\n\n错误详情:{detail}", + "load_list_file_error_title": "播放列表数据加载错误(建议到 GitHub 反馈)", + "loading": "加载中...", + "love_list": "收藏", + "lyric__load_error": "歌词获取失败", + "lyric__select": "歌词文本选择", + "lyric_menu__align": "歌词对齐方式", + "lyric_menu__align_center": "居中", + "lyric_menu__align_left": "居左", + "lyric_menu__lrc_size": "字体大小 [ {size} ]", + "lyric_menu__offset": "歌词偏移 [ {offset}ms ]", + "lyric_menu__offset_add_10": "加快 10 毫秒", + "lyric_menu__offset_add_100": "加快 100 毫秒", + "lyric_menu__offset_dec_10": "减慢 10 毫秒", + "lyric_menu__offset_dec_100": "减慢 100 毫秒", + "lyric_menu__offset_reset": "重置", + "lyric_menu__size_add": "加大字体(右击可微调)", + "lyric_menu__size_dec": "减小字体(右击可微调)", + "lyric_menu__size_reset": "重置", + "media_device__empty_device_tip": "音频输出设备为空,若出现无法播放的情况,请检查声卡驱动是否已安装。或卸载并重新安装软件,安装时选「仅为我安装」,并保持默认安装路径。", + "min": "最小化", + "music_album": "专辑名", + "music_duplicate": "重复歌曲", + "music_name": "歌曲名", + "music_singer": "艺术家", + "music_sort__input_tip": "请输入要调整到第几个位置", + "music_sort__title": "将「{name}」的位置调整到:", + "music_sort__title_multiple": "将已选的 {num} 首歌曲的位置调整到:", + "music_time": "时长", + "music_toggle_clean": "取消换源", + "music_toggle_confirm": "确认", + "music_toggle_duplicate_tip": "列表中已存在相同的歌曲,是否将其移除并继续?", + "my_list": "我的列表", + "no_item": "列表竟然是空的...", + "not_agree": "不接受", + "ok": "我知道了", + "pagination__next": "下一页", + "pagination__page": "第 {num} 页", + "pagination__prev": "上一页", + "play_timeout": "定时暂停", + "play_timeout_close": "关闭", + "play_timeout_confirm": "确认", + "play_timeout_end": "等待歌曲播放完毕再暂停", + "play_timeout_stop": "取消定时", + "play_timeout_tip": "{time} 后暂停播放", + "play_timeout_unit": "分钟", + "play_timeout_update": "更新定时", + "player__add_music_to": "添加当前歌曲到...", + "player__buffering": "缓冲中...", + "player__desktop_lyric_lock": "右击锁定歌词", + "player__desktop_lyric_off": "关闭桌面歌词", + "player__desktop_lyric_on": "开启桌面歌词", + "player__desktop_lyric_unlock": "右击解锁歌词", + "player__end": "播放完毕", + "player__error": "音频加载出错,5 秒后切换下一首", + "player__getting_url": "歌曲链接获取中...", + "player__getting_url_delay_retry": "服务繁忙,{time} 秒后重试...", + "player__hide_detail_tip": "隐藏详情页(界面内右键双击可快速隐藏详情页)", + "player__loading": "音乐加载中...", + "player__music_album": "专辑名:", + "player__music_name": "歌曲名:", + "player__music_singer": "艺术家:", + "player__next": "下一首", + "player__pause": "暂停", + "player__pic_tip": "播放详情页(右击在「我的列表」定位当前播放的歌曲)", + "player__play": "播放", + "player__play_toggle_mode_list": "顺序播放", + "player__play_toggle_mode_list_loop": "列表循环播放", + "player__play_toggle_mode_off": "禁用歌曲切换", + "player__play_toggle_mode_random": "列表随机播放", + "player__play_toggle_mode_single_loop": "单曲循环播放", + "player__playback_preserves_pitch": "音调补偿", + "player__playback_rate": "当前播放速率:", + "player__playback_rate_reset_btn": "重置", + "player__playing": "播放中...", + "player__prev": "上一首", + "player__refresh_url": "URL 过期,正在刷新 URL...", + "player__sound_effect": "音效设置(实验性)", + "player__sound_effect_biquad_filter": "均衡器", + "player__sound_effect_biquad_filter_preset_classical": "古典", + "player__sound_effect_biquad_filter_preset_dance": "舞曲", + "player__sound_effect_biquad_filter_preset_electronic": "电子乐", + "player__sound_effect_biquad_filter_preset_pop": "流行", + "player__sound_effect_biquad_filter_preset_rock": "摇滚", + "player__sound_effect_biquad_filter_preset_slow": "慢歌", + "player__sound_effect_biquad_filter_preset_soft": "柔和", + "player__sound_effect_biquad_filter_preset_subwoofer": "重低音", + "player__sound_effect_biquad_filter_preset_vocal": "人声", + "player__sound_effect_biquad_filter_reset_btn": "重置", + "player__sound_effect_biquad_filter_save_btn": "另存预设", + "player__sound_effect_biquad_filter_save_input": "新预设...", + "player__sound_effect_convolution": "环境混响音效", + "player__sound_effect_convolution_file_bright_hall": "大厅", + "player__sound_effect_convolution_file_cardiod_35_10_spread": "心形扩散", + "player__sound_effect_convolution_file_cinema_diningroom": "电影院", + "player__sound_effect_convolution_file_dining_living_true_stereo": "餐厅", + "player__sound_effect_convolution_file_feedback_spring": "反馈弹簧", + "player__sound_effect_convolution_file_living_bedroom_leveled": "卫生间", + "player__sound_effect_convolution_file_matrix_1": "矩阵混响(1)", + "player__sound_effect_convolution_file_matrix_2": "矩阵混响(2)", + "player__sound_effect_convolution_file_s2_r4_bd": "教堂", + "player__sound_effect_convolution_file_s3_r1_bd": "立体声", + "player__sound_effect_convolution_file_spreader50_65ms": "室内", + "player__sound_effect_convolution_file_telephone": "电话", + "player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "磁性立体声", + "player__sound_effect_convolution_main_gain": "原始音频增益", + "player__sound_effect_convolution_send_gain": "环境音效增益", + "player__sound_effect_features_tip": "提示:「音效设置」与「自定义音频输出设备」冲突,启用音效设置后音频输出设备将会被重置为默认的输出设备,目前此问题暂无法解决。", + "player__sound_effect_panner": "3D 立体环绕(需使用耳机)", + "player__sound_effect_panner_enabled": "启用", + "player__sound_effect_panner_sound_r": "声音距离", + "player__sound_effect_panner_sound_speed": "环绕速度", + "player__sound_effect_pitch_shifter": "音调升降调节", + "player__sound_effect_pitch_shifter_preset_semitones": "{num} 半音", + "player__sound_effect_pitch_shifter_reset_btn": "重置", + "player__sound_effect_pitch_shifter_tip": "由于升降调需要实时处理音频数据,这会导致额外的 CPU 占用。\n\n已知问题:\n如果 CPU 资源不够将导致处理任务堆积而出现声音异常。\n这时需要暂停播放一段时间等堆积的任务处理完毕再播放。", + "player__stop": "暂停播放", + "player__volume": "当前音量:", + "player__volume_mute_label": "静音", + "player__volume_muted": "已静音", + "search": "搜索", + "search__hot_search": "热门搜索", + "search__type_music": "歌曲", + "search__type_songlist": "歌单", + "search__welcome": "搜我所想~~😉", + "setting": "设置", + "setting__about": "关于 LX Music", + "setting__backup": "备份与恢复", + "setting__backup_all": "所有数据(列表数据与设置数据)", + "setting__backup_all_export": "导出", + "setting__backup_all_export_desc": "选择备份保存位置", + "setting__backup_all_import": "导入", + "setting__backup_all_import_desc": "选择备份文件", + "setting__backup_other": "其他备份格式(目前不支持恢复此类备份文件)", + "setting__backup_other_export_dir": "选择文件保存位置", + "setting__backup_other_export_list_csv": "导出 CSV 格式列表", + "setting__backup_other_export_list_text": "导出 TXT 格式列表", + "setting__backup_other_export_list_text_confirm": "是否将所有列表合并为一个文件?", + "setting__backup_part": "部分数据(列表数据包括「试听列表」、「我的收藏」和用户自行创建的列表;设置数据不包括快捷键设置)", + "setting__backup_part_export_list": "导出列表", + "setting__backup_part_export_list_desc": "选择歌单保存位置", + "setting__backup_part_export_setting": "导出设置", + "setting__backup_part_export_setting_desc": "选择设置保存位置", + "setting__backup_part_import_list": "导入列表", + "setting__backup_part_import_list_confirm": "备份文件中列表与现有列表 ID 相同时,现有列表内歌曲将会被覆盖,是否要继续?", + "setting__backup_part_import_list_desc": "选择列表文件", + "setting__backup_part_import_setting": "导入设置", + "setting__backup_part_import_setting_desc": "选择配置文件", + "setting__basic": "基本设置", + "setting__basic_animation": "弹出层随机动画", + "setting__basic_control_btn_position": "控制按钮位置", + "setting__basic_control_btn_position_left": "左边", + "setting__basic_control_btn_position_right": "右边", + "setting__basic_font": "字体", + "setting__basic_font_size": "字体大小", + "setting__basic_font_size_14px": "更小", + "setting__basic_font_size_15px": "小", + "setting__basic_font_size_16px": "标准", + "setting__basic_font_size_17px": "大", + "setting__basic_font_size_18px": "更大", + "setting__basic_font_size_19px": "非常大", + "setting__basic_lang": "语言", + "setting__basic_lang_title": "软件显示的语言", + "setting__basic_playbar_progress_style": "播放栏进度条样式", + "setting__basic_playbar_progress_style_full": "全宽", + "setting__basic_playbar_progress_style_middle": "中等", + "setting__basic_playbar_progress_style_mini": "迷你", + "setting__basic_show_animation": "显示动画效果", + "setting__basic_source": "自定义源", + "setting__basic_source_status_failed": "初始化失败", + "setting__basic_source_status_initing": "初始化中...", + "setting__basic_source_status_success": "初始化成功", + "setting__basic_source_temp": "临时接口(软件的某些功能不可用,建议测试接口不可用再使用本接口)", + "setting__basic_source_test": "测试接口(几乎软件的所有功能都可用)", + "setting__basic_source_user_api_btn": "自定义源管理", + "setting__basic_sourcename": "歌曲来源名称", + "setting__basic_sourcename_alias": "别名", + "setting__basic_sourcename_real": "原名", + "setting__basic_sourcename_title": "选择歌曲来源名称类型", + "setting__basic_start_in_fullscreen": "以全屏模式启动", + "setting__basic_theme": "主题颜色", + "setting__basic_theme_auto_tip": "此乃动态主题,你可以预先设置一个亮色主题及暗色主题,此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。\n\n注:鼠标右击此主题项即可打开亮、暗色主题设置窗口。", + "setting__basic_to_tray": "关闭窗口时不退出软件将其最小化到系统托盘", + "setting__basic_window_size": "窗口尺寸", + "setting__basic_window_size_big": "大", + "setting__basic_window_size_huge": "巨大", + "setting__basic_window_size_larger": "更大", + "setting__basic_window_size_medium": "中", + "setting__basic_window_size_oversized": "超大", + "setting__basic_window_size_small": "小", + "setting__basic_window_size_smaller": "更小", + "setting__basic_window_size_title": "设置软件窗口尺寸", + "setting__click_copy": "点击复制", + "setting__click_open": "点击打开", + "setting__desktop_lyric": "桌面歌词设置", + "setting__desktop_lyric_align": "歌词对齐方式", + "setting__desktop_lyric_align_center": "居中", + "setting__desktop_lyric_align_left": "居左", + "setting__desktop_lyric_align_right": "居右", + "setting__desktop_lyric_always_on_top": "使歌词总是在其他窗口之上", + "setting__desktop_lyric_always_on_top_loop": "自动刷新歌词置顶", + "setting__desktop_lyric_always_on_top_loop_tip": "当歌词置顶后仍被某些程序遮挡时可尝试启用此选项", + "setting__desktop_lyric_audio_visualization": "音频可视化(实验性)", + "setting__desktop_lyric_color": "歌词字体颜色", + "setting__desktop_lyric_color_reset": "重置颜色", + "setting__desktop_lyric_delay_scroll": "延迟歌词滚动", + "setting__desktop_lyric_direction": "歌词显示方向", + "setting__desktop_lyric_direction_horizontal": "水平方向", + "setting__desktop_lyric_direction_vertical": "垂直方向", + "setting__desktop_lyric_ellipsis": "不允许歌词换行", + "setting__desktop_lyric_enable": "显示歌词", + "setting__desktop_lyric_font": "歌词字体", + "setting__desktop_lyric_font_default": "默认", + "setting__desktop_lyric_font_weight": "加粗字体", + "setting__desktop_lyric_font_weight_extended": "翻译、罗马音歌词", + "setting__desktop_lyric_font_weight_font": "逐字歌词", + "setting__desktop_lyric_font_weight_line": "逐行歌词", + "setting__desktop_lyric_fullscreen_hide": "全屏时自动关闭歌词", + "setting__desktop_lyric_hover_hide": "鼠标移入歌词区域时提高歌词透明度", + "setting__desktop_lyric_hover_hide_tip": "此功能存在平台兼容性问题,在某些平台上可能会出现预料外的问题", + "setting__desktop_lyric_line_gap": "歌词间距({num})", + "setting__desktop_lyric_line_gap_add": "加大间距", + "setting__desktop_lyric_line_gap_dec": "减小间距", + "setting__desktop_lyric_lock": "锁定歌词", + "setting__desktop_lyric_lock_screen": "不允许歌词窗口拖出主屏幕之外", + "setting__desktop_lyric_pause_hide": "暂停时提高歌词透明度", + "setting__desktop_lyric_played_color": "已播放颜色", + "setting__desktop_lyric_reset": "重置", + "setting__desktop_lyric_reset_window": "重置窗口设置", + "setting__desktop_lyric_scroll_align": "正在播放歌词滚动位置", + "setting__desktop_lyric_scroll_align_center": "中心", + "setting__desktop_lyric_scroll_align_top": "顶部", + "setting__desktop_lyric_shadow_color": "阴影颜色", + "setting__desktop_lyric_show_taskbar": "在任务栏显示歌词进程", + "setting__desktop_lyric_show_taskbar_tip": "此选项作为在录屏软件无法捕获歌词窗口时的变通解决方法", + "setting__desktop_lyric_unplay_color": "未播放颜色", + "setting__dislike_list_input_tip": "歌曲名@艺术家\n歌曲名\n@艺术家", + "setting__dislike_list_save_btn": "保存", + "setting__dislike_list_tips": "1. 每条一行,若歌曲或者艺术家名字中存在「@」符号,需要将其替换成「#」\n2. 指定某艺术家的某首歌:歌曲名@艺术家\n3. 指定某首歌:歌曲名\n4. 指定某艺术家:@艺术家", + "setting__dislike_list_title": "「不喜欢的歌曲」规则列表", + "setting__download": "下载设置", + "setting__download_data_embed": "嵌入到音频文件中的内容", + "setting__download_embed_lxlyric": "同时嵌入 LX Music 歌词(如果有,使用 LX Music 播放时可支持逐字歌词)", + "setting__download_embed_lyric": "嵌入歌词", + "setting__download_embed_pic": "嵌入封面", + "setting__download_embed_rlyric": "同时嵌入罗马音歌词(如果有)", + "setting__download_embed_tlyric": "同时嵌入翻译歌词(如果有)", + "setting__download_enable": "启用下载功能", + "setting__download_lxlyric": "同时将 LX Music 歌词写入歌词文件中(如果有,使用 LX Music 播放时可支持逐字歌词)", + "setting__download_lyric": "歌词下载", + "setting__download_lyric_format": "下载的歌词文件编码格式", + "setting__download_lyric_format_gbk": "GBK", + "setting__download_lyric_format_tip": "在某些设备上出现中文乱码时可尝试选择 GBK 格式", + "setting__download_lyric_format_utf8": "UTF-8", + "setting__download_lyric_title": "同时下载歌词文件", + "setting__download_max_num": "同时下载任务数", + "setting__download_max_num_tip": "过大的同时下载数量可能会导致你的 IP 被自定义源封禁,是否确认修改?", + "setting__download_max_num_tooltip": "设置过大可能会导致 IP 被封,这取决于自定义源", + "setting__download_name": "文件命名方式", + "setting__download_name1": "歌曲名 - 艺术家", + "setting__download_name2": "艺术家 - 歌曲名", + "setting__download_name3": "歌曲名", + "setting__download_name_title": "下载歌曲时的命名方式", + "setting__download_path": "下载路径", + "setting__download_path_change_btn": "更改", + "setting__download_path_label": "当前下载路径:", + "setting__download_path_open_label": "点击打开当前路径", + "setting__download_path_title": "下载歌曲保存的路径", + "setting__download_rlyric": "同时将罗马音歌词写入歌词文件中(如果有)", + "setting__download_select_save_path": "选择歌曲保存路径", + "setting__download_skip_exist_file": "下载目录存在同名文件时跳过下载此任务", + "setting__download_tlyric": "同时将翻译歌词写入歌词文件中(如果有)", + "setting__download_use_other_source": "自动换源下载", + "setting__download_use_other_source_tip": "当无法从歌曲的原始来源下载时,尝试切换到其他来源下载。\n注:此功能不 100% 保证换源后的歌曲版本与原版一致。", + "setting__hot_key": "快捷键设置", + "setting__hot_key_common_focus_search_input": "聚焦搜索框", + "setting__hot_key_common_min": "最小化程序", + "setting__hot_key_common_toggle_close": "退出程序", + "setting__hot_key_common_toggle_hide": "显示/隐藏程序", + "setting__hot_key_common_toggle_min": "最小化/还原程序", + "setting__hot_key_desktop_lyric_toggle_always_top": "桌面歌词置顶切换", + "setting__hot_key_desktop_lyric_toggle_lock": "桌面歌词锁定切换", + "setting__hot_key_desktop_lyric_toggle_visible": "开/关桌面歌词", + "setting__hot_key_global_title": "全局快捷键", + "setting__hot_key_local_title": "软件内快捷键", + "setting__hot_key_player_music_dislike": "不喜欢该歌曲", + "setting__hot_key_player_music_love": "收藏歌曲", + "setting__hot_key_player_music_unlove": "取消收藏", + "setting__hot_key_player_next": "下一首歌曲", + "setting__hot_key_player_prev": "上一首歌曲", + "setting__hot_key_player_seekbackward": "快退 5s", + "setting__hot_key_player_seekforward": "快进 5s", + "setting__hot_key_player_toggle_play": "播放/暂停控制", + "setting__hot_key_player_volume_down": "减少音量", + "setting__hot_key_player_volume_mute": "静音切换", + "setting__hot_key_player_volume_up": "增加音量", + "setting__hot_key_tip_input": "请输入新的按键", + "setting__hot_key_unset_input": "未设置", + "setting__is_enable": "启用", + "setting__is_show": "显示", + "setting__list": "列表设置", + "setting__list_action_btn": "显示列表操作按钮", + "setting__list_add_music_location_type": "添加歌曲到列表时的位置", + "setting__list_add_music_location_type_bottom": "底部", + "setting__list_add_music_location_type_top": "顶部", + "setting__list_click_action": "双击列表里的歌曲时自动切换到当前列表播放(仅对「歌单」和「排行榜」有效)", + "setting__list_scroll": "记住播放列表滚动条位置(仅对「我的列表」有效)", + "setting__list_source": "显示歌曲来源平台(仅对「我的列表」有效)", + "setting__network": "网络设置", + "setting__network_proxy_host": "主机", + "setting__network_proxy_password": "密码", + "setting__network_proxy_port": "端口", + "setting__network_proxy_title": "HTTP 代理设置(乱设置软件将无法联网)", + "setting__network_proxy_username": "用户名", + "setting__odc": "强迫症设置", + "setting__odc_clear_search_input": "离开搜索界面时清空搜索框", + "setting__odc_clear_search_list": "离开搜索界面时清空搜索列表", + "setting__open_api": "开放 API", + "setting__open_api_address": "服务地址:", + "setting__open_api_bind_lan": "允许来自局域网的访问", + "setting__open_api_enable": "启用开放 API 服务", + "setting__open_api_port": "服务端口", + "setting__open_api_port_tip": "请输入开放 API 服务端口", + "setting__open_api_tip": "该功能用于为第三方软件提供调用 LX Music 的能力,目前可用的功能可以看:", + "setting__open_api_tip_link": "接入文档", + "setting__other": "其他", + "setting__other_dislike_list": "「不喜欢的歌曲」规则", + "setting__other_dislike_list_label": "规则数量:", + "setting__other_dislike_list_show_btn": "编辑规则", + "setting__other_listdata": "列表数据清理", + "setting__other_listdata_clear_btn": "清空「我的列表」数据", + "setting__other_listdata_clear_tip_confirm": "这将清理你创建的「所有列表」及收藏的「所有歌曲」,是否真的要继续?", + "setting__other_lyric_edited_cache": "已调整过偏移时间的歌词管理", + "setting__other_lyric_edited_clear_btn": "清理已调整过时间的歌词", + "setting__other_lyric_edited_clear_tip_confirm": "这将清理所有你之前已调整过偏移时间的歌词,是否确认清理?(手抖确认🤪)", + "setting__other_lyric_edited_label": "歌词数量:", + "setting__other_lyric_raw_clear_btn": "清理歌词缓存", + "setting__other_lyric_raw_label": "歌词数量:", + "setting__other_music_url_clear_btn": "清理歌曲 URL 缓存", + "setting__other_music_url_label": "歌曲 URL 数量:", + "setting__other_other_cache": "其他缓存管理", + "setting__other_other_source_clear_btn": "清理换源歌曲缓存", + "setting__other_other_source_label": "换源歌曲信息数量:", + "setting__other_resource_cache": "资源缓存管理", + "setting__other_resource_cache_clear_btn": "清理资源缓存", + "setting__other_resource_cache_confirm": "我要清掉", + "setting__other_resource_cache_label": "软件已使用缓存大小:", + "setting__other_resource_cache_tip": "包括图片、音频等缓存,清理后这些资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小", + "setting__other_resource_cache_tip_confirm": "涉及图片、音频等缓存,清理后这些资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小,是否仍要清理?", + "setting__other_transparent_window": "主窗口使用软件内置的圆角及阴影", + "setting__other_transparent_window_tip": "关闭后将使用系统原生窗口样式。更改后重启软件生效。", + "setting__other_tray_theme": "托盘图标样式", + "setting__other_tray_theme_auto": "跟随系统亮暗模式", + "setting__other_tray_theme_black": "黑色", + "setting__other_tray_theme_native": "白色", + "setting__other_tray_theme_origin": "原色", + "setting__play": "播放设置", + "setting__play_advanced_audio_features_tip": "自定义音频输出设备与该功能冲突,启用该功能后音频输出设备将会被重置为默认,目前此问题暂无法解决。是否仍要开启?", + "setting__play_auto_clean_played_list": "点击与播放列表相同的列表切歌时清空已播放列表", + "setting__play_auto_clean_played_list_tip": "随机模式下列表内所有歌曲会重新参与随机", + "setting__play_auto_skip_on_error": "播放错误时自动切换歌曲", + "setting__play_detail": "播放详情页设置", + "setting__play_detail_align": "歌词对齐方式", + "setting__play_detail_align_center": "居中", + "setting__play_detail_align_left": "居左", + "setting__play_detail_align_right": "居右", + "setting__play_detail_font_size": "歌词字体大小(可以在播放详情页使用键盘的「+」「-」调整字体大小)", + "setting__play_detail_font_size_current": "当前字体大小:{size}", + "setting__play_detail_font_size_reset": "重置", + "setting__play_detail_font_zoom": "缩放当前正在播放的歌词", + "setting__play_detail_lyric_delay_scroll": "延迟歌词滚动", + "setting__play_detail_lyric_progress": "允许通过拖拽歌词调整播放进度", + "setting__play_lyric_lxlrc": "使用卡拉 OK 式歌词播放(如果可用)", + "setting__play_lyric_lxlrc_tip": "此功能比较耗性能,低配置电脑不建议开启!", + "setting__play_lyric_roma": "显示歌词罗马音(如果可用)", + "setting__play_lyric_s2t": "将播放与下载的中文歌词转换为繁体", + "setting__play_lyric_transition": "显示歌词翻译(如果可用)", + "setting__play_max_output_channel_count": "使用设备能处理的最大声道数输出音频", + "setting__play_mediaDevice": "音频输出", + "setting__play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲", + "setting__play_mediaDevice_title": "选择声音输出的媒体设备", + "setting__play_media_device_error_tip": "此功能与高级音频功能(音频可视化、音效设置、使用设备能处理的最大声道数输出音频功能)冲突。你本次启动软件时已启用这些功能,此选项暂不可用。请关闭这些功能并重启软件后,再修改此选项!", + "setting__play_media_device_tip": "此功能与音频可视化功能冲突,两者无法同时启用。是否关闭「音频可视化」并应用所选音频输出设置?", + "setting__play_playQuality": "优先播放的音质(如果可用)", + "setting__play_power_save_blocker": "播放歌曲时阻止电脑休眠", + "setting__play_save_play_time": "记住播放进度", + "setting__play_startup_auto_play": "启动软件后自动播放音乐", + "setting__play_statusbar_lyric": "在菜单栏的状态菜单显示歌词", + "setting__play_statusbar_lyric_tip": "需要在「设置 → 基本设置」启用「关闭窗口时不退出软件将其最小化到系统托盘」。", + "setting__play_task_bar": "在任务栏上显示当前歌曲播放进度", + "setting__play_timeout": "定时暂停", + "setting__player_audio_visualization_tip": "自定义音频输出设备与音频可视化功能冲突,启用「音频可视化」后音频输出设备将会被重置为默认,目前此问题暂无法解决。是否仍要开启?", + "setting__player_swap_lyric_trans_roma": "调换歌词翻译与歌词罗马音位置", + "setting__search": "搜索设置", + "setting__search_focus_search_box": "启动时自动聚焦搜索框", + "setting__search_history": "显示历史搜索记录", + "setting__search_hot": "显示热门搜索", + "setting__sync": "数据同步", + "setting__sync_client_address": "当前设备地址:{address}", + "setting__sync_client_host": "同步服务地址", + "setting__sync_client_host_tip": "http://:<端口号>", + "setting__sync_client_mode": "客户端模式", + "setting__sync_client_status": "状态:{status}", + "setting__sync_code_blocked_ip": "当前设备的 IP 已被服务端封禁!", + "setting__sync_code_fail": "连接码无效", + "setting__sync_enable": "启用同步功能", + "setting__sync_mode": "同步模式", + "setting__sync_mode_client": "客户端模式", + "setting__sync_mode_server": "服务端模式", + "setting__sync_server_address": "同步服务地址:{address}", + "setting__sync_server_auth_code": "连接码:{code}", + "setting__sync_server_device": "已连接的设备:{devices}", + "setting__sync_server_device_list_btn_remove": "移除", + "setting__sync_server_device_list_noitem": "这里啥也没有 ┗( ▔, ▔ )┛", + "setting__sync_server_device_list_time": "最后连接时间:{time}", + "setting__sync_server_device_list_tips": "💡 设备被移除后,再连接时需要重新输入连接码", + "setting__sync_server_device_list_title": "已认证设备", + "setting__sync_server_mode": "服务端模式(由于数据是明文传输,请在受信任的网络下使用)", + "setting__sync_server_port": "同步端口设置", + "setting__sync_server_port_tip": "请输入同步服务端口号", + "setting__sync_server_refresh_code": "刷新连接码", + "setting__sync_server_show_device_list": "已认证设备列表", + "setting__sync_tip": "使用方式请看常见问题「同步功能」部分", + "setting__update": "软件更新", + "setting__update_checking": "检查更新中...", + "setting__update_commit_date": "提交日期:", + "setting__update_commit_id": "代码版本:", + "setting__update_current_label": "当前版本:", + "setting__update_downloading": "发现新版本并在努力下载中,请稍后...⏳", + "setting__update_init": "处理更新中...", + "setting__update_latest": "软件已是最新,尽情地体验吧~🥂", + "setting__update_latest_label": "最新版本:", + "setting__update_new_version": "发现新版本,赶快去更新吧~🚀🚀", + "setting__update_open_version_modal_btn": "打开更新窗口", + "setting__update_progress": "状态:", + "setting__update_show_change_log": "更新版本后的首次启动时显示更新日志", + "setting__update_try_auto_update": "发现新版本时尝试自动下载更新", + "setting__update_unknown": "未知", + "setting__update_unknown_tip": "❓ 获取最新版本信息失败,建议去「关于」页面打开项目发布地址查看当前版本是否最新", + "setting_download_save_group_list_name": "将文件保存到以对应列表命名的子目录中", + "setting_sync_status_enabled": "已连接", + "song_list": "歌单", + "songlist__import_input_btn_confirm": "打开", + "songlist__import_input_show_btn": "打开歌单", + "songlist__import_input_tip": "输入歌单链接或歌单 ID", + "songlist__import_input_tip_1": "不支持跨源打开歌单,请确认要打开的歌单与当前选择的歌单来源是否对应", + "songlist__import_input_tip_2": "若遇到无法打开的歌单链接,欢迎反馈", + "songlist__import_input_tip_3": "酷狗源歌单不支持用歌单 ID 或概念版链接打开,但支持用普通版链接或酷狗码打开", + "songlist__import_input_tip_4": "网易源「私人雷达」歌单需要 Token 才能打开,企鹅源个人歌单需要将个人主页公开才能打开,请参阅 ", + "songlist__import_input_title": "打开分享的歌单", + "songlist__open_list": "打开「{name}」歌单", + "songlist__tag_info_hot_tag": "热门标签", + "source_alias_all": "聚合大会", + "source_alias_bd": "小杜音乐", + "source_alias_kg": "小枸音乐", + "source_alias_kw": "小蜗音乐", + "source_alias_mg": "小蜜音乐", + "source_alias_tx": "小秋音乐", + "source_alias_wy": "小芸音乐", + "source_alias_xm": "小霞音乐", + "source_all": "聚合搜索", + "source_bd": "百度音乐", + "source_kg": "酷狗音乐", + "source_kw": "酷我音乐", + "source_mg": "咪咕音乐", + "source_tx": "企鹅音乐", + "source_wy": "网易音乐", + "source_xm": "虾米音乐", + "sync__auth_code_input_tip": "请输入连接码", + "sync__auth_code_title": "需要输入连接码", + "sync__dislike_merge_tip_desc": "合并两边列表内容并去重", + "sync__dislike_other_tip_desc": "「取消同步」将不使用「不喜欢的歌曲」列表同步功能", + "sync__dislike_overwrite_tip_desc": "被覆盖者的列表将被替换成覆盖者的列表", + "sync__dislike_title": "选择与「{name}」的「不喜欢的歌曲」列表同步方式", + "sync__list_merge_tip_desc": "将两边的列表合并到一起,相同的歌曲将被去掉(去掉的是被合并者的歌曲),不同的歌曲将被添加。", + "sync__list_other_tip_desc": "「取消同步」将不使用列表同步功能。", + "sync__list_overwrite_tip_desc": "被覆盖者与覆盖者列表 ID 相同的列表将被移除后替换成覆盖者的列表(列表 ID 不同的列表将被合并到一起)。若勾选「完全覆盖」,则被覆盖者的所有列表将被移除,然后替换成覆盖者的列表。", + "sync__list_title": "选择与「{name}」的列表同步方式", + "sync__merge_btn_local_remote": "「本机列表」合并「远程列表」", + "sync__merge_btn_remote_local": "「远程列表」合并「本机列表」", + "sync__merge_label": "合并", + "sync__merge_tip": "合并:", + "sync__other_label": "其他", + "sync__other_tip": "其他:", + "sync__overwrite": "完全覆盖", + "sync__overwrite_btn_cancel": "取消同步", + "sync__overwrite_btn_local_remote": "「本机列表」覆盖「远程列表」", + "sync__overwrite_btn_none": "仅使用实时同步功能", + "sync__overwrite_btn_remote_local": "「远程列表」覆盖「本机列表」", + "sync__overwrite_label": "覆盖", + "sync__overwrite_tip": "覆盖:", + "sync_status_disabled": "未连接", + "tag__high_quality": "HQ", + "tag__lossless": "SQ", + "tag__lossless_24bit": "24bit", + "theme_add": "添加主题", + "theme_auto": "道法自然", + "theme_auto_tip": "鼠标右击可打开亮、暗主题设置窗口", + "theme_black": "黑灯瞎火", + "theme_blue": "蓝田生玉", + "theme_blue2": "清热版蓝", + "theme_blue_plus": "蛋雅深蓝", + "theme_china_ink": "近墨者黑", + "theme_edit_modal__app_bg": "应用背景颜色", + "theme_edit_modal__aside_color": "侧栏按钮颜色", + "theme_edit_modal__badge": "标签颜色", + "theme_edit_modal__badge_primary": "主颜色", + "theme_edit_modal__badge_secondary": "次要颜色", + "theme_edit_modal__badge_tertiary": "第三颜色", + "theme_edit_modal__bg_image": "背景图片", + "theme_edit_modal__bg_image_add": "背景图片:", + "theme_edit_modal__bg_image_change": "更改背景图片", + "theme_edit_modal__bg_image_remove": "移除背景图片", + "theme_edit_modal__close_btn": "关闭", + "theme_edit_modal__control_btn": "左侧控制按钮颜色", + "theme_edit_modal__copy": "复制主题", + "theme_edit_modal__dark": "暗色主题", + "theme_edit_modal__dark_font": "深色字体", + "theme_edit_modal__font": "字体颜色", + "theme_edit_modal__hide_btn": "隐藏播放详情页", + "theme_edit_modal__main_bg": "内容区域背景颜色", + "theme_edit_modal__min_btn": "最小化", + "theme_edit_modal__pick_cancel": "重置", + "theme_edit_modal__pick_color": "选择颜色", + "theme_edit_modal__pick_last_color": "使用之前的颜色", + "theme_edit_modal__pick_save": "确认", + "theme_edit_modal__preview": "预览主题", + "theme_edit_modal__primary": "主题色", + "theme_edit_modal__remove": "移除", + "theme_edit_modal__remove_tip": "是否真的要移除这个主题?", + "theme_edit_modal__save_new": "另存", + "theme_edit_modal__select_bg_file": "选择背景图片", + "theme_edit_modal__title_add": "添加主题", + "theme_edit_modal__title_edit": "编辑主题", + "theme_green": "绿意盎然", + "theme_grey": "灰常美丽", + "theme_happy_new_year": "新年快乐", + "theme_max_tip": "最多只能添加 10 个主题哦,删掉一些再添加吧😜", + "theme_mid_autumn": "月里嫦娥", + "theme_ming": "青出于黑", + "theme_more_btn_show": "更多主题", + "theme_naruto": "木叶之村", + "theme_orange": "橙黄橘绿", + "theme_pink": "粉装玉琢", + "theme_purple": "重斤球紫", + "theme_red": "热情似火", + "theme_selector_modal__dark_title": "暗色主题", + "theme_selector_modal__light_title": "亮色主题", + "theme_selector_modal__theme_name": "主题名称", + "theme_selector_modal__title": "跟随系统主题设置", + "theme_selector_modal__title_tip": "注:你可以预先设置一个亮色主题及暗色主题,此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。", + "toggle_source_failed": "换源失败,请尝试手动在搜索页指定其他来源搜索该歌曲播放", + "toggle_source_try": "尝试切换到其他来源...", + "update__downgrade_tip": "我们发现你降级了版本({ver}),若使用新版本时遇到问题,请先尝试阅读常见问题解决,若你遇到的问题在常见问题中未记录或无法解决,可以通过文档中提到的反馈渠道给我们反馈😘!\n\n注意:从新版本降级旧版时建议先备份歌单,若出现异常则可通过清理数据解决,数据目录路径文档有记录。", + "update__error_top": "自动下载新版本失败,你可以尝试重新下载更新或者手动去下载更新。\n\n新版地址在更新弹窗下面有写,下载新版直接覆盖安装即可,若安装失败则看常见问题解决。\n\n注意:目前只有 Windows 安装版可以自动更新(Linux 的 AppImage、deb 版似乎也可以,未测试),其他版本请手动下载更新!", + "update__ignore_cancel": "我就不想更新🤨", + "update__ignore_confirm": "好,去更新看看❤️", + "update__ignore_confirm_tip": "目前只有 Windows 安装版可以自动更新(Linux 的 AppImage、deb 版似乎也可以,未测试),其他版本请手动下载更新,\n新版地址在更新弹窗下面有写,下载新版直接覆盖安装即可,若安装失败看常见问题解决。", + "update__ignore_confirm_tip_confirm": "OK,已了解", + "update__ignore_tip": "你现在使用的版本距离最新版本已经落后了 {num} 个版本🤪,为了更好的使用体验,建议更新到最新版本哦~!\n\n注:若使用新版本时遇到问题,请先尝试阅读常见问题解决,若你遇到的问题在常见问题中未记录或无法解决,可以通过文档中提到的反馈渠道给我们反馈😘!", + "update__timeout_top": "下载时间过长提示\n\n你当前所在网络访问 GitHub 较慢,新版本已经下了一个钟了还没完成😳,你仍可选择继续等,但墙裂建议手动更新版本!", + "user_api__allow_show_update_alert": "允许显示更新弹窗", + "user_api__btn_export": "导出", + "user_api__btn_import": "本地导入", + "user_api__btn_import_online": "在线导入", + "user_api__btn_remove": "移除", + "user_api__import_file": "选择自定义源 API 脚本文件", + "user_api__init_failed_alert": "自定义源「{name}」初始化失败:", + "user_api__max_tip": "最多只能同时存在 20 个源哦🤪\n想要继续导入的话,请先移除一些旧的源腾出位置吧", + "user_api__noitem": "这里竟然是空的😲", + "user_api__note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。", + "user_api__readme": "源编写说明:", + "user_api__title": "自定义源管理", + "user_api__update_alert": "自定义源「{name}」发现新版本:", + "user_api__update_alert_open_url": "打开更新地址", + "user_api_import__failed": "自定义源导入失败:\n{message}", + "user_api_import_online__input_confirm": "导入", + "user_api_import_online__input_loading": "导入中...", + "user_api_import_online__input_tip": "请输入 HTTP 链接", + "user_api_import_online__title": "在线导入自定义源" +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/lang/zh-tw.json b/plugins/lx-music-ztools/vendor/lx/src/lang/zh-tw.json new file mode 100644 index 000000000..7fdbcd9c2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/lang/zh-tw.json @@ -0,0 +1,748 @@ +{ + "action": "操作", + "agree": "接受", + "alert_button_text": "好吧", + "audio_visualization": "音訊視覺化(實驗性)", + "back": "返回", + "btn_cancel": "取消", + "btn_close": "關閉", + "btn_confirm": "確定", + "btn_save": "儲存", + "cancel_button_text": "我不", + "cancel_button_text_2": "不不不,點錯了", + "close": "關閉", + "comment__hot_load_error": "熱門評論載入失敗,點擊嘗試重新載入", + "comment__hot_loading": "熱門評論載入中...", + "comment__hot_title": "熱門評論", + "comment__location": "來自{location}", + "comment__new_load_error": "最新評論載入失敗,點擊嘗試重新載入", + "comment__new_loading": "最新評論載入中", + "comment__new_title": "最新評論", + "comment__no_content": "暫無評論", + "comment__refresh": "重新整理評論", + "comment__show": "歌曲評論", + "comment__title": "「{name}」的評論", + "comment__unavailable": "此歌曲不支援獲取評論", + "confirm_button_text": "是的", + "copy_tip": "(點擊複製)", + "date_format_hour": "{num} 小時前", + "date_format_minute": "{num} 分鐘前", + "date_format_second": "{num} 秒前", + "deep_link__handle_error_tip": "呼叫失敗:{message}", + "default": "預設", + "default_list": "試聽清單", + "desktop_lyric__back": "返回", + "desktop_lyric__close": "關閉", + "desktop_lyric__font_decrease": "減小字體大小", + "desktop_lyric__font_increase": "增加字體大小", + "desktop_lyric__lock": "鎖定歌詞視窗", + "desktop_lyric__lrc_active_zoom_off": "取消縮放目前播放的歌詞", + "desktop_lyric__lrc_active_zoom_on": "縮放目前播放的歌詞", + "desktop_lyric__opacity_decrease": "增加透明度(右擊可微調)", + "desktop_lyric__opacity_increase": "減小透明度(右擊可微調)", + "desktop_lyric__theme": "主題配色", + "desktop_lyric__unlock": "解鎖歌詞視窗", + "desktop_lyric__win_top_off": "取消置頂歌詞視窗", + "desktop_lyric__win_top_on": "置頂歌詞視窗", + "download": "下載", + "download___status_completed": "下載完成", + "download___status_error": "任務出錯", + "download___status_paused": "暫停下載", + "download___status_running": "正在下載", + "download___status_waiting": "等待下載", + "download__all": "所有任務", + "download__error": "出錯", + "download__finished": "下載完成", + "download__high_quality": "高音質", + "download__lossless": "無損音質", + "download__multiple_tip": "已選取 {len} 首歌曲", + "download__multiple_tip2": "請選取要優先下載的音質", + "download__normal": "一般音質", + "download__not_available_tip": "該音質不可用", + "download__paused": "已暫停", + "download__progress": "進度", + "download__quality": "品質", + "download__running": "正在下載", + "download__status": "狀態", + "download_status_error_check_path": "檢查下載路徑出錯,請檢查設定的下載目錄是否正常", + "download_status_error_check_path_exist": "存在同名檔案,跳過下載", + "download_status_error_refresh_url": "連結失效,正在重新整理連結", + "download_status_error_response": "下載失敗:", + "download_status_error_url_failed": "獲取音樂連結失敗", + "download_status_error_write": "歌曲儲存位置被占用或沒有寫入權限,請嘗試變更歌曲儲存目錄或重啟軟體或重啟電腦,錯誤詳情:", + "download_status_start": "開始下載", + "download_status_url_getting": "音樂連結獲取中...", + "download_status_write_queue": "資料寫入中({num})", + "duplicate_list_tip": "你之前已收藏過清單「{name}」,是否需要更新裡面的歌曲?", + "export": "匯出", + "fullscreen_exit": "退出全螢幕", + "history_clear": "清空搜尋歷史", + "history_remove": "右擊移除該歷史", + "history_search": "歷史搜尋", + "import": "匯入", + "leaderboard": "排行榜", + "list__add_to": "加入到...", + "list__collect": "收藏", + "list__copy_name": "複製歌曲名", + "list__dislike": "不喜歡", + "list__download": "下載", + "list__export_part_desc": "選取清單檔案儲存位置", + "list__file": "定位檔案", + "list__import_part_button_cancel": "不要啊", + "list__import_part_button_confirm": "覆寫掉", + "list__import_part_confirm": "匯入的清單「{importName}」與本機清單「{localName}」的 ID 相同,是否覆寫本機清單?", + "list__import_part_desc": "選取清單檔案", + "list__load_failed": "啊,載入失敗了😭", + "list__loading": "清單載入中...⏳", + "list__move_to": "移動到...", + "list__movedown": "下移", + "list__moveup": "上移", + "list__name_default": "試聽清單", + "list__name_love": "我的最愛", + "list__new_list_btn": "建立清單", + "list__new_list_input": "新清單...", + "list__pause": "暫停任務", + "list__play": "播放", + "list__play_later": "稍後播放", + "list__remove": "移除", + "list__remove_tip": "你真的想要移除「{name}」嗎?", + "list__remove_tip_button": "是的,沒錯", + "list__rename": "重新命名", + "list__search": "搜尋", + "list__sort": "調整位置", + "list__source_detail": "歌曲詳情頁", + "list__start": "開始任務", + "list__sync": "更新", + "list__toggle_source": "變更來源", + "list_add__btn_title": "把該歌曲加入到「{name}」", + "list_add__multiple_btn_title": "把這些歌曲加入到「{name}」", + "list_add__multiple_title_add": "將已選的 {num} 首歌曲加入到...", + "list_add__multiple_title_move": "將已選的 {num} 首歌曲移動到...", + "list_add__title_first_add": "添加", + "list_add__title_first_move": "移動", + "list_add__title_last": "到...", + "list_duplicate_tip": "已存在同名清單,是否仍要繼續建立?", + "list_import_tip__alldata": "匯入失敗,這是一個「所有資料」備份檔案,你需要在這裡匯入:\n\n設定 → 備份與復原 → 所有資料 → 匯入", + "list_import_tip__playlist": "匯入失敗,這是一個「清單」備份檔案,你需要在這裡匯入:\n\n設定 → 備份與復原 → 部分資料 → 匯入清單", + "list_import_tip__playlist_part": "匯入失敗,這是一個「僅清單」檔案,你需要在這裡匯入:\n\n我的清單 → 右擊任意一個清單名 → 在彈出的選單中選取「匯入」", + "list_import_tip__setting": "匯入失敗,這是一個「設定」備份檔案,你需要在這裡匯入:\n\n設定 → 備份與復原 → 部分資料 → 匯入設定", + "list_import_tip__unknown": "匯入失敗,未知的檔案類型,請嘗試升級到最新版本後再試", + "list_sort_modal_by_album": "專輯", + "list_sort_modal_by_down": "降序", + "list_sort_modal_by_field": "排序欄位", + "list_sort_modal_by_name": "標題", + "list_sort_modal_by_random": "隨機亂序", + "list_sort_modal_by_singer": "演出者", + "list_sort_modal_by_source": "音樂串流平台", + "list_sort_modal_by_time": "長度", + "list_sort_modal_by_type": "排序類別", + "list_sort_modal_by_up": "升序", + "list_sort_modal_tip_confirm": "你確定要這麼做嗎?", + "list_update_modal__auto_update": "自動更新", + "list_update_modal__tips": "💡 每次啟動軟體時將會自動更新已勾選「自動更新」的清單", + "list_update_modal__title": "清單更新管理", + "list_update_modal__update": "立即更新", + "lists__add_local_file_desc": "選取歌曲檔案", + "lists__dislike_music_singer_tip": "你真的不喜歡「{singer}」的「{name}」嗎?", + "lists__dislike_music_tip": "你真的不喜歡「{name}」嗎?", + "lists__duplicate": "重複歌曲", + "lists__export": "匯出", + "lists__export_part_desc": "選取清單檔案儲存位置", + "lists__import": "匯入", + "lists__import_part_button_cancel": "不要啊", + "lists__import_part_button_confirm": "覆寫掉", + "lists__import_part_confirm": "匯入的清單「{importName}」與本機清單「{localName}」的 ID 相同,是否覆寫本機清單?", + "lists__import_part_desc": "選取清單檔案", + "lists__new_list_btn": "建立清單", + "lists__new_list_input": "新清單...", + "lists__remove": "移除", + "lists__remove_music_tip": "你真的要移除所選的 {len} 首歌曲嗎?", + "lists__remove_tip": "你真的想要移除「{name}」嗎?", + "lists__remove_tip_button": "是的,沒錯", + "lists__rename": "重新命名", + "lists__select_local_file": "添加本機歌曲", + "lists__sort_list": "排序歌曲", + "lists__source_detail": "歌單詳情頁", + "lists__sync": "更新", + "lists__sync_confirm_tip": "這將會把「{name}」內的歌曲取代成線上清單的歌曲,你確認要更新嗎?", + "load_list_file_error_detail": "我們已經幫你把舊的清單檔案備份到:{path}\n\n它以 JSON 格式儲存,你可以嘗試手動修復並復原它。\n\n錯誤詳情:{detail}", + "load_list_file_error_title": "播放清單資料載入錯誤(建議到 GitHub 回報)", + "loading": "載入中...", + "love_list": "收藏", + "lyric__load_error": "歌詞獲取失敗", + "lyric__select": "歌詞文字選取", + "lyric_menu__align": "歌詞對齊方式", + "lyric_menu__align_center": "置中", + "lyric_menu__align_left": "置左", + "lyric_menu__lrc_size": "字體大小 [ {size} ]", + "lyric_menu__offset": "歌詞偏移 [ {offset}ms ]", + "lyric_menu__offset_add_10": "加快 10 毫秒", + "lyric_menu__offset_add_100": "加快 100 毫秒", + "lyric_menu__offset_dec_10": "減慢 10 毫秒", + "lyric_menu__offset_dec_100": "減慢 100 毫秒", + "lyric_menu__offset_reset": "重設", + "lyric_menu__size_add": "加大字體(右擊可微調)", + "lyric_menu__size_dec": "減小字體(右擊可微調)", + "lyric_menu__size_reset": "重設", + "media_device__empty_device_tip": "音頻輸出設備為空,若出現無法播放的情況,請檢查聲卡驅動是否已安裝。\n或卸載並重新安裝軟件,安裝時選「僅為我安裝」,並保持默認安裝路徑。", + "min": "最小化", + "music_album": "專輯", + "music_duplicate": "重複歌曲", + "music_name": "標題", + "music_singer": "演出者", + "music_sort__input_tip": "請輸入要調整到第幾個位置", + "music_sort__title": "將「{name}」的位置調整到:", + "music_sort__title_multiple": "將已選取的 {num} 首歌曲的位置調整到:", + "music_time": "長度", + "music_toggle_clean": "取消變更", + "music_toggle_confirm": "確認", + "music_toggle_duplicate_tip": "列表中已存在相同的歌曲,是否將其移除並繼續?", + "my_list": "我的清單", + "no_item": "清單竟然是空的...", + "not_agree": "不接受", + "ok": "我知道了", + "pagination__next": "下一頁", + "pagination__page": "第 {num} 頁", + "pagination__prev": "上一頁", + "play_timeout": "定時暫停", + "play_timeout_close": "關閉", + "play_timeout_confirm": "確認", + "play_timeout_end": "等待歌曲播放完畢再暫停", + "play_timeout_stop": "取消定時", + "play_timeout_tip": "{time} 後暫停播放", + "play_timeout_unit": "分鐘", + "play_timeout_update": "更新定時", + "player__add_music_to": "將目前歌曲加入到...", + "player__buffering": "緩衝中...", + "player__desktop_lyric_lock": "右擊鎖定歌詞", + "player__desktop_lyric_off": "關閉歌詞視窗", + "player__desktop_lyric_on": "開啟歌詞視窗", + "player__desktop_lyric_unlock": "右擊解鎖歌詞", + "player__end": "播放完畢", + "player__error": "音訊載入出錯,5 秒後切換下一首", + "player__getting_url": "歌曲連結獲取中...", + "player__getting_url_delay_retry": "服務繁忙,{time} 秒後重試...", + "player__hide_detail_tip": "隱藏詳情頁(介面內右鍵雙擊可快速隱藏詳情頁)", + "player__loading": "音樂載入中...", + "player__music_album": "專輯:", + "player__music_name": "標題:", + "player__music_singer": "演出者:", + "player__next": "下一首", + "player__pause": "暫停", + "player__pic_tip": "播放詳情頁(右擊在「我的清單」定位目前播放的歌曲)", + "player__play": "播放", + "player__play_toggle_mode_list": "順序播放", + "player__play_toggle_mode_list_loop": "重複播放清單", + "player__play_toggle_mode_off": "停用歌曲切換", + "player__play_toggle_mode_random": "隨機播放", + "player__play_toggle_mode_single_loop": "重複播放", + "player__playback_preserves_pitch": "音調補償", + "player__playback_rate": "目前播放速率:", + "player__playback_rate_reset_btn": "重設", + "player__playing": "播放中...", + "player__prev": "上一首", + "player__refresh_url": "URL 過期,正在重新整理 URL...", + "player__sound_effect": "音效設定(實驗性)", + "player__sound_effect_biquad_filter": "均衡器", + "player__sound_effect_biquad_filter_preset_classical": "古典", + "player__sound_effect_biquad_filter_preset_dance": "舞曲", + "player__sound_effect_biquad_filter_preset_electronic": "電子樂", + "player__sound_effect_biquad_filter_preset_pop": "流行", + "player__sound_effect_biquad_filter_preset_rock": "搖滾", + "player__sound_effect_biquad_filter_preset_slow": "慢歌", + "player__sound_effect_biquad_filter_preset_soft": "柔和", + "player__sound_effect_biquad_filter_preset_subwoofer": "重低音", + "player__sound_effect_biquad_filter_preset_vocal": "人聲", + "player__sound_effect_biquad_filter_reset_btn": "重設", + "player__sound_effect_biquad_filter_save_btn": "另存", + "player__sound_effect_biquad_filter_save_input": "新預設...", + "player__sound_effect_convolution": "環境混響音效", + "player__sound_effect_convolution_file_bright_hall": "大廳", + "player__sound_effect_convolution_file_cardiod_35_10_spread": "心形擴散", + "player__sound_effect_convolution_file_cinema_diningroom": "電影院", + "player__sound_effect_convolution_file_dining_living_true_stereo": "餐廳", + "player__sound_effect_convolution_file_feedback_spring": "回饋彈簧", + "player__sound_effect_convolution_file_living_bedroom_leveled": "洗手間", + "player__sound_effect_convolution_file_matrix_1": "矩陣混響(1)", + "player__sound_effect_convolution_file_matrix_2": "矩陣混響(2)", + "player__sound_effect_convolution_file_s2_r4_bd": "教堂", + "player__sound_effect_convolution_file_s3_r1_bd": "立體聲", + "player__sound_effect_convolution_file_spreader50_65ms": "室內", + "player__sound_effect_convolution_file_telephone": "電話", + "player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "磁性立體聲", + "player__sound_effect_convolution_main_gain": "原始音訊增益", + "player__sound_effect_convolution_send_gain": "環境音效增益", + "player__sound_effect_features_tip": "提示:「音效設定」與「自訂音訊輸出裝置」衝突,啟用音效設定後音訊輸出裝置將會被重設為預設的輸出裝置,目前此問題暫無法解決。", + "player__sound_effect_panner": "3D 立體環繞(需使用耳機)", + "player__sound_effect_panner_enabled": "啟用", + "player__sound_effect_panner_sound_r": "聲音距離", + "player__sound_effect_panner_sound_speed": "環繞速度", + "player__sound_effect_pitch_shifter": "音調升降調節", + "player__sound_effect_pitch_shifter_preset_semitones": "{num} 半音", + "player__sound_effect_pitch_shifter_reset_btn": "重設", + "player__sound_effect_pitch_shifter_tip": "由於升降調需要即時處理音訊資料,這會導致額外的 CPU 占用。\n\n已知問題:\n如果 CPU 資源不夠將導致處理任務堆積而出現聲音異常。\n這時需要暫停播放一段時間等堆積的任務處理完畢再播放。", + "player__stop": "暫停播放", + "player__volume": "目前音量:", + "player__volume_mute_label": "靜音", + "player__volume_muted": "已靜音", + "search": "搜尋", + "search__hot_search": "熱門搜尋", + "search__type_music": "歌曲", + "search__type_songlist": "歌單", + "search__welcome": "搜我所想~~😉", + "setting": "設定", + "setting__about": "關於 LX Music", + "setting__backup": "備份與復原", + "setting__backup_all": "所有資料(清單資料與設定資料)", + "setting__backup_all_export": "匯出", + "setting__backup_all_export_desc": "選取備份儲存位置", + "setting__backup_all_import": "匯入", + "setting__backup_all_import_desc": "選取備份檔案", + "setting__backup_other": "其他備份格式(目前不支援復原此類備份檔案)", + "setting__backup_other_export_dir": "選取檔案儲存位置", + "setting__backup_other_export_list_csv": "匯出 CSV 格式清單", + "setting__backup_other_export_list_text": "匯出 TXT 格式清單", + "setting__backup_other_export_list_text_confirm": "是否將所有清單合併為一個檔案?", + "setting__backup_part": "部分資料(清單資料包括「試聽清單」、「我的最愛」和使用者自行建立的清單;設定資料不包括快速鍵設定)", + "setting__backup_part_export_list": "匯出清單", + "setting__backup_part_export_list_desc": "選取歌單儲存位置", + "setting__backup_part_export_setting": "匯出設定", + "setting__backup_part_export_setting_desc": "選取設定儲存位置", + "setting__backup_part_import_list": "匯入清單", + "setting__backup_part_import_list_confirm": "備份檔案中清單與現有清單 ID 相同時,現有清單內歌曲將會被覆寫,是否要繼續?", + "setting__backup_part_import_list_desc": "選取清單檔案", + "setting__backup_part_import_setting": "匯入設定", + "setting__backup_part_import_setting_desc": "選取設定檔", + "setting__basic": "基本設定", + "setting__basic_animation": "彈出層隨機動畫", + "setting__basic_control_btn_position": "控制按鈕位置", + "setting__basic_control_btn_position_left": "左邊", + "setting__basic_control_btn_position_right": "右邊", + "setting__basic_font": "字體", + "setting__basic_font_size": "字體大小", + "setting__basic_font_size_14px": "更小", + "setting__basic_font_size_15px": "小", + "setting__basic_font_size_16px": "標準", + "setting__basic_font_size_17px": "大", + "setting__basic_font_size_18px": "更大", + "setting__basic_font_size_19px": "非常大", + "setting__basic_lang": "語言", + "setting__basic_lang_title": "軟體顯示的語言", + "setting__basic_playbar_progress_style": "播放欄進度條樣式", + "setting__basic_playbar_progress_style_full": "全寬", + "setting__basic_playbar_progress_style_middle": "中等", + "setting__basic_playbar_progress_style_mini": "迷你", + "setting__basic_show_animation": "顯示動畫效果", + "setting__basic_source": "自訂來源 API", + "setting__basic_source_status_failed": "初始化失敗", + "setting__basic_source_status_initing": "初始化中...", + "setting__basic_source_status_success": "初始化成功", + "setting__basic_source_temp": "臨時 API(軟體的某些功能不可用,建議測試 API 不可用時再使用這個 API)", + "setting__basic_source_test": "測試 API(幾乎軟體的所有功能都可用)", + "setting__basic_source_user_api_btn": "自訂來源 API 管理", + "setting__basic_sourcename": "音樂串流平台名稱", + "setting__basic_sourcename_alias": "別名", + "setting__basic_sourcename_real": "原名", + "setting__basic_sourcename_title": "選取音樂串流平台名稱類型", + "setting__basic_start_in_fullscreen": "以全螢幕模式啟動", + "setting__basic_theme": "主題顏色", + "setting__basic_theme_auto_tip": "此乃動態主題,你可以預先設定一個亮色主題及暗色主題,此後將根據系統的亮、暗主題色自動切換為你預先設定的相應主題。\n\n註:滑鼠右擊此主題項即可開啟亮、暗色主題設定視窗。", + "setting__basic_to_tray": "關閉視窗時不退出軟體將其最小化到系統匣", + "setting__basic_window_size": "視窗尺寸", + "setting__basic_window_size_big": "大", + "setting__basic_window_size_huge": "巨大", + "setting__basic_window_size_larger": "更大", + "setting__basic_window_size_medium": "中", + "setting__basic_window_size_oversized": "超大", + "setting__basic_window_size_small": "小", + "setting__basic_window_size_smaller": "更小", + "setting__basic_window_size_title": "設定軟體視窗尺寸", + "setting__click_copy": "點擊複製", + "setting__click_open": "點擊開啟", + "setting__desktop_lyric": "桌面歌詞設定", + "setting__desktop_lyric_align": "歌詞對齊方式", + "setting__desktop_lyric_align_center": "置中", + "setting__desktop_lyric_align_left": "置左", + "setting__desktop_lyric_align_right": "置右", + "setting__desktop_lyric_always_on_top": "使歌詞視窗總是在其他視窗之上", + "setting__desktop_lyric_always_on_top_loop": "自動重新整理歌詞視窗置頂", + "setting__desktop_lyric_always_on_top_loop_tip": "當歌詞視窗置頂後仍被某些程式遮擋時可嘗試啟用此選項", + "setting__desktop_lyric_audio_visualization": "音訊視覺化(實驗性)", + "setting__desktop_lyric_color": "歌詞字體顏色", + "setting__desktop_lyric_color_reset": "重設顏色", + "setting__desktop_lyric_delay_scroll": "延遲歌詞滾動", + "setting__desktop_lyric_direction": "歌詞顯示方向", + "setting__desktop_lyric_direction_horizontal": "水平方向", + "setting__desktop_lyric_direction_vertical": "垂直方向", + "setting__desktop_lyric_ellipsis": "不允許歌詞換行", + "setting__desktop_lyric_enable": "顯示歌詞視窗", + "setting__desktop_lyric_font": "歌詞字體", + "setting__desktop_lyric_font_default": "預設", + "setting__desktop_lyric_font_weight": "加粗字體", + "setting__desktop_lyric_font_weight_extended": "翻譯、羅馬音歌詞", + "setting__desktop_lyric_font_weight_font": "逐字歌詞", + "setting__desktop_lyric_font_weight_line": "逐行歌詞", + "setting__desktop_lyric_fullscreen_hide": "全螢幕時自動關閉歌詞視窗", + "setting__desktop_lyric_hover_hide": "滑鼠移入歌詞視窗時增加歌詞透明度", + "setting__desktop_lyric_hover_hide_tip": "此功能存在平台相容性問題,在某些平台上可能會出現預料外的問題", + "setting__desktop_lyric_line_gap": "歌詞間距({num})", + "setting__desktop_lyric_line_gap_add": "加大間距", + "setting__desktop_lyric_line_gap_dec": "減小間距", + "setting__desktop_lyric_lock": "鎖定歌詞視窗", + "setting__desktop_lyric_lock_screen": "不允許歌詞視窗拖出主螢幕之外", + "setting__desktop_lyric_pause_hide": "暫停時增加歌詞視窗透明度", + "setting__desktop_lyric_played_color": "已播放顏色", + "setting__desktop_lyric_reset": "重設", + "setting__desktop_lyric_reset_window": "重設視窗設定", + "setting__desktop_lyric_scroll_align": "正在播放歌詞滾動位置", + "setting__desktop_lyric_scroll_align_center": "中心", + "setting__desktop_lyric_scroll_align_top": "頂部", + "setting__desktop_lyric_shadow_color": "陰影顏色", + "setting__desktop_lyric_show_taskbar": "在工具列顯示歌詞視窗處理程式", + "setting__desktop_lyric_show_taskbar_tip": "此選項作為在螢幕錄影軟體無法擷取歌詞視窗時的變通解決方法", + "setting__desktop_lyric_unplay_color": "未播放顏色", + "setting__dislike_list_input_tip": "標題@演出者\n標題\n@演出者", + "setting__dislike_list_save_btn": "儲存", + "setting__dislike_list_tips": "1. 每條一行,若歌曲或者演出者名字中存在「@」符號,需要將其取代成「#」\n2. 指定某演出者的某首歌:標題@演出者\n3. 指定某首歌:標題\n4. 指定某演出者:@演出者", + "setting__dislike_list_title": "「不喜歡的歌曲」規則清單", + "setting__download": "下載設定", + "setting__download_data_embed": "嵌入到音訊檔案中的內容", + "setting__download_embed_lxlyric": "同時嵌入 LX Music 歌詞(如果有,使用 LX Music 播放時可支持逐字歌詞)", + "setting__download_embed_lyric": "嵌入歌詞", + "setting__download_embed_pic": "嵌入封面", + "setting__download_embed_rlyric": "同時嵌入羅馬音歌詞(如果有)", + "setting__download_embed_tlyric": "同時嵌入翻譯歌詞(如果有)", + "setting__download_enable": "啟用下載功能", + "setting__download_lxlyric": "同時將 LX Music 歌詞寫入歌詞文件中(如果有,使用 LX Music 播放時可支持逐字歌詞)", + "setting__download_lyric": "歌詞下載", + "setting__download_lyric_format": "下載的歌詞檔案編碼格式", + "setting__download_lyric_format_gbk": "GBK", + "setting__download_lyric_format_tip": "在某些裝置上出現中文亂碼時可嘗試選取 GBK 格式", + "setting__download_lyric_format_utf8": "UTF-8", + "setting__download_lyric_title": "同時下載歌詞檔案", + "setting__download_max_num": "同時下載任務數", + "setting__download_max_num_tip": "過大的同時下載數量可能會導致你的 IP 被自訂來源 API 封禁,是否確認修改?", + "setting__download_max_num_tooltip": "設定過大可能會導致 IP 被封,這取決於自訂來源 API", + "setting__download_name": "檔案命名方式", + "setting__download_name1": "「標題 - 演出者」", + "setting__download_name2": "「演出者 - 標題」", + "setting__download_name3": "僅標題", + "setting__download_name_title": "下載歌曲時的命名方式", + "setting__download_path": "下載路徑", + "setting__download_path_change_btn": "變更", + "setting__download_path_label": "目前下載路徑:", + "setting__download_path_open_label": "點擊開啟目前路徑", + "setting__download_path_title": "下載歌曲儲存的路徑", + "setting__download_rlyric": "同時將羅馬音歌詞寫入歌詞檔案中(如果有)", + "setting__download_select_save_path": "選取歌曲儲存路徑", + "setting__download_skip_exist_file": "下載目錄存在同名檔案時跳過下載此任務", + "setting__download_tlyric": "同時將翻譯歌詞寫入歌詞檔案中(如果有)", + "setting__download_use_other_source": "自動變更來源下載", + "setting__download_use_other_source_tip": "當無法從歌曲的原始來源下載時,嘗試變更到其他來源下載。\n註:此功能不 100% 保證變更來源後的歌曲與原版一致。", + "setting__hot_key": "快速鍵設定", + "setting__hot_key_common_focus_search_input": "聚焦搜尋框", + "setting__hot_key_common_min": "最小化軟體", + "setting__hot_key_common_toggle_close": "退出軟體", + "setting__hot_key_common_toggle_hide": "顯示/隱藏軟體", + "setting__hot_key_common_toggle_min": "最小化/還原軟體", + "setting__hot_key_desktop_lyric_toggle_always_top": "歌詞視窗置頂切換", + "setting__hot_key_desktop_lyric_toggle_lock": "歌詞視窗鎖定切換", + "setting__hot_key_desktop_lyric_toggle_visible": "開啟/關閉歌詞視窗", + "setting__hot_key_global_title": "全域快速鍵", + "setting__hot_key_local_title": "軟體內快速鍵", + "setting__hot_key_player_music_dislike": "不喜歡該歌曲", + "setting__hot_key_player_music_love": "收藏歌曲", + "setting__hot_key_player_music_unlove": "取消收藏", + "setting__hot_key_player_next": "下一首歌曲", + "setting__hot_key_player_prev": "上一首歌曲", + "setting__hot_key_player_seekbackward": "快退 5s", + "setting__hot_key_player_seekforward": "快進 5s", + "setting__hot_key_player_toggle_play": "播放/暫停控制", + "setting__hot_key_player_volume_down": "減少音量", + "setting__hot_key_player_volume_mute": "靜音切換", + "setting__hot_key_player_volume_up": "增加音量", + "setting__hot_key_tip_input": "請輸入新的按鍵", + "setting__hot_key_unset_input": "未設定", + "setting__is_enable": "啟用", + "setting__is_show": "顯示", + "setting__list": "清單設定", + "setting__list_action_btn": "顯示清單操作按鈕", + "setting__list_add_music_location_type": "加入歌曲到清單時的位置", + "setting__list_add_music_location_type_bottom": "底部", + "setting__list_add_music_location_type_top": "頂部", + "setting__list_click_action": "雙擊清單裡的歌曲時自動切換到目前清單播放(僅對「歌單」、「排行榜」有效)", + "setting__list_scroll": "記住播放清單滾動條位置(僅對「我的清單」有效)", + "setting__list_source": "顯示歌曲來自哪個音樂串流平台(僅對「我的清單」有效)", + "setting__network": "網路設定", + "setting__network_proxy_host": "主機", + "setting__network_proxy_password": "密碼", + "setting__network_proxy_port": "埠", + "setting__network_proxy_title": "HTTP 代理設定(亂設定軟體將無法聯網)", + "setting__network_proxy_username": "使用者名稱", + "setting__odc": "強迫症設定", + "setting__odc_clear_search_input": "離開搜尋頁面時清空搜尋框", + "setting__odc_clear_search_list": "離開搜尋頁面時清空搜尋清單", + "setting__open_api": "開放 API", + "setting__open_api_address": "服務位址:", + "setting__open_api_bind_lan": "允許來自區域網路的訪問", + "setting__open_api_enable": "啟用開放 API 服務", + "setting__open_api_port": "服務埠", + "setting__open_api_port_tip": "請輸入開放 API 服務埠", + "setting__open_api_tip": "該功能用於為第三方軟體提供串接 LX Music 的能力,目前可用的功能可以看:", + "setting__open_api_tip_link": "串接教學文件", + "setting__other": "其他", + "setting__other_dislike_list": "「不喜歡的歌曲」規則", + "setting__other_dislike_list_label": "規則數量:", + "setting__other_dislike_list_show_btn": "編輯規則", + "setting__other_listdata": "清單資料清理", + "setting__other_listdata_clear_btn": "清空「我的清單」資料", + "setting__other_listdata_clear_tip_confirm": "這將清理你建立的「所有清單」及收藏的「所有歌曲」,是否真的要繼續?", + "setting__other_lyric_edited_cache": "已調整過偏移時間的歌詞管理", + "setting__other_lyric_edited_clear_btn": "清理已調整過時間的歌詞", + "setting__other_lyric_edited_clear_tip_confirm": "這將清理所有你之前已調整過偏移時間的歌詞,是否確認清理?(手抖確認🤪)", + "setting__other_lyric_edited_label": "歌詞數量:", + "setting__other_lyric_raw_clear_btn": "清理歌詞快取", + "setting__other_lyric_raw_label": "歌詞數量:", + "setting__other_music_url_clear_btn": "清理歌曲 URL 快取", + "setting__other_music_url_label": "歌曲 URL 數量:", + "setting__other_other_cache": "其他快取管理", + "setting__other_other_source_clear_btn": "清理變更來源歌曲快取", + "setting__other_other_source_label": "變更來源歌曲資訊數量:", + "setting__other_resource_cache": "資源快取管理", + "setting__other_resource_cache_clear_btn": "清理資源快取", + "setting__other_resource_cache_confirm": "我要清掉", + "setting__other_resource_cache_label": "軟體已使用快取大小:", + "setting__other_resource_cache_tip": "包括圖片、音訊等快取,清理後這些資源將需要重新下載,不建議清理,軟體會根據磁碟空間動態管理快取大小", + "setting__other_resource_cache_tip_confirm": "涉及圖片、音訊等快取,清理後這些資源將需要重新下載,不建議清理,軟體會根據磁碟空間動態管理快取大小,是否仍要清理?", + "setting__other_transparent_window": "主視窗使用軟體內建的圓角及陰影", + "setting__other_transparent_window_tip": "關閉後將使用系統原生視窗樣式。更改後重啟軟體生效。", + "setting__other_tray_theme": "系統匣圖示樣式", + "setting__other_tray_theme_auto": "跟隨系統亮暗模式", + "setting__other_tray_theme_black": "黑色", + "setting__other_tray_theme_native": "白色", + "setting__other_tray_theme_origin": "原色", + "setting__play": "播放設定", + "setting__play_advanced_audio_features_tip": "自訂音訊輸出裝置與該功能衝突,啟用該功能後音訊輸出裝置將會被重設為預設,目前此問題暫無法解決。是否仍要開啟?", + "setting__play_auto_clean_played_list": "點擊與播放清單相同的清單切歌時清空已播放清單", + "setting__play_auto_clean_played_list_tip": "隨機模式下清單內所有歌曲會重新參與隨機", + "setting__play_auto_skip_on_error": "播放錯誤時自動切換歌曲", + "setting__play_detail": "播放詳情頁設定", + "setting__play_detail_align": "歌詞對齊方式", + "setting__play_detail_align_center": "置中", + "setting__play_detail_align_left": "置左", + "setting__play_detail_align_right": "置右", + "setting__play_detail_font_size": "歌詞字體大小(可以在播放詳情頁使用鍵盤的「+」「-」調整字體大小)", + "setting__play_detail_font_size_current": "目前字體大小:{size}", + "setting__play_detail_font_size_reset": "重設", + "setting__play_detail_font_zoom": "縮放目前正在播放的歌詞", + "setting__play_detail_lyric_delay_scroll": "延遲歌詞滾動", + "setting__play_detail_lyric_progress": "允許透過拖曳歌詞調整播放進度", + "setting__play_lyric_lxlrc": "使用卡拉 OK 式歌詞播放(如果可用)", + "setting__play_lyric_lxlrc_tip": "此功能比較耗性能,低配置電腦不建議開啟!", + "setting__play_lyric_roma": "顯示歌詞羅馬音(如果可用)", + "setting__play_lyric_s2t": "將播放與下載的中文歌詞轉換為繁體", + "setting__play_lyric_transition": "顯示歌詞翻譯(如果可用)", + "setting__play_max_output_channel_count": "使用裝置能處理的最大聲道數輸出音訊", + "setting__play_mediaDevice": "音訊輸出", + "setting__play_mediaDevice_remove_stop_play": "目前的音訊輸出裝置被改變時暫停播放歌曲", + "setting__play_mediaDevice_title": "選取音訊輸出的媒體裝置", + "setting__play_media_device_error_tip": "此功能與進階音訊功能(音訊可視化、音效設定、使用裝置能處理的最大聲道數輸出音訊功能)衝突。你本次啟動軟體時已啟用這些功能,此選項暫不可用。請關閉這些功能並重啟軟體後,再修改此選項!", + "setting__play_media_device_tip": "此功能與音訊可視化功能衝突,兩者無法同時啟用。是否關閉「音訊可視化」並應用所選音訊輸出設定?", + "setting__play_playQuality": "優先播放的音質(如果可用)", + "setting__play_power_save_blocker": "播放歌曲時阻止電腦休眠", + "setting__play_save_play_time": "記住播放進度", + "setting__play_startup_auto_play": "啟動軟體後自動播放音樂", + "setting__play_statusbar_lyric": "在選單列的「狀態」選單顯示歌詞", + "setting__play_statusbar_lyric_tip": "需要在「設定 → 基本設定」啟用「關閉視窗時不退出軟體將其最小化到系統匣」。", + "setting__play_task_bar": "在工具列上顯示目前歌曲播放進度", + "setting__play_timeout": "定時暫停", + "setting__player_audio_visualization_tip": "自訂音訊輸出裝置與音訊可視化功能衝突,啟用「音訊可視化」後音訊輸出裝置將會被重設為預設,目前此問題暫無法解決。是否仍要開啟?", + "setting__player_swap_lyric_trans_roma": "調換歌詞翻譯與歌詞羅馬音位置", + "setting__search": "搜尋設定", + "setting__search_focus_search_box": "啟動時自動聚焦搜尋框", + "setting__search_history": "顯示歷史搜尋記錄", + "setting__search_hot": "顯示熱門搜尋", + "setting__sync": "資料同步", + "setting__sync_client_address": "目前裝置位址:{address}", + "setting__sync_client_host": "同步服務位址", + "setting__sync_client_host_tip": "http://:<埠號>", + "setting__sync_client_mode": "用戶端模式", + "setting__sync_client_status": "狀態:{status}", + "setting__sync_code_blocked_ip": "目前裝置的 IP 已被服務端封禁!", + "setting__sync_code_fail": "連線碼無效", + "setting__sync_enable": "啟用同步功能", + "setting__sync_mode": "同步模式", + "setting__sync_mode_client": "用戶端模式", + "setting__sync_mode_server": "服務端模式", + "setting__sync_server_address": "同步服務位址:{address}", + "setting__sync_server_auth_code": "連線碼:{code}", + "setting__sync_server_device": "已連線的裝置:{devices}", + "setting__sync_server_device_list_btn_remove": "移除", + "setting__sync_server_device_list_noitem": "這裡什麼也沒有 ┗( ▔, ▔ )┛", + "setting__sync_server_device_list_time": "最後連線時間:{time}", + "setting__sync_server_device_list_tips": "💡 裝置被移除後,再連線時需要重新輸入連線碼", + "setting__sync_server_device_list_title": "已認證裝置", + "setting__sync_server_mode": "服務端模式(由於資料是明文傳輸,請在受信任的網路下使用)", + "setting__sync_server_port": "同步埠設定", + "setting__sync_server_port_tip": "請輸入同步服務埠號", + "setting__sync_server_refresh_code": "重新獲取連線碼", + "setting__sync_server_show_device_list": "已認證裝置清單", + "setting__sync_tip": "使用方式請看常見問題「同步功能」部分", + "setting__update": "軟體更新", + "setting__update_checking": "檢查更新中...", + "setting__update_commit_date": "提交日期:", + "setting__update_commit_id": "程式碼版本:", + "setting__update_current_label": "目前版本:", + "setting__update_downloading": "發現新版本並在努力下載中,請稍後...⏳", + "setting__update_init": "處理更新中...", + "setting__update_latest": "軟體已是最新,盡情地體驗吧~🥂", + "setting__update_latest_label": "最新版本:", + "setting__update_new_version": "發現新版本,趕快去更新吧~🚀🚀", + "setting__update_open_version_modal_btn": "開啟更新視窗", + "setting__update_progress": "狀態:", + "setting__update_show_change_log": "更新版本後的首次啟動時顯示更新日誌", + "setting__update_try_auto_update": "發現新版本時嘗試自動下載更新", + "setting__update_unknown": "未知", + "setting__update_unknown_tip": "❓ 獲取最新版本資訊失敗,建議在「關於」頁面開啟項目發布位址查看目前版本是否最新", + "setting_download_save_group_list_name": "將檔案儲存到以對應清單命名的子目錄中", + "setting_sync_status_enabled": "已連線", + "song_list": "歌單", + "songlist__import_input_btn_confirm": "開啟", + "songlist__import_input_show_btn": "開啟歌單", + "songlist__import_input_tip": "輸入歌單連結或歌單 ID", + "songlist__import_input_tip_1": "不支援跨平台開啟歌單,請確認要開啟的歌單與目前歌單的來源平台是否對應", + "songlist__import_input_tip_2": "若遇到無法開啟的歌單連結,歡迎回報", + "songlist__import_input_tip_3": "酷狗音樂歌單不支援用歌單 ID 與概念版連結開啟,但支援用普通版連結與酷狗碼開啟", + "songlist__import_input_tip_4": "網易雲音樂「私人雷達」歌單需要 Token 才能開啟,企鵝音樂個人歌單需要將個人資料頁公開才能開啟,詳情看 ", + "songlist__import_input_title": "開啟分享的歌單", + "songlist__open_list": "開啟「{name}」歌單", + "songlist__tag_info_hot_tag": "熱門標籤", + "source_alias_all": "聚合大會", + "source_alias_bd": "小杜音樂", + "source_alias_kg": "小枸音樂", + "source_alias_kw": "小蝸音樂", + "source_alias_mg": "小蜜音樂", + "source_alias_tx": "小秋音樂", + "source_alias_wy": "小芸音樂", + "source_alias_xm": "小霞音樂", + "source_all": "聚合搜尋", + "source_bd": "百度音樂", + "source_kg": "酷狗音樂", + "source_kw": "酷我音樂", + "source_mg": "咪咕音樂", + "source_tx": "企鵝音樂", + "source_wy": "網易音樂", + "source_xm": "蝦米音樂", + "sync__auth_code_input_tip": "請輸入連線碼", + "sync__auth_code_title": "需要輸入連線碼", + "sync__dislike_merge_tip_desc": "合併兩邊清單內容並去重", + "sync__dislike_other_tip_desc": "「取消同步」將不使用「不喜歡的歌曲」清單同步功能", + "sync__dislike_overwrite_tip_desc": "被覆寫者的清單將被取代成覆寫者的清單", + "sync__dislike_title": "選取與「{name}」的「不喜歡的歌曲」清單同步方式", + "sync__list_merge_tip_desc": "將兩邊的清單合併到一起,相同的歌曲將被去掉(去掉的是被合併者的歌曲),不同的歌曲將被添加。", + "sync__list_other_tip_desc": "「取消同步」將不使用清單同步功能。", + "sync__list_overwrite_tip_desc": "被覆寫者與覆寫者清單 ID 相同的清單將被移除後取代成覆寫者的清單(清單 ID 不同的清單將被合併到一起)。若勾選「完全覆寫」,則被覆寫者的所有清單將被移除,然後取代成覆寫者的清單。", + "sync__list_title": "選取與「{name}」的清單同步方式", + "sync__merge_btn_local_remote": "「本機清單」合併「遠端清單」", + "sync__merge_btn_remote_local": "「遠端清單」合併「本機清單」", + "sync__merge_label": "合併", + "sync__merge_tip": "合併:", + "sync__other_label": "其他", + "sync__other_tip": "其他:", + "sync__overwrite": "完全覆寫", + "sync__overwrite_btn_cancel": "取消同步", + "sync__overwrite_btn_local_remote": "「本機清單」覆寫「遠端清單」", + "sync__overwrite_btn_none": "僅使用即時同步功能", + "sync__overwrite_btn_remote_local": "「遠端清單」覆寫「本機清單」", + "sync__overwrite_label": "覆寫", + "sync__overwrite_tip": "覆寫:", + "sync_status_disabled": "未連線", + "tag__high_quality": "HQ", + "tag__lossless": "SQ", + "tag__lossless_24bit": "24bit", + "theme_add": "加入主題", + "theme_auto": "道法自然", + "theme_auto_tip": "滑鼠右擊可開啟亮、暗主題設定視窗", + "theme_black": "黑燈瞎火", + "theme_blue": "藍田生玉", + "theme_blue2": "清熱版藍", + "theme_blue_plus": "蛋雅深藍", + "theme_china_ink": "近墨者黑", + "theme_edit_modal__app_bg": "應用背景顏色", + "theme_edit_modal__aside_color": "側欄按鈕顏色", + "theme_edit_modal__badge": "標籤顏色", + "theme_edit_modal__badge_primary": "主顏色", + "theme_edit_modal__badge_secondary": "次要顏色", + "theme_edit_modal__badge_tertiary": "第三顏色", + "theme_edit_modal__bg_image": "背景圖片", + "theme_edit_modal__bg_image_add": "背景圖片:", + "theme_edit_modal__bg_image_change": "變更背景圖片", + "theme_edit_modal__bg_image_remove": "移除背景圖片", + "theme_edit_modal__close_btn": "關閉", + "theme_edit_modal__control_btn": "左側控制按鈕顏色", + "theme_edit_modal__copy": "複製主題", + "theme_edit_modal__dark": "暗色主題", + "theme_edit_modal__dark_font": "深色字體", + "theme_edit_modal__font": "字體顏色", + "theme_edit_modal__hide_btn": "隱藏播放詳情頁", + "theme_edit_modal__main_bg": "內容區域背景顏色", + "theme_edit_modal__min_btn": "最小化", + "theme_edit_modal__pick_cancel": "重設", + "theme_edit_modal__pick_color": "選取顏色", + "theme_edit_modal__pick_last_color": "使用之前的顏色", + "theme_edit_modal__pick_save": "確認", + "theme_edit_modal__preview": "預覽主題", + "theme_edit_modal__primary": "主題色", + "theme_edit_modal__remove": "移除", + "theme_edit_modal__remove_tip": "是否真的要移除這個主題?", + "theme_edit_modal__save_new": "另存", + "theme_edit_modal__select_bg_file": "選取背景圖片", + "theme_edit_modal__title_add": "新增主題", + "theme_edit_modal__title_edit": "編輯主題", + "theme_green": "綠意盎然", + "theme_grey": "灰常美麗", + "theme_happy_new_year": "新年快樂", + "theme_max_tip": "最多只能加入 10 個主題哦,刪掉一些再加吧😜", + "theme_mid_autumn": "月裡嫦娥", + "theme_ming": "青出於黑", + "theme_more_btn_show": "更多主題", + "theme_naruto": "木葉之村", + "theme_orange": "橙黃橘綠", + "theme_pink": "粉裝玉琢", + "theme_purple": "重斤球紫", + "theme_red": "熱情似火", + "theme_selector_modal__dark_title": "暗色主題", + "theme_selector_modal__light_title": "亮色主題", + "theme_selector_modal__theme_name": "主題名稱", + "theme_selector_modal__title": "跟隨系統主題設定", + "theme_selector_modal__title_tip": "註:你可以預先設定一個亮色主題及暗色主題,此後將根據系統的亮、暗主題色自動切換為你預先設定的相應主題。", + "toggle_source_failed": "變更來源失敗,請嘗試手動在搜尋頁指定其他音樂串流平台搜尋該歌曲播放", + "toggle_source_try": "嘗試變更到其他來源...", + "update__downgrade_tip": "我們發現你降級了版本({ver}),若使用新版本時遇到問題,請先嘗試閱讀常見問題解決,若你遇到的問題在常見問題中未記錄或無法解決,可以透過教學文件中提到的回報管道向我們回報😘!\n\n注意:從新版本降級舊版時建議先備份歌單,若出現異常則可透過清理資料解決,資料目錄路徑在教學文件有記錄。", + "update__error_top": "自動下載新版本失敗,你可以嘗試重新下載更新或者手動去下載更新。\n\n新版位址在更新彈出視窗下面有寫,下載新版直接覆寫安裝即可,若安裝失敗則看常見問題解決。\n\n注意:目前只有 Windows 安裝版可以自動更新(Linux 的 AppImage、deb 版似乎也可以,未測試),其他版本請手動下載更新!", + "update__ignore_cancel": "我就不想更新🤨", + "update__ignore_confirm": "好,去更新看看❤️", + "update__ignore_confirm_tip": "目前只有 Windows 安裝版可以自動更新(Linux 的 AppImage、deb 版似乎也可以,未測試),其他版本請手動下載更新,\n新版位址在更新彈出視窗下面有寫,下載新版直接覆寫安裝即可,若安裝失敗看常見問題解決。", + "update__ignore_confirm_tip_confirm": "OK,已了解", + "update__ignore_tip": "你現在使用的版本距離最新版本已經落後了 {num} 個版本🤪,為了更好的使用體驗,建議更新到最新版本哦~!\n\n註:若使用新版本時遇到問題,請先嘗試閱讀常見問題解決,若你遇到的問題在常見問題中未記錄或無法解決,可以透過教學文件中提到的回報管道向我們回報😘!", + "update__timeout_top": "下載時間過長提示\n\n你目前所在網路訪問 GitHub 較慢,新版本已經下了一個鐘了還沒完成😳,你仍可以繼續等,但牆裂建議手動更新版本!", + "user_api__allow_show_update_alert": "允許顯示更新彈出視窗", + "user_api__btn_export": "匯出", + "user_api__btn_import": "從本機匯入", + "user_api__btn_import_online": "從線上匯入", + "user_api__btn_remove": "移除", + "user_api__import_file": "選取自訂來源 API 腳本檔案", + "user_api__init_failed_alert": "自訂來源 API「{name}」初始化失敗:", + "user_api__max_tip": "最多只能同時存在 20 個 API 哦🤪\n想要繼續匯入的話,請先移除一些舊的 API 騰出位置吧", + "user_api__noitem": "這裡竟然是空的😲", + "user_api__note": "提示:雖然我們已經盡可能地隔離了 API 腳本的執行環境,但匯入包含惡意行為的 API 腳本仍可能會影響你的系統,請謹慎匯入。", + "user_api__readme": "API 編寫說明:", + "user_api__title": "自訂來源 API 管理", + "user_api__update_alert": "自訂來源 API「{name}」發現新版本:", + "user_api__update_alert_open_url": "開啟更新位址", + "user_api_import__failed": "自訂來源 API 匯入失敗:\n{message}", + "user_api_import_online__input_confirm": "匯入", + "user_api_import_online__input_loading": "匯入中...", + "user_api_import_online__input_tip": "請輸入 HTTP 連結", + "user_api_import_online__title": "從線上匯入自訂來源 API" +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/.eslintrc.cjs b/plugins/lx-music-ztools/vendor/lx/src/main/.eslintrc.cjs new file mode 100644 index 000000000..3adda96a1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/.eslintrc.cjs @@ -0,0 +1,18 @@ +/* eslint-env node */ +const { base, typescript } = require('../../.eslintrc.base.cjs') + +module.exports = { + root: true, + ...base, + overrides: [ + { + ...typescript, + parserOptions: { + project: './tsconfig.json', + }, + }, + ], + ignorePatterns: [ + 'vendors', + ], +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/app.ts b/plugins/lx-music-ztools/vendor/lx/src/main/app.ts new file mode 100644 index 000000000..53696fb85 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/app.ts @@ -0,0 +1,337 @@ +import path from 'node:path' +import { existsSync, mkdirSync, renameSync } from 'fs' +import { app, shell, screen, nativeTheme, dialog } from 'electron' +import { URL_SCHEME_RXP } from '@common/constants' +import { getProxy, getTheme, initHotKey, initSetting, parseEnvParams } from './utils' +import { navigationUrlWhiteList } from '@common/config' +import defaultSetting from '@common/defaultSetting' +import { isExistWindow as isExistMainWindow, showWindow as showMainWindow } from './modules/winMain' +import { createAppEvent, createDislikeEvent, createListEvent } from '@main/event' +import { isMac, log } from '@common/utils' +import createWorkers from './worker' +import { migrateDBData } from './utils/migrate' +import { openDirInExplorer } from '@common/utils/electron' +import { setProxyByHost } from '@common/utils/request' + +export const initGlobalData = () => { + const envParams = parseEnvParams() + // envParams.cmdParams.dt = !!envParams.cmdParams.dt + + global.envParams = { + cmdParams: envParams.cmdParams, + deeplink: envParams.deeplink, + } + global.lx = { + inited: false, + isSkipTrayQuit: false, + // mainWindowClosed: true, + event_app: createAppEvent(), + event_list: createListEvent(), + event_dislike: createDislikeEvent(), + appSetting: defaultSetting, + worker: createWorkers(), + hotKey: { + enable: true, + config: { + local: { + enable: false, + keys: {}, + }, + global: { + enable: false, + keys: {}, + }, + }, + state: new Map(), + }, + theme: { + shouldUseDarkColors: nativeTheme.shouldUseDarkColors, + theme: { + id: '', + name: '', + isDark: false, + colors: {}, + }, + }, + player_status: { + status: 'stoped', + name: '', + singer: '', + albumName: '', + picUrl: '', + progress: 0, + duration: 0, + playbackRate: 1, + lyricLineText: '', + lyricLineAllText: '', + lyric: '', + tlyric: '', + rlyric: '', + lxlyric: '', + collect: false, + volume: 0, + mute: false, + }, + } + + global.staticPath = + process.env.NODE_ENV !== 'production' + ? webpackStaticPath + : path.join(__dirname, 'static') +} + +export const initSingleInstanceHandle = () => { + // 单例应用程序 + if (!app.requestSingleInstanceLock()) { + app.quit() + process.exit(0) + } + + app.on('second-instance', (event, argv, cwd) => { + if (isExistMainWindow()) { + const envParams = parseEnvParams(argv) + if (envParams.deeplink) { + global.envParams.deeplink = envParams.deeplink + global.lx.event_app.deeplink(global.envParams.deeplink) + return + } + if (envParams.cmdParams.hidden !== true) { + showMainWindow() + } + } else { + app.quit() + } + }) +} + +export const applyElectronEnvParams = () => { + // Is disable hardware acceleration + if (global.envParams.cmdParams.dha) app.disableHardwareAcceleration() + if (global.envParams.cmdParams.dhmkh) app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling') + + // fix linux transparent fail. https://github.com/electron/electron/issues/25153#issuecomment-843688494 + if (process.platform == 'linux') app.commandLine.appendSwitch('use-gl', 'desktop') + + // https://github.com/electron/electron/issues/22691 + app.commandLine.appendSwitch('wm-window-animations-disabled') + + app.commandLine.appendSwitch('--disable-gpu-sandbox') + + // proxy + if (global.envParams.cmdParams['proxy-server']) { + app.commandLine.appendSwitch('proxy-server', global.envParams.cmdParams['proxy-server']) + app.commandLine.appendSwitch('proxy-bypass-list', global.envParams.cmdParams['proxy-bypass-list'] ?? '') + } +} + +export const setUserDataPath = () => { + // windows平台下如果应用目录下存在 portable 文件夹则将数据存在此文件下 + if (process.platform == 'win32') { + const portablePath = path.join(path.dirname(app.getPath('exe')), '/portable') + if (existsSync(portablePath)) { + app.setPath('appData', portablePath) + const appDataPath = path.join(portablePath, '/userData') + if (!existsSync(appDataPath)) mkdirSync(appDataPath) + app.setPath('userData', appDataPath) + } + } + + const userDataPath = app.getPath('userData') + global.lxOldDataPath = userDataPath + global.lxDataPath = path.join(userDataPath, 'LxDatas') + if (!existsSync(global.lxDataPath)) mkdirSync(global.lxDataPath) +} + +export const registerDeeplink = (startApp: () => void) => { + if (process.env.NODE_ENV !== 'production' && process.platform === 'win32') { + // Set the path of electron.exe and your app. + // These two additional parameters are only available on windows. + // console.log(process.execPath, process.argv) + app.setAsDefaultProtocolClient('lxmusic', process.execPath, process.argv.slice(1)) + } else { + app.setAsDefaultProtocolClient('lxmusic') + } + + // deep link + app.on('open-url', (event, url) => { + if (!URL_SCHEME_RXP.test(url)) return + event.preventDefault() + global.envParams.deeplink = url + if (isExistMainWindow()) { + if (global.envParams.deeplink) global.lx.event_app.deeplink(global.envParams.deeplink) + else showMainWindow() + } else { + startApp() + } + }) +} + +export const listenerAppEvent = (startApp: () => void) => { + app.on('web-contents-created', (event, contents) => { + contents.on('will-navigate', (event, navigationUrl) => { + if (process.env.NODE_ENV !== 'production') { + console.log('navigation to url:', navigationUrl.length > 130 ? navigationUrl.substring(0, 130) + '...' : navigationUrl) + return + } + if (!navigationUrlWhiteList.some(url => url.test(navigationUrl))) { + event.preventDefault() + return + } + console.log('navigation to url:', navigationUrl) + }) + contents.setWindowOpenHandler(({ url }) => { + if (!/^devtools/.test(url) && /^https?:\/\//.test(url)) { + void shell.openExternal(url) + } + console.log(url) + return { action: 'deny' } + }) + contents.on('will-attach-webview', (event, webPreferences, params) => { + // Strip away preload scripts if unused or verify their location is legitimate + delete webPreferences.preload + // delete webPreferences.preloadURL + + // Disable Node.js integration + webPreferences.nodeIntegration = false + + // Verify URL being loaded + if (!navigationUrlWhiteList.some(url => url.test(params.src))) { + event.preventDefault() + } + }) + + // disable create dictionary + // https://github.com/lyswhut/lx-music-desktop/issues/773 + contents.session.setSpellCheckerDictionaryDownloadURL('http://0.0.0.0') + }) + + app.on('activate', () => { + if (isExistMainWindow()) { + showMainWindow() + } else { + startApp() + } + }) + + app.on('before-quit', () => { + global.lx.isSkipTrayQuit = true + }) + app.on('window-all-closed', () => { + if (isMac) return + + app.quit() + }) + + const initScreenParams = () => { + global.envParams.workAreaSize = screen.getPrimaryDisplay().workAreaSize + } + app.on('ready', () => { + screen.on('display-metrics-changed', initScreenParams) + initScreenParams() + }) + + nativeTheme.addListener('updated', () => { + const shouldUseDarkColors = nativeTheme.shouldUseDarkColors + if (shouldUseDarkColors == global.lx.theme.shouldUseDarkColors) return + global.lx.theme.shouldUseDarkColors = shouldUseDarkColors + global.lx?.event_app.system_theme_change(shouldUseDarkColors) + }) + + const setProxy = () => { + const proxy = getProxy() + if (proxy) { + setProxyByHost(proxy.host, proxy.port ? String(proxy.port) : undefined) + } else setProxyByHost() + } + global.lx.event_app.on('updated_config', (keys, setting) => { + if (keys.includes('network.proxy.enable') || (global.lx.appSetting['network.proxy.enable'] && keys.some(k => k.includes('network.proxy.')))) { + setProxy() + } + + if (keys.includes('player.volume')) { + global.lx.event_app.player_status({ volume: Math.trunc(setting['player.volume']! * 100) }) + } + if (keys.includes('player.isMute')) { + global.lx.event_app.player_status({ mute: setting['player.isMute'] }) + } + }) + global.lx.event_app.on('app_inited', () => { + setProxy() + global.lx.event_app.player_status({ volume: Math.trunc(global.lx.appSetting['player.volume'] * 100) }) + global.lx.event_app.player_status({ mute: global.lx.appSetting['player.isMute'] }) + }) +} + +const initTheme = () => { + global.lx.theme = getTheme() + const themeConfigKeys = ['theme.id', 'theme.lightId', 'theme.darkId'] + global.lx.event_app.on('updated_config', (keys) => { + let requireUpdate = false + for (const key of keys) { + if (themeConfigKeys.includes(key)) { + requireUpdate = true + break + } + } + if (requireUpdate) { + global.lx.theme = getTheme() + global.lx.event_app.theme_change() + } + }) + global.lx.event_app.on('system_theme_change', () => { + if (global.lx.appSetting['theme.id'] == 'auto') { + global.lx.theme = getTheme() + global.lx.event_app.theme_change() + } + }) +} + +const backupDB = (backupPath: string) => { + const dbPath = path.join(global.lxDataPath, 'lx.data.db') + try { + renameSync(dbPath, backupPath) + } catch {} + try { + renameSync(`${dbPath}-wal`, `${backupPath}-wal`) + } catch {} + try { + renameSync(`${dbPath}-shm`, `${backupPath}-shm`) + } catch {} + openDirInExplorer(backupPath) +} + +let isInitialized = false +export const initAppSetting = async() => { + if (!global.lx.inited) { + const config = await initHotKey() + global.lx.hotKey.config.local = config.local + global.lx.hotKey.config.global = config.global + global.lx.inited = true + } + + if (!isInitialized) { + let dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath) + if (dbFileExists === null) { + const backupPath = path.join(global.lxDataPath, `lx.data.db.${Date.now()}.bak`) + dialog.showMessageBoxSync({ + type: 'warning', + message: 'Database verify failed', + detail: `数据库表结构校验失败,我们将把有问题的数据库备份到:${backupPath}\n若此问题导致你的数据丢失,你可以尝试从备份文件找回它们。\n\nThe database table structure verification failed, we will back up the problematic database to: ${backupPath}\nIf this problem causes your data to be lost, you can try to retrieve them from the backup file.`, + }) + backupDB(backupPath) + dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath) + } + global.lx.appSetting = (await initSetting()).setting + if (!dbFileExists) await migrateDBData().catch(err => { log.error(err) }) + initTheme() + if (envParams.cmdParams.dt == null) envParams.cmdParams.dt = !global.lx.appSetting['common.transparentWindow'] + } + // global.lx.theme = getTheme() + + isInitialized ||= true +} + +export const quitApp = () => { + global.lx.isSkipTrayQuit = true + app.quit() +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/event/AppEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/event/AppEvent.ts new file mode 100644 index 000000000..d128fa0c0 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/event/AppEvent.ts @@ -0,0 +1,122 @@ +import { EventEmitter } from 'events' + +import { saveAppHotKeyConfig, updateSetting } from '@main/utils' +import type { BrowserWindow } from 'electron' + +export class Event extends EventEmitter { + // closeAll() { + // this.emit(COMMON_EVENT_NAME.closeAll) + // } + // initSetting() { + // this.emit(COMMON_EVENT_NAME.initConfig) + // // this.configStatus(null) + // } + + /** + * 初始化APP + */ + app_inited() { + this.emit('app_inited') + } + + /** + * 已更新的配置 + * @param keys 已更新配置的key + * @param setting 已更新配置 + */ + updated_config(keys: Array, setting: Partial) { + this.emit('updated_config', keys, setting) + } + + /** + * 更新配置 + * @param setting 新设置 + */ + update_config(setting: Partial) { + const { setting: newSetting, updatedSettingKeys, updatedSetting } = updateSetting(setting) + global.lx.appSetting = newSetting + if (!updatedSettingKeys.length) return + this.emit('update_config', newSetting) + // console.log(updatedSetting) + this.updated_config(updatedSettingKeys, updatedSetting) + } + + system_theme_change(isDark: boolean) { + this.emit('system_theme_change', isDark) + } + + theme_change() { + this.emit('theme_change') + } + + deeplink(link: string) { + this.emit('deeplink', link) + } + + player_status(status: Partial) { + for (const [key, value] of Object.entries(status)) { + // @ts-expect-error + global.lx.player_status[key] = value + } + this.emit('player_status', status) + } + + hot_key_down(keyInfo: LX.HotKeyDownInfo) { + this.emit('hot_key_down', keyInfo) + } + + hot_key_config_update(config: LX.HotKeyConfigAll) { + saveAppHotKeyConfig(config) + this.emit('hot_key_config_update', config) + } + + main_window_created(win: BrowserWindow) { + this.emit('main_window_created', win) + } + + main_window_ready_to_show() { + this.emit('main_window_ready_to_show') + } + + main_window_inited() { + this.emit('main_window_inited') + } + + main_window_show() { + this.emit('main_window_show') + } + + main_window_hide() { + this.emit('main_window_hide') + } + + main_window_focus() { + this.emit('main_window_focus') + } + + main_window_blur() { + this.emit('main_window_blur') + } + + main_window_close() { + this.emit('main_window_close') + } + + main_window_fullscreen(isFullscreen: boolean) { + this.emit('main_window_fullscreen', isFullscreen) + } + + desktop_lyric_window_created(win: BrowserWindow) { + this.emit('desktop_lyric_window_created', win) + } +} + + +type EventMethods = Omit +declare class EventType extends Event { + on(event: K, listener: EventMethods[K]): this + once(event: K, listener: EventMethods[K]): this + off(event: K, listener: EventMethods[K]): this +} + +export type Type = Omit> diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/event/DislikeEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/event/DislikeEvent.ts new file mode 100644 index 000000000..c22734993 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/event/DislikeEvent.ts @@ -0,0 +1,56 @@ +import { EventEmitter } from 'events' + + +export class Event extends EventEmitter { + dislike_changed() { + this.emit('dislike_changed') + } + + /** + * 覆盖整个列表数据 + * @param dislikeData 列表数据 + * @param isRemote 是否属于远程操作 + */ + async dislike_data_overwrite(dislikeData: LX.Dislike.DislikeRules, isRemote: boolean = false) { + await global.lx.worker.dbService.dislikeInfoOverwrite(dislikeData) + this.emit('dislike_data_overwrite', dislikeData, isRemote) + this.dislike_changed() + } + + /** + * 批量添加歌曲到列表 + * @param dislikeId 列表id + * @param musicInfos 添加的歌曲信息 + * @param addMusicLocationType 添加在到列表的位置 + * @param isRemote 是否属于远程操作 + */ + async dislike_music_add(musicInfo: LX.Dislike.DislikeMusicInfo[], isRemote: boolean = false) { + // const changedIds = + await global.lx.worker.dbService.dislikeInfoAdd(musicInfo) + // await checkUpdateDislike(changedIds) + this.emit('dislike_music_add', musicInfo, isRemote) + this.dislike_changed() + } + + /** + * 清空列表内的歌曲 + * @param ids 列表Id + * @param isRemote 是否属于远程操作 + */ + async dislike_music_clear(isRemote: boolean = false) { + // const changedIds = + await global.lx.worker.dbService.dislikeInfoOverwrite('') + // await checkUpdateDislike(changedIds) + this.emit('dislike_music_clear', isRemote) + this.dislike_changed() + } +} + + +type EventMethods = Omit +declare class EventType extends Event { + on(event: K, listener: EventMethods[K]): this + once(event: K, listener: EventMethods[K]): this + off(event: K, listener: EventMethods[K]): this +} +export type Type = Omit> diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/event/ListEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/event/ListEvent.ts new file mode 100644 index 000000000..28e55b30a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/event/ListEvent.ts @@ -0,0 +1,188 @@ +import { EventEmitter } from 'events' +// import { +// // getAllUserList as getAllUserListByDB, +// createUserLists, +// removeUserLists, +// updateUserLists, +// updateUserListsPosition, +// musicsAdd, +// musicsMove, +// musicsRemove, +// musicsUpdate, +// musicsClear, +// musicsPositionUpdate, +// musicOverwrite, +// } from '@main/workers/dbService/modules/list' + +// 兼容v2.3.0之前版本插入数字类型的ID导致其意外在末尾追加 .0 的问题,确保所有ID都是字符串类型 +const fixListIdType = (lists: LX.List.UserListInfo[] | LX.List.UserListInfoFull[]) => { + for (const list of lists) { + if (typeof list.sourceListId == 'number') { + list.sourceListId = String(list.sourceListId) + if (typeof list.id == 'number') { + list.id = String(list.id) + } + } + } +} + +export class Event extends EventEmitter { + list_changed() { + this.emit('list_changed') + } + + /** + * 覆盖整个列表数据 + * @param listData 列表数据 + * @param isRemote 是否属于远程操作 + */ + async list_data_overwrite(listData: MakeOptional, isRemote: boolean = false) { + fixListIdType(listData.userList) + await global.lx.worker.dbService.listDataOverwrite(listData) + this.emit('list_data_overwrite', listData, isRemote) + this.list_changed() + } + + /** + * 批量创建列表 + * @param position 列表位置 + * @param lists 列表信息 + * @param isRemote 是否属于远程操作 + */ + async list_create(position: number, lists: LX.List.UserListInfo[], isRemote: boolean = false) { + fixListIdType(lists) + await global.lx.worker.dbService.createUserLists(position, lists) + this.emit('list_create', position, lists, isRemote) + this.list_changed() + } + + /** + * 批量删除列表及列表内歌曲 + * @param ids 列表ids + * @param isRemote 是否属于远程操作 + */ + async list_remove(ids: string[], isRemote: boolean = false) { + await global.lx.worker.dbService.removeUserLists(ids) + this.emit('list_remove', ids, isRemote) + this.list_changed() + } + + /** + * 批量更新列表信息 + * @param lists 列表信息 + * @param isRemote 是否属于远程操作 + */ + async list_update(lists: LX.List.UserListInfo[], isRemote: boolean = false) { + await global.lx.worker.dbService.updateUserLists(lists) + this.emit('list_update', lists, isRemote) + this.list_changed() + } + + /** + * 批量更新列表位置 + * @param position 列表位置 + * @param ids 列表ids + * @param isRemote 是否属于远程操作 + */ + async list_update_position(position: number, ids: string[], isRemote: boolean = false) { + await global.lx.worker.dbService.updateUserListsPosition(position, ids) + this.emit('list_update_position', position, ids, isRemote) + this.list_changed() + } + + /** + * 覆盖列表内歌曲 + * @param listId 列表id + * @param musicInfos 音乐信息 + * @param isRemote 是否属于远程操作 + */ + async list_music_overwrite(listId: string, musicInfos: LX.Music.MusicInfo[], isRemote: boolean = false) { + await global.lx.worker.dbService.musicOverwrite(listId, musicInfos) + this.emit('list_music_overwrite', listId, musicInfos, isRemote) + this.list_changed() + } + + /** + * 批量添加歌曲到列表 + * @param listId 列表id + * @param musicInfos 添加的歌曲信息 + * @param addMusicLocationType 添加在到列表的位置 + * @param isRemote 是否属于远程操作 + */ + async list_music_add(listId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) { + await global.lx.worker.dbService.musicsAdd(listId, musicInfos, addMusicLocationType) + this.emit('list_music_add', listId, musicInfos, addMusicLocationType, isRemote) + this.list_changed() + } + + /** + * 批量移动歌曲 + * @param fromId 源列表id + * @param toId 目标列表id + * @param musicInfos 移动的歌曲信息 + * @param addMusicLocationType 添加在到列表的位置 + * @param isRemote 是否属于远程操作 + */ + async list_music_move(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) { + await global.lx.worker.dbService.musicsMove(fromId, toId, musicInfos, addMusicLocationType) + this.emit('list_music_move', fromId, toId, musicInfos, addMusicLocationType, isRemote) + this.list_changed() + } + + /** + * 批量移除歌曲 + * @param listId + * @param listId 列表Id + * @param ids 要删除歌曲的id + * @param isRemote 是否属于远程操作 + */ + async list_music_remove(listId: string, ids: string[], isRemote: boolean = false) { + await global.lx.worker.dbService.musicsRemove(listId, ids) + this.emit('list_music_remove', listId, ids, isRemote) + this.list_changed() + } + + /** + * 批量更新歌曲信息 + * @param musicInfos 歌曲&列表信息 + * @param isRemote 是否属于远程操作 + */ + async list_music_update(musicInfos: LX.List.ListActionMusicUpdate, isRemote: boolean = false) { + await global.lx.worker.dbService.musicsUpdate(musicInfos) + this.emit('list_music_update', musicInfos, isRemote) + this.list_changed() + } + + /** + * 清空列表内的歌曲 + * @param ids 列表Id + * @param isRemote 是否属于远程操作 + */ + async list_music_clear(ids: string[], isRemote: boolean = false) { + await global.lx.worker.dbService.musicsClear(ids) + this.emit('list_music_clear', ids, isRemote) + this.list_changed() + } + + /** + * 批量更新歌曲位置 + * @param listId 列表ID + * @param position 新位置 + * @param ids 歌曲id + * @param isRemote 是否属于远程操作 + */ + async list_music_update_position(listId: string, position: number, ids: string[], isRemote: boolean = false) { + await global.lx.worker.dbService.musicsPositionUpdate(listId, position, ids) + this.emit('list_music_update_position', listId, position, ids, isRemote) + this.list_changed() + } +} + + +type EventMethods = Omit +declare class EventType extends Event { + on(event: K, listener: EventMethods[K]): this + once(event: K, listener: EventMethods[K]): this + off(event: K, listener: EventMethods[K]): this +} +export type Type = Omit> diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/event/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/event/index.ts new file mode 100644 index 000000000..63a36f6fb --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/event/index.ts @@ -0,0 +1,22 @@ +import { Event as App, type Type as AppType } from './AppEvent' +import { Event as List, type Type as ListType } from './ListEvent' +import { Event as Dislike, type Type as DislikeType } from './DislikeEvent' + +export type { + AppType, + ListType, + DislikeType, +} + +export const createAppEvent = (): AppType => { + return new App() +} + +export const createListEvent = (): ListType => { + return new List() +} + +export const createDislikeEvent = (): DislikeType => { + return new Dislike() +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/index-dev.ts b/plugins/lx-music-ztools/vendor/lx/src/main/index-dev.ts new file mode 100644 index 000000000..f6ebaab47 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/index-dev.ts @@ -0,0 +1,44 @@ +/** + * This file is used specifically and only for development. It installs + * `electron-debug` & `vue-devtools`. There shouldn't be any need to + * modify this file, but it can be used to extend your development + * environment. + */ + +import { app } from 'electron' +import electronDebug from 'electron-debug' +import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' +import { openDevTools } from './utils' +// Install `electron-debug` with `devtron` +electronDebug({ + showDevTools: false, + devToolsMode: 'undocked', +}) + +// Install `vue-devtools` +app.on('ready', () => { + global.lx.event_app.on('main_window_created', (win) => { + openDevTools(win.webContents) + installExtension(VUEJS_DEVTOOLS, { session: win.webContents.session }) + .then((name: string) => { + console.log(`[main window] Added Extension: ${name}`) + }) + .catch((err: Error) => { + console.log('[main window] An error occurred: ', err) + }) + }) + global.lx.event_app.on('desktop_lyric_window_created', (win) => { + openDevTools(win.webContents) + installExtension(VUEJS_DEVTOOLS, { session: win.webContents.session }) + .then((name: string) => { + console.log(`[lyric window] Added Extension: ${name}`) + }) + .catch((err: Error) => { + console.log('[lyric window] An error occurred: ', err) + }) + }) +}) + +// Require `main` process to boot app +require('./index') + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/index.ts new file mode 100644 index 000000000..318846b23 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/index.ts @@ -0,0 +1,37 @@ +import { app } from 'electron' +import './utils/logInit' +import '@common/error' +import { + initGlobalData, + initSingleInstanceHandle, + applyElectronEnvParams, + setUserDataPath, + registerDeeplink, + listenerAppEvent, +} from './app' +import { isLinux } from '@common/utils' +import { initAppSetting } from '@main/app' +import registerModules from '@main/modules' + +// 初始化应用 +const init = () => { + console.log('init') + if (process.env.BUILD_WIN7 == 'true') import('./utils/winLegacy') + void initAppSetting().then(() => { + registerModules() + global.lx.event_app.app_inited() + }) +} + +initGlobalData() +initSingleInstanceHandle() +applyElectronEnvParams() +setUserDataPath() +registerDeeplink(init) +listenerAppEvent(init) + + +// https://github.com/electron/electron/issues/16809 +void app.whenReady().then(() => { + isLinux ? setTimeout(init, 300) : init() +}) diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/appMenu.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/appMenu.ts new file mode 100644 index 000000000..b7297d85b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/appMenu.ts @@ -0,0 +1,51 @@ +import { app, Menu } from 'electron' +import { isMac } from '@common/utils' + + +export default () => { + if (isMac) { + const template: Electron.MenuItemConstructorOptions[] = [ + { + label: app.getName(), + submenu: [ + { label: '关于洛雪音乐', role: 'about' }, + { type: 'separator' }, + { label: '隐藏', role: 'hide' }, + { type: 'separator' }, + { + label: '退出', + accelerator: 'Command+Q', + click() { + global.lx.isSkipTrayQuit = true + app.quit() + }, + }, + ], + }, + { + label: '窗口', + role: 'window', + submenu: [ + { label: '最小化', role: 'minimize' }, + { label: '关闭', role: 'close', accelerator: 'Command+W' }, + ], + }, + { + label: '编辑', + submenu: [ + { label: '撤销', accelerator: 'Command+Z', role: 'undo' }, + { label: '恢复', accelerator: 'Shift+Command+Z', role: 'redo' }, + { type: 'separator' }, + { label: '剪切', accelerator: 'Command+X', role: 'cut' }, + { label: '复制', accelerator: 'Command+C', role: 'copy' }, + { label: '粘贴', accelerator: 'Command+V', role: 'paste' }, + { label: '选择全部', accelerator: 'Command+A', role: 'selectAll' }, + ], + }, + ] + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)) + } else { + Menu.setApplicationMenu(null) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/common/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/common/index.ts new file mode 100644 index 000000000..e70df1469 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/common/index.ts @@ -0,0 +1,3 @@ +export { registerRendererEvents } from './winRendererEvent' + +export { default } from './rendererEvent' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/common/rendererEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/common/rendererEvent.ts new file mode 100644 index 000000000..35ed0530d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/common/rendererEvent.ts @@ -0,0 +1,26 @@ +import { mainHandle, mainOn } from '@common/mainIpc' +import { CMMON_EVENT_NAME } from '@common/ipcNames' +import { getFonts } from '@main/utils/fontManage' + +// 公共操作事件(公共,只注册一次) +export default () => { + mainHandle(CMMON_EVENT_NAME.get_app_setting, async() => { + return global.lx.appSetting + }) + mainHandle>(CMMON_EVENT_NAME.set_app_setting, async({ params: config }) => { + global.lx.event_app.update_config(config) + }) + + mainHandle(CMMON_EVENT_NAME.get_env_params, async() => { + return global.envParams + }) + + mainOn(CMMON_EVENT_NAME.clear_env_params_deeplink, () => { + global.envParams.deeplink = null + }) + + mainHandle(CMMON_EVENT_NAME.get_system_fonts, async() => { + return getFonts() + }) +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/common/winRendererEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/common/winRendererEvent.ts new file mode 100644 index 000000000..9b0ad3713 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/common/winRendererEvent.ts @@ -0,0 +1,21 @@ +import { CMMON_EVENT_NAME } from '@common/ipcNames' + +// 发送列表操作事件到渲染进程的注册方法 +// 哪个渲染进程需要接收则引入此方法注册 +export const registerRendererEvents = (sendEvent: (name: string, params?: T | undefined) => void) => { + const sendDeeplink = (link: string) => { + sendEvent(CMMON_EVENT_NAME.deeplink, link) + } + const sendSystemThemeChange = () => { + sendEvent(CMMON_EVENT_NAME.theme_change, global.lx.theme) + } + + global.lx.event_app.on('deeplink', sendDeeplink) + global.lx.event_app.on('theme_change', sendSystemThemeChange) + + return () => { + global.lx.event_app.off('deeplink', sendDeeplink) + global.lx.event_app.off('theme_change', sendSystemThemeChange) + } +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/dislike/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/dislike/index.ts new file mode 100644 index 000000000..e70df1469 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/dislike/index.ts @@ -0,0 +1,3 @@ +export { registerRendererEvents } from './winRendererEvent' + +export { default } from './rendererEvent' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/dislike/rendererEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/dislike/rendererEvent.ts new file mode 100644 index 000000000..19bc3ef24 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/dislike/rendererEvent.ts @@ -0,0 +1,18 @@ +import { mainHandle } from '@common/mainIpc' +import { DISLIKE_EVENT_NAME } from '@common/ipcNames' + +// 列表操作事件(公共,只注册一次) +export default () => { + mainHandle(DISLIKE_EVENT_NAME.get_dislike_music_infos, async() => { + return global.lx.worker.dbService.getDislikeListInfo() + }) + mainHandle(DISLIKE_EVENT_NAME.add_dislike_music_infos, async({ params: listData }) => { + await global.lx.event_dislike.dislike_music_add(listData, false) + }) + mainHandle(DISLIKE_EVENT_NAME.overwrite_dislike_music_infos, async({ params: rules }) => { + await global.lx.event_dislike.dislike_data_overwrite(rules, false) + }) + mainHandle(DISLIKE_EVENT_NAME.clear_dislike_music_infos, async() => { + await global.lx.event_dislike.dislike_music_clear(false) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/dislike/winRendererEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/dislike/winRendererEvent.ts new file mode 100644 index 000000000..c477b19c8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/dislike/winRendererEvent.ts @@ -0,0 +1,24 @@ +import { DISLIKE_EVENT_NAME } from '@common/ipcNames' + +// 发送列表操作事件到渲染进程的注册方法 +// 哪个渲染进程需要接收则引入此方法注册 +export const registerRendererEvents = (sendEvent: (name: string, params?: T | undefined) => void) => { + const dislike_music_add = async(listData: LX.Dislike.DislikeMusicInfo[]) => { + sendEvent(DISLIKE_EVENT_NAME.add_dislike_music_infos, listData) + } + const dislike_data_overwrite = async(rules: LX.Dislike.DislikeRules) => { + sendEvent(DISLIKE_EVENT_NAME.overwrite_dislike_music_infos, rules) + } + const dislike_music_clear = async() => { + sendEvent(DISLIKE_EVENT_NAME.clear_dislike_music_infos) + } + global.lx.event_dislike.on('dislike_music_add', dislike_music_add) + global.lx.event_dislike.on('dislike_data_overwrite', dislike_data_overwrite) + global.lx.event_dislike.on('dislike_music_clear', dislike_music_clear) + + return () => { + global.lx.event_dislike.off('dislike_music_add', dislike_music_add) + global.lx.event_dislike.off('dislike_data_overwrite', dislike_data_overwrite) + global.lx.event_dislike.off('dislike_music_clear', dislike_music_clear) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/index.ts new file mode 100644 index 000000000..f2eadbde4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/index.ts @@ -0,0 +1,9 @@ +import common from './common' +import list from './list' +import dislike from './dislike' + +export default () => { + common() + list() + dislike() +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/list/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/list/index.ts new file mode 100644 index 000000000..e70df1469 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/list/index.ts @@ -0,0 +1,3 @@ +export { registerRendererEvents } from './winRendererEvent' + +export { default } from './rendererEvent' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/list/rendererEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/list/rendererEvent.ts new file mode 100644 index 000000000..185b38a5d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/list/rendererEvent.ts @@ -0,0 +1,54 @@ +import { mainHandle } from '@common/mainIpc' +import { PLAYER_EVENT_NAME } from '@common/ipcNames' + +// 列表操作事件(公共,只注册一次) +export default () => { + mainHandle(PLAYER_EVENT_NAME.list_get, async() => { + return global.lx.worker.dbService.getAllUserList() + }) + mainHandle(PLAYER_EVENT_NAME.list_data_overwire, async({ params: listData }) => { + await global.lx.event_list.list_data_overwrite(listData, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_add, async({ params: { position, listInfos } }) => { + await global.lx.event_list.list_create(position, listInfos, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_remove, async({ params: ids }) => { + await global.lx.event_list.list_remove(ids, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_update, async({ params: listInfos }) => { + await global.lx.event_list.list_update(listInfos, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_update_position, async({ params: { position, ids } }) => { + await global.lx.event_list.list_update_position(position, ids, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_music_get, async({ params: listId }) => { + return global.lx.worker.dbService.getListMusics(listId) + }) + mainHandle(PLAYER_EVENT_NAME.list_music_add, async({ params: { id, musicInfos, addMusicLocationType } }) => { + await global.lx.event_list.list_music_add(id, musicInfos, addMusicLocationType, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_music_move, async({ params: { fromId, toId, musicInfos, addMusicLocationType } }) => { + await global.lx.event_list.list_music_move(fromId, toId, musicInfos, addMusicLocationType, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_music_remove, async({ params: { listId, ids } }) => { + await global.lx.event_list.list_music_remove(listId, ids, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_music_update, async({ params: musicInfos }) => { + await global.lx.event_list.list_music_update(musicInfos, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_music_update_position, async({ params: { listId, position, ids } }) => { + await global.lx.event_list.list_music_update_position(listId, position, ids, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_music_overwrite, async({ params: { listId, musicInfos } }) => { + await global.lx.event_list.list_music_overwrite(listId, musicInfos, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_music_clear, async({ params: listId }) => { + await global.lx.event_list.list_music_clear(listId, false) + }) + mainHandle(PLAYER_EVENT_NAME.list_music_check_exist, async({ params: { listId, musicInfoId } }) => { + return global.lx.worker.dbService.checkListExistMusic(listId, musicInfoId) + }) + mainHandle(PLAYER_EVENT_NAME.list_music_get_list_ids, async({ params: musicInfoId }) => { + return global.lx.worker.dbService.getMusicExistListIds(musicInfoId) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/list/winRendererEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/list/winRendererEvent.ts new file mode 100644 index 000000000..106ce5eea --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/commonRenderers/list/winRendererEvent.ts @@ -0,0 +1,69 @@ +import { PLAYER_EVENT_NAME } from '@common/ipcNames' + +// 发送列表操作事件到渲染进程的注册方法 +// 哪个渲染进程需要接收则引入此方法注册 +export const registerRendererEvents = (sendEvent: (name: string, params?: T | undefined) => void) => { + const list_data_overwrite = async(listData: LX.List.ListActionDataOverwrite) => { + sendEvent(PLAYER_EVENT_NAME.list_data_overwire, listData) + } + const list_create = async(position: number, listInfos: LX.List.UserListInfo[]) => { + sendEvent(PLAYER_EVENT_NAME.list_add, { position, listInfos }) + } + const list_remove = async(ids: string[]) => { + sendEvent(PLAYER_EVENT_NAME.list_remove, ids) + } + const list_update = async(lists: LX.List.UserListInfo[]) => { + sendEvent(PLAYER_EVENT_NAME.list_update, lists) + } + const list_update_position = async(position: number, ids: string[]) => { + sendEvent(PLAYER_EVENT_NAME.list_update_position, { position, ids }) + } + const list_music_add = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType) => { + sendEvent(PLAYER_EVENT_NAME.list_music_add, { id, musicInfos, addMusicLocationType }) + } + const list_music_move = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType) => { + sendEvent(PLAYER_EVENT_NAME.list_music_move, { fromId, toId, musicInfos, addMusicLocationType }) + } + const list_music_remove = async(listId: string, ids: string[]) => { + sendEvent(PLAYER_EVENT_NAME.list_music_remove, { listId, ids }) + } + const list_music_update = async(musicInfos: LX.List.ListActionMusicUpdate) => { + sendEvent(PLAYER_EVENT_NAME.list_music_update, musicInfos) + } + const list_music_update_position = async(listId: string, position: number, ids: string[]) => { + sendEvent(PLAYER_EVENT_NAME.list_music_update_position, { listId, position, ids }) + } + const list_music_overwrite = async(listId: string, musicInfos: LX.Music.MusicInfo[]) => { + sendEvent(PLAYER_EVENT_NAME.list_music_overwrite, { listId, musicInfos }) + } + const list_music_clear = async(ids: string[]) => { + sendEvent(PLAYER_EVENT_NAME.list_data_overwire, ids) + } + global.lx.event_list.on('list_data_overwrite', list_data_overwrite) + global.lx.event_list.on('list_create', list_create) + global.lx.event_list.on('list_remove', list_remove) + global.lx.event_list.on('list_update', list_update) + global.lx.event_list.on('list_update_position', list_update_position) + global.lx.event_list.on('list_music_add', list_music_add) + global.lx.event_list.on('list_music_move', list_music_move) + global.lx.event_list.on('list_music_remove', list_music_remove) + global.lx.event_list.on('list_music_update', list_music_update) + global.lx.event_list.on('list_music_update_position', list_music_update_position) + global.lx.event_list.on('list_music_overwrite', list_music_overwrite) + global.lx.event_list.on('list_music_clear', list_music_clear) + + return () => { + global.lx.event_list.off('list_data_overwrite', list_data_overwrite) + global.lx.event_list.off('list_create', list_create) + global.lx.event_list.off('list_remove', list_remove) + global.lx.event_list.off('list_update', list_update) + global.lx.event_list.off('list_update_position', list_update_position) + global.lx.event_list.off('list_music_add', list_music_add) + global.lx.event_list.off('list_music_move', list_music_move) + global.lx.event_list.off('list_music_remove', list_music_remove) + global.lx.event_list.off('list_music_update', list_music_update) + global.lx.event_list.off('list_music_update_position', list_music_update_position) + global.lx.event_list.off('list_music_overwrite', list_music_overwrite) + global.lx.event_list.off('list_music_clear', list_music_clear) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/hotKey/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/hotKey/index.ts new file mode 100644 index 000000000..a4c2a9b4d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/hotKey/index.ts @@ -0,0 +1,9 @@ +import { app } from 'electron' +import { unRegisterHotkeyAll } from './utils' + +import init from './rendererEvent' + +export default () => { + app.on('will-quit', unRegisterHotkeyAll) + init() +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/hotKey/rendererEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/hotKey/rendererEvent.ts new file mode 100644 index 000000000..70c8e1169 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/hotKey/rendererEvent.ts @@ -0,0 +1,32 @@ +import { HOTKEY_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { mainHandle } from '@common/mainIpc' +import { init, registerHotkey, unRegisterHotkey, unRegisterHotkeyAll } from './utils' + + +export default () => { + mainHandle(HOTKEY_RENDERER_EVENT_NAME.set_config, async({ params }) => { + switch (params.action) { + case 'config': + // global.lx.event_app.saveConfig(data, source) + global.lx.event_app.hot_key_config_update(params.data) + return true + case 'enable': + global.lx.hotKey.enable = params.data + params.data ? init(true) : unRegisterHotkeyAll() + return true + case 'register': + return registerHotkey(params.data) + case 'unregister': + unRegisterHotkey(params.data) + return true + } + }) + + mainHandle(HOTKEY_RENDERER_EVENT_NAME.status, async() => global.lx.hotKey.state) + + mainHandle(HOTKEY_RENDERER_EVENT_NAME.enable, async({ params: flag }) => { + flag ? init() : unRegisterHotkeyAll() + }) + + init() +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/hotKey/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/hotKey/utils.ts new file mode 100644 index 000000000..807aec590 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/hotKey/utils.ts @@ -0,0 +1,69 @@ +import { globalShortcut } from 'electron' +import { log } from '@common/utils' + +export const handleKeyDown = (key: string) => { + if (!global.lx.hotKey.enable) return + global.lx.event_app.hot_key_down({ type: 'global', key }) +} + +const transformedKeyRxp = /(^|\+)[a-z]/g + +export const transformedKey = (key: string): string => { + if (key.includes('arrow')) key = key.replace(/arrow/g, '') + return key.replace('mod', 'CommandOrControl').replace(transformedKeyRxp, l => l.toUpperCase()) +} + +export const registerHotkey = ({ key, info }: LX.RegisterKeyInfo): boolean => { + let targetKey = global.lx.hotKey.state.get(key) + if (targetKey?.status) return true + const transKey = transformedKey(key) + // console.log('Register key:', transKey) + if (targetKey) { + targetKey.info = info + } else { + targetKey = { + status: false, + info, + } + global.lx.hotKey.state.set(key, targetKey) + } + const status = targetKey.status = globalShortcut.isRegistered(transKey) + ? false + : globalShortcut.register(transKey, () => { + handleKeyDown(key) + }) + return status +} + +export const unRegisterHotkey = (key: string) => { + let transKey = transformedKey(key) + // console.log('Unregister key:', transKey) + globalShortcut.unregister(transKey) + global.lx.hotKey.state.delete(key) +} + +export const unRegisterHotkeyAll = () => { + global.lx.hotKey.state.clear() + globalShortcut.unregisterAll() +} + + +const handleRegisterHotkey = (data: LX.RegisterKeyInfo) => { + let ret = registerHotkey(data) + if (!ret) log.info('Register hot key failed:', data.key) +} + + +export const init = (isForce = false) => { + unRegisterHotkeyAll() + if (!isForce && !global.lx.hotKey.config.global.enable) return + // global.lx.hotKey.state = {} + // console.log(global.lx.hotKey.config.global.keys) + for (const key of Object.keys(global.lx.hotKey.config.global.keys)) { + try { + handleRegisterHotkey({ key, info: global.lx.hotKey.config.global.keys[key] }) + } catch (err) { + log.info(err) + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/index.ts new file mode 100644 index 000000000..e95d1713c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/index.ts @@ -0,0 +1,20 @@ +import registerUserApi from './userApi' +import registerWinMain from './winMain' +import registerHotKey from './hotKey' +import registerTray from './tray' +import registerAppMenu from './appMenu' +import registerWinLyric from './winLyric' +import registerCommonRenderers from './commonRenderers' + +let isRegistered = false +export default () => { + if (isRegistered) return + registerUserApi() + registerCommonRenderers() + registerWinMain() + registerHotKey() + registerTray() + registerAppMenu() + registerWinLyric() + isRegistered = true +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/openApi/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/openApi/index.ts new file mode 100644 index 000000000..866da4a44 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/openApi/index.ts @@ -0,0 +1,297 @@ +import http from 'node:http' +import querystring from 'node:querystring' +import type { Socket } from 'node:net' +import { getAddress } from '@common/utils/nodejs' +import { sendTaskbarButtonClick } from '@main/modules/winMain' + +const sendResponse = (res: http.ServerResponse, code = 200, msg: string | Record = 'OK', contentType = 'text/plain; charset=utf-8') => { + res.writeHead(code, { + 'Content-Type': contentType, + 'Access-Control-Allow-Origin': '*', + }) + if (typeof msg === 'object') { + res.end(JSON.stringify(msg)) + } else { + res.end(msg) + } +} + +let status: LX.OpenAPI.Status = { + status: false, + message: '', + address: '', +} + +type SubscribeKeys = keyof LX.Player.Status + +let httpServer: http.Server +let sockets = new Set() +let responses = new Map, SubscribeKeys[]>() +let playerStatusKeys: SubscribeKeys[] + +const defaultFilter = [ + 'status', + 'name', + 'singer', + 'albumName', + 'lyricLineText', + 'duration', + 'progress', + 'playbackRate', +] satisfies SubscribeKeys[] + +const parseFilter = (filter: any) => { + if (typeof filter != 'string') return defaultFilter + filter = filter.split(',') + const subKeys = playerStatusKeys.filter(k => filter.includes(k)) + return subKeys.length ? subKeys : defaultFilter +} +const handleSendStatus = (res: http.ServerResponse, query?: string) => { + const keys = parseFilter(querystring.parse(query ?? '').filter) + const resp: Partial> = {} + for (const k of keys) resp[k] = global.lx.player_status[k] + sendResponse(res, 200, resp, 'application/json; charset=utf-8') +} +const handleSendAllLyric = (res: http.ServerResponse) => { + const resp: Partial> = { + lyric: global.lx.player_status.lyric, + tlyric: global.lx.player_status.tlyric, + rlyric: global.lx.player_status.rlyric, + lxlyric: global.lx.player_status.lxlyric, + } + sendResponse(res, 200, resp, 'application/json; charset=utf-8') +} +const handleSubscribePlayerStatus = (req: http.IncomingMessage, res: http.ServerResponse, query?: string) => { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + Connection: 'keep-alive', + 'Cache-Control': 'no-cache', + 'Access-Control-Allow-Origin': '*', + }) + req.socket.setTimeout(0) + req.on('close', () => { + res.end('OK') + responses.delete(res) + }) + const keys = parseFilter(querystring.parse(query ?? '').filter) + responses.set(res, keys) + for (const [k, v] of Object.entries(global.lx.player_status)) { + if (!keys.includes(k as SubscribeKeys)) continue + res.write(`event: ${k}\n`) + res.write(`data: ${JSON.stringify(v)}\n\n`) + } +} + +const handleStartServer = async(port: number, ip: string) => new Promise((resolve, reject) => { + playerStatusKeys = Object.keys(global.lx.player_status) as SubscribeKeys[] + httpServer = http.createServer((req, res): void => { + const [endUrl, query] = `/${req.url?.split('/').at(-1) ?? ''}`.split('?') + let code = 200 + let msg = 'OK' + switch (endUrl) { + case '/status': + handleSendStatus(res, query) + return + // case '/test': + // code = 200 + // res.setHeader('Content-Type', 'text/html; charset=utf-8') + // msg = ` + // + // + // + // + // + // Nodejs Server-Sent Events + // + // + //

Hello SSE!

+ + //

List of Server-sent events

+ //
    + + // + // + // ` + // break + case '/lyric': + msg = global.lx.player_status.lyric + break + case '/lyric-all': + handleSendAllLyric(res) + return + case '/play': + sendTaskbarButtonClick('play') + break + case '/pause': + sendTaskbarButtonClick('pause') + break + case '/skip-next': + sendTaskbarButtonClick('next') + break + case '/skip-prev': + sendTaskbarButtonClick('prev') + break + case '/seek': { + const offset = parseFloat(querystring.parse(query ?? '').offset as string) + if (Number.isNaN(offset) || offset < 0 || offset > global.lx.player_status.duration) { + code = 400 + msg = 'Invalid offset' + } else { + sendTaskbarButtonClick('seek', parseFloat(offset.toFixed(3))) + } + break + } + case '/collect': + sendTaskbarButtonClick('collect') + break + case '/uncollect': + sendTaskbarButtonClick('unCollect') + break + case '/volume': { + const volume = parseInt(querystring.parse(query ?? '').volume as string) + if (Number.isNaN(volume) || volume < 0 || volume > 100) { + code = 400 + msg = 'Invalid volume' + } else { + sendTaskbarButtonClick('volume', volume / 100) + } + break + } + case '/mute': { + const mute = querystring.parse(query ?? '').mute + if (mute == 'true') { + sendTaskbarButtonClick('mute', true) + } else if (mute == 'false') { + sendTaskbarButtonClick('mute', false) + } else { + code = 400 + msg = 'Invalid mute value' + } + break + } + case '/subscribe-player-status': + try { + handleSubscribePlayerStatus(req, res, query) + return + } catch (err) { + console.log(err) + code = 500 + msg = 'Error' + } + break + default: + code = 401 + msg = 'Forbidden' + break + } + sendResponse(res, code, msg) + }) + httpServer.on('error', error => { + console.log(error) + reject(error) + }) + httpServer.on('connection', (socket) => { + sockets.add(socket) + socket.once('close', () => { + sockets.delete(socket) + }) + socket.setTimeout(4000) + }) + + httpServer.on('listening', () => { + const addr = httpServer.address() + // console.log(addr) + if (!addr) { + reject(new Error('address is null')) + return + } + resolve() + }) + httpServer.listen(port, ip) +}) + +const handleStopServer = async() => new Promise((resolve, reject) => { + if (!httpServer) return + httpServer.close((err) => { + if (err) { + reject(err) + return + } + resolve() + }) + for (const socket of sockets) socket.destroy() + sockets.clear() + responses.clear() +}) + + +const sendStatus = (status: Partial) => { + if (!responses.size) return + for (const [resp, keys] of responses) { + for (const [k, v] of Object.entries(status)) { + if (!keys.includes(k as SubscribeKeys)) continue + resp.write(`event: ${k}\n`) + resp.write(`data: ${JSON.stringify(v)}\n\n`) + } + } +} +export const stopServer = async() => { + global.lx.event_app.off('player_status', sendStatus) + if (!status.status) { + status.status = false + status.message = '' + status.address = '' + return status + } + await handleStopServer().then(() => { + status.status = false + status.message = '' + status.address = '' + }).catch(err => { + console.log(err) + status.message = err.message + }) + return status +} +export const startServer = async(port: number, bindLan: boolean) => { + if (status.status) await stopServer() + await handleStartServer(port, bindLan ? '0.0.0.0' : '127.0.0.1').then(() => { + status.status = true + status.message = '' + let address = ['127.0.0.1'] + if (bindLan) address = [...address, ...getAddress()] + status.address = address.join(', ') + }).catch(err => { + console.log(err) + status.status = false + status.message = err.message + status.address = '' + }) + global.lx.event_app.on('player_status', sendStatus) + return status +} + +export const getStatus = (): LX.OpenAPI.Status => status diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/auth.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/auth.ts new file mode 100644 index 000000000..429721444 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/auth.ts @@ -0,0 +1,94 @@ +import { request, generateRsaKey } from './utils' +import { getSyncAuthKey, setSyncAuthKey } from './data' +import log from '../log' +import { aesDecrypt, aesEncrypt, getComputerName, rsaDecrypt } from '../utils' +import { toMD5 } from '@common/utils/nodejs' +import { SYNC_CODE } from '@common/constants_sync' + + +const hello = async(urlInfo: LX.Sync.Client.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/hello`) + .then(({ text }) => text == SYNC_CODE.helloMsg) + .catch((err: any) => { + log.error('[auth] hello', err.message) + console.log(err) + return false + }) + +const getServerId = async(urlInfo: LX.Sync.Client.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/id`) + .then(({ text }) => { + if (!text.startsWith(SYNC_CODE.idPrefix)) return '' + return text.replace(SYNC_CODE.idPrefix, '') + }) + .catch((err: any) => { + log.error('[auth] getServerId', err.message) + console.log(err) + throw err + }) + +const codeAuth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCode: string) => { + let key = toMD5(authCode).substring(0, 16) + // const iv = Buffer.from(key.split('').reverse().join('')).toString('base64') + key = Buffer.from(key).toString('base64') + let { publicKey, privateKey } = await generateRsaKey() + publicKey = publicKey.replace(/\n/g, '') + .replace('-----BEGIN PUBLIC KEY-----', '') + .replace('-----END PUBLIC KEY-----', '') + const msg = aesEncrypt(`${SYNC_CODE.authMsg}\n${publicKey}\n${getComputerName()}\nlx_music_desktop`, key) + // console.log(msg, key) + return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { m: msg } }).then(async({ text, code }) => { + // console.log(text) + switch (text) { + case SYNC_CODE.msgBlockedIp: + throw new Error(SYNC_CODE.msgBlockedIp) + case SYNC_CODE.authFailed: + throw new Error(SYNC_CODE.authFailed) + default: + if (code != 200) throw new Error(SYNC_CODE.authFailed) + } + let msg + try { + msg = rsaDecrypt(Buffer.from(text, 'base64'), privateKey).toString() + } catch (err: any) { + log.error('[auth] codeAuth decryptMsg error', err.message) + throw new Error(SYNC_CODE.authFailed) + } + // console.log(msg) + if (!msg) return Promise.reject(new Error(SYNC_CODE.authFailed)) + const info = JSON.parse(msg) as LX.Sync.ClientKeyInfo + void setSyncAuthKey(serverId, info) + return info + }) +} + +const keyAuth = async(urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => { + const msg = aesEncrypt(SYNC_CODE.authMsg + getComputerName(), keyInfo.key) + // eslint-disable-next-line @typescript-eslint/promise-function-async + return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { i: keyInfo.clientId, m: msg } }).then(({ text, code }) => { + if (code != 200) throw new Error(SYNC_CODE.authFailed) + + let msg + try { + msg = aesDecrypt(text, keyInfo.key) + } catch (err: any) { + log.error('[auth] keyAuth decryptMsg error', err.message) + throw new Error(SYNC_CODE.authFailed) + } + if (msg != SYNC_CODE.helloMsg) return Promise.reject(new Error(SYNC_CODE.authFailed)) + }) +} + +const auth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCode?: string) => { + if (authCode) return codeAuth(urlInfo, serverId, authCode) + const keyInfo = await getSyncAuthKey(serverId) + if (!keyInfo) throw new Error(SYNC_CODE.missingAuthCode) + await keyAuth(urlInfo, keyInfo) + return keyInfo +} + +export default async(urlInfo: LX.Sync.Client.UrlInfo, authCode?: string) => { + console.log('connect: ', urlInfo.href, authCode) + if (!await hello(urlInfo)) throw new Error(SYNC_CODE.connectServiceFailed) + const serverId = await getServerId(urlInfo) + if (!serverId) throw new Error(SYNC_CODE.getServiceIdFailed) + return auth(urlInfo, serverId, authCode) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/client.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/client.ts new file mode 100644 index 000000000..ad0032ee5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/client.ts @@ -0,0 +1,294 @@ +import WebSocket from 'ws' +import { encryptMsg, decryptMsg } from './utils' +import { callObj } from './sync' +// import { action as commonAction } from '@root/store/modules/common' +// import { getStore } from '@root/store' +// import registerSyncListHandler from './syncList' +import log from '../log' +import { dateFormat } from '@common/utils/common' +import { aesEncrypt } from '../utils' +import { sendClientStatus } from '@main/modules/winMain' +import { createMsg2call } from 'message2call' +import { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants_sync' +import { getAddress } from '@common/utils/nodejs' + +let status: LX.Sync.ClientStatus = { + status: false, + message: '', + address: [], +} + +export const sendSyncStatus = (newStatus: Omit) => { + status.status = newStatus.status + status.message = newStatus.message + if (status.status) { + status.address = getAddress() + } + sendClientStatus(status) +} + +export const sendSyncMessage = (message: string) => { + status.message = message + sendClientStatus(status) +} + +const heartbeatTools = { + failedNum: 0, + maxTryNum: 100000, + stepMs: 3000, + connectTimeout: null as NodeJS.Timeout | null, + pingTimeout: null as NodeJS.Timeout | null, + delayRetryTimeout: null as NodeJS.Timeout | null, + handleOpen() { + console.log('open') + // this.failedNum = 0 + this.heartbeat() + }, + heartbeat() { + if (this.pingTimeout) clearTimeout(this.pingTimeout) + + // Use `WebSocket#terminate()`, which immediately destroys the connection, + // instead of `WebSocket#close()`, which waits for the close timer. + // Delay should be equal to the interval at which your server + // sends out pings plus a conservative assumption of the latency. + this.pingTimeout = setTimeout(() => { + client?.terminate() + }, 30000 + 1000) + }, + reConnnect() { + this.clearTimeout() + // client = null + if (!client) return + + if (++this.failedNum > this.maxTryNum) { + this.failedNum = 0 + sendSyncStatus({ + status: false, + message: 'Connect error', + }) + throw new Error('connect error') + } + + const waitTime = Math.min(2000 + Math.floor(this.failedNum / 2) * this.stepMs, 15000) + + // sendSyncStatus({ + // status: false, + // message: `Waiting ${waitTime / 1000}s reconnnect...`, + // }) + + this.delayRetryTimeout = setTimeout(() => { + this.delayRetryTimeout = null + if (!client) return + console.log(dateFormat(new Date()), 'reconnnect...') + sendSyncStatus({ + status: false, + message: `Try reconnnect... (${this.failedNum})`, + }) + connect(client.data.urlInfo, client.data.keyInfo) + }, waitTime) + }, + clearTimeout() { + if (this.connectTimeout) { + clearTimeout(this.connectTimeout) + this.connectTimeout = null + } + if (this.delayRetryTimeout) { + clearTimeout(this.delayRetryTimeout) + this.delayRetryTimeout = null + } + if (this.pingTimeout) { + clearTimeout(this.pingTimeout) + this.pingTimeout = null + } + }, + connect(socket: LX.Sync.Client.Socket) { + console.log('heartbeatTools connect') + this.connectTimeout = setTimeout(() => { + this.connectTimeout = null + if (client) { + try { + client.close(SYNC_CLOSE_CODE.failed) + } catch {} + } + if (++this.failedNum > this.maxTryNum) { + this.failedNum = 0 + sendSyncStatus({ + status: false, + message: 'Connect error', + }) + throw new Error('connect error') + } + sendSyncStatus({ + status: false, + message: 'Connect timeout, try reconnect...', + }) + this.reConnnect() + }, 2 * 60 * 1000) + socket.on('open', () => { + if (this.connectTimeout) { + clearTimeout(this.connectTimeout) + this.connectTimeout = null + } + this.handleOpen() + }) + socket.on('ping', () => { + this.heartbeat() + }) + socket.on('close', (code) => { + console.log(code) + switch (code) { + case SYNC_CLOSE_CODE.normal: + case SYNC_CLOSE_CODE.failed: + return + } + this.reConnnect() + }) + }, +} + + +let client: LX.Sync.Client.Socket | null +// let listSyncPromise: Promise +export const connect = (urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => { + client = new WebSocket(`${urlInfo.wsProtocol}//${urlInfo.hostPath}/socket?i=${encodeURIComponent(keyInfo.clientId)}&t=${encodeURIComponent(aesEncrypt(SYNC_CODE.msgConnect, keyInfo.key))}`, { + followRedirects: true, + }) as LX.Sync.Client.Socket + client.data = { + keyInfo, + urlInfo, + } + heartbeatTools.connect(client) + + let closeEvents: Array<(err: Error) => (void | Promise)> = [] + let disconnected = true + + const message2read = createMsg2call({ + funcsObj: { + ...callObj, + finished() { + log.info('sync list success') + client!.isReady = true + sendSyncStatus({ + status: true, + message: '', + }) + heartbeatTools.failedNum = 0 + }, + }, + timeout: 120 * 1000, + sendMessage(data) { + if (disconnected) throw new Error('disconnected') + void encryptMsg(keyInfo, JSON.stringify(data)).then((data) => { + client?.send(data) + }).catch((err) => { + log.error('encrypt msg error: ', err) + client?.close(SYNC_CLOSE_CODE.failed) + }) + }, + onCallBeforeParams(rawArgs) { + return [client, ...rawArgs] + }, + onError(error, path, groupName) { + const name = groupName ?? '' + log.error(`sync call ${name} ${path.join('.')} error:`, error) + // if (groupName == null) return + // client?.close(SYNC_CLOSE_CODE.failed) + // sendSyncStatus({ + // status: false, + // message: error.message, + // }) + }, + }) + + client.remote = message2read.remote + client.remoteQueueList = message2read.createQueueRemote('list') + client.remoteQueueDislike = message2read.createQueueRemote('dislike') + + client.addEventListener('message', ({ data }) => { + if (data == 'ping') return + if (typeof data === 'string') { + void decryptMsg(keyInfo, data).then((data) => { + let syncData: LX.Sync.ServerSyncActions + try { + syncData = JSON.parse(data) + } catch (err) { + log.error('parse msg error: ', err) + client?.close(SYNC_CLOSE_CODE.failed) + return + } + message2read.message(syncData) + }).catch((error) => { + log.error('decrypt msg error: ', error) + client?.close(SYNC_CLOSE_CODE.failed) + }) + } + }) + client.onClose = function(handler: typeof closeEvents[number]) { + closeEvents.push(handler) + return () => { + closeEvents.splice(closeEvents.indexOf(handler), 1) + } + } + + const initMessage = 'Wait syncing...' + client.addEventListener('open', () => { + log.info('connect') + // const store = getStore() + // global.lx.syncKeyInfo = keyInfo + client!.isReady = false + client!.moduleReadys = { + list: false, + dislike: false, + } + disconnected = false + sendSyncStatus({ + status: false, + message: initMessage, + }) + }) + client.addEventListener('close', ({ code }) => { + const err = new Error('closed') + try { + for (const handler of closeEvents) void handler(err) + } catch (err: any) { + log.error(err?.message) + } + closeEvents = [] + disconnected = true + message2read.destroy() + switch (code) { + case SYNC_CLOSE_CODE.normal: + // case SYNC_CLOSE_CODE.failed: + sendSyncStatus({ + status: false, + message: '', + }) + break + case SYNC_CLOSE_CODE.failed: + if (!status.message || status.message == initMessage) { + sendSyncStatus({ + status: false, + message: 'failed', + }) + } + break + } + }) + client.addEventListener('error', ({ message }) => { + sendSyncStatus({ + status: false, + message, + }) + }) +} + +export const disconnect = async() => { + if (!client) return + log.info('disconnecting...') + client.close(SYNC_CLOSE_CODE.normal) + client = null + heartbeatTools.clearTimeout() + heartbeatTools.failedNum = 0 +} + +export const getStatus = (): LX.Sync.ClientStatus => status diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/data.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/data.ts new file mode 100644 index 000000000..1fb8b75af --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/data.ts @@ -0,0 +1,78 @@ +import fs from 'node:fs' +import path from 'node:path' +import { File } from '../../../../common/constants_sync' +import { exists } from '../utils' + + +let syncAuthKeys: Record + + +const saveSyncAuthKeys = async() => { + const syncAuthKeysFilePath = path.join(global.lxDataPath, File.clientDataPath, File.syncAuthKeysJSON) + return fs.promises.writeFile(syncAuthKeysFilePath, JSON.stringify(syncAuthKeys), 'utf8') +} + +export const initClientInfo = async() => { + if (syncAuthKeys != null) return + const syncAuthKeysFilePath = path.join(global.lxDataPath, File.clientDataPath, File.syncAuthKeysJSON) + if (await fs.promises.stat(syncAuthKeysFilePath).then(() => true).catch(() => false)) { + // eslint-disable-next-line require-atomic-updates + syncAuthKeys = JSON.parse((await fs.promises.readFile(syncAuthKeysFilePath)).toString()) + } else { + // eslint-disable-next-line require-atomic-updates + syncAuthKeys = {} + const syncDataPath = path.join(global.lxDataPath, File.clientDataPath) + if (!await exists(syncDataPath)) { + await fs.promises.mkdir(syncDataPath, { recursive: true }) + } + void saveSyncAuthKeys() + } +} + +export const getSyncAuthKey = async(serverId: string) => { + await initClientInfo() + return syncAuthKeys[serverId] ?? null +} +export const setSyncAuthKey = async(serverId: string, info: LX.Sync.ClientKeyInfo) => { + await initClientInfo() + syncAuthKeys[serverId] = info + void saveSyncAuthKeys() +} + +// let syncHost: string +// export const getSyncHost = async() => { +// if (syncHost === undefined) { +// const store = getStore(STORE_NAMES.SYNC) +// syncHost = (store.get('syncHost') as typeof syncHost | null) ?? '' +// } +// return syncHost +// } +// export const setSyncHost = async(host: string) => { +// // let hostInfo = await getData(syncHostPrefix) || {} +// // hostInfo.host = host +// // hostInfo.port = port +// syncHost = host +// const store = getStore(STORE_NAMES.SYNC) +// store.set('syncHost', syncHost) +// } +// let syncHostHistory: string[] +// export const getSyncHostHistory = async() => { +// if (syncHostHistory === undefined) { +// const store = getStore(STORE_NAMES.SYNC) +// syncHostHistory = (store.get('syncHostHistory') as string[]) ?? [] +// } +// return syncHostHistory +// } +// export const addSyncHostHistory = async(host: string) => { +// let syncHostHistory = await getSyncHostHistory() +// if (syncHostHistory.some(h => h == host)) return +// syncHostHistory.unshift(host) +// if (syncHostHistory.length > 20) syncHostHistory = syncHostHistory.slice(0, 20) // 最多存储20个 +// const store = getStore(STORE_NAMES.SYNC) +// store.set('syncHostHistory', syncHostHistory) +// } +// export const removeSyncHostHistory = async(index: number) => { +// syncHostHistory.splice(index, 1) +// const store = getStore(STORE_NAMES.SYNC) +// store.set('syncHostHistory', syncHostHistory) +// } diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/index.ts new file mode 100644 index 000000000..85c34b49e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/index.ts @@ -0,0 +1,75 @@ +import handleAuth from './auth' +import { connect as socketConnect, disconnect as socketDisconnect, sendSyncStatus, sendSyncMessage } from './client' +// import { getSyncHost } from '@root/utils/data' +import log from '../log' +import { parseUrl } from './utils' +import migrateData from '../migrate' +import { SYNC_CODE } from '@common/constants_sync' + +let connectId = 0 + +const handleConnect = async(host: string, authCode?: string) => { + // const hostInfo = await getSyncHost() + // console.log(hostInfo) + // if (!hostInfo || !hostInfo.host || !hostInfo.port) throw new Error(SYNC_CODE.unknownServiceAddress) + const id = connectId + const urlInfo = parseUrl(host) + await disconnectServer(false) + if (id != connectId) return + const keyInfo = await handleAuth(urlInfo, authCode) + if (id != connectId) return + socketConnect(urlInfo, keyInfo) +} +const handleDisconnect = async() => { + await socketDisconnect() +} + +const connectServer = async(host: string, authCode?: string) => { + sendSyncStatus({ + status: false, + message: SYNC_CODE.connecting, + }) + const id = connectId + await migrateData(global.lxDataPath) + + return handleConnect(host, authCode).catch(async err => { + if (id != connectId) return + sendSyncStatus({ + status: false, + message: err.message, + }) + switch (err.message) { + case SYNC_CODE.connectServiceFailed: + case SYNC_CODE.missingAuthCode: + break + default: + log.r_warn(err.message) + break + } + + return Promise.reject(err) + }) +} + +const disconnectServer = async(isResetStatus = true) => handleDisconnect().then(() => { + log.info('disconnect...') + if (isResetStatus) { + connectId++ + sendSyncStatus({ + status: false, + message: '', + }) + } +}).catch((err: any) => { + log.error(`disconnect error: ${err.message as string}`) + sendSyncMessage(err.message) +}) + +export { + connectServer, + disconnectServer, +} + +export { + getStatus, +} from './client' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/dislike/handler.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/dislike/handler.ts new file mode 100644 index 000000000..03859441d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/dislike/handler.ts @@ -0,0 +1,68 @@ +// 这个文件导出的方法将暴露给服务端调用,第一个参数固定为当前 socket 对象 +import { handleRemoteDislikeAction, getLocalDislikeData, setLocalDislikeData } from '@main/modules/sync/dislikeEvent' +import { toMD5 } from '@common/utils/nodejs' +import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain' +import log from '@main/modules/sync/log' +import { registerEvent, unregisterEvent } from './localEvent' + +const logInfo = (eventName: string, success = false) => { + log.info(`[${eventName}]${eventName.replace('dislike:sync:dislike_sync_', '').replaceAll('_', ' ')}${success ? ' success' : ''}`) +} +// const logError = (eventName: string, err: Error) => { +// log.error(`[${eventName}]${eventName.replace('dislike:sync:dislike_sync_', '').replaceAll('_', ' ')} error: ${err.message}`) +// } +const getSyncMode = async(socket: LX.Sync.Client.Socket): Promise => new Promise((resolve, reject) => { + const handleDisconnect = (err: Error) => { + sendCloseSelectMode() + removeSelectModeListener() + reject(err) + } + let removeEventClose = socket.onClose(handleDisconnect) + sendSelectMode(socket.data.keyInfo.serverName, 'dislike', (mode) => { + if (mode == null) { + reject(new Error('cancel')) + return + } + resolve(mode) + removeSelectModeListener() + removeEventClose() + }) +}) +const handler: LX.Sync.ClientSyncHandlerDislikeActions = { + async onDislikeSyncAction(socket, action) { + if (!socket.moduleReadys?.dislike) return + await handleRemoteDislikeAction(action) + }, + + async dislike_sync_get_md5(socket) { + logInfo('dislike:sync:dislike_sync_get_md5') + return toMD5((await getLocalDislikeData()).trim()) + }, + + + async dislike_sync_get_sync_mode(socket) { + return getSyncMode(socket) + }, + + async dislike_sync_get_list_data(socket) { + logInfo('dislike:sync:dislike_sync_get_list_data') + return getLocalDislikeData() + }, + + async dislike_sync_set_list_data(socket, data) { + logInfo('dislike:sync:dislike_sync_set_list_data') + await setLocalDislikeData(data) + }, + + async dislike_sync_finished(socket) { + logInfo('dislike:sync:finished') + socket.moduleReadys.dislike = true + registerEvent(socket) + socket.onClose(() => { + unregisterEvent() + }) + }, +} + +export default handler + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/dislike/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/dislike/index.ts new file mode 100644 index 000000000..ae2f08140 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/dislike/index.ts @@ -0,0 +1,4 @@ + +export { default as handler } from './handler' + +export * from './localEvent' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/dislike/localEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/dislike/localEvent.ts new file mode 100644 index 000000000..4a8211e36 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/dislike/localEvent.ts @@ -0,0 +1,27 @@ +import { SYNC_CLOSE_CODE } from '@common/constants_sync' +import { registerDislikeActionEvent } from '@main/modules/sync/dislikeEvent' + +let unregisterLocalListAction: (() => void) | null + +export const registerEvent = (socket: LX.Sync.Client.Socket) => { + // socket = _socket + // socket.onClose(() => { + // unregisterLocalListAction?.() + // unregisterLocalListAction = null + // }) + unregisterEvent() + unregisterLocalListAction = registerDislikeActionEvent((action) => { + if (!socket.moduleReadys?.dislike) return + void socket.remoteQueueDislike.onDislikeSyncAction(action).catch(err => { + // TODO send status + socket.moduleReadys.dislike = false + socket.close(SYNC_CLOSE_CODE.failed) + console.log(err.message) + }) + }) +} + +export const unregisterEvent = () => { + unregisterLocalListAction?.() + unregisterLocalListAction = null +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/index.ts new file mode 100644 index 000000000..5a548e3d7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/index.ts @@ -0,0 +1,20 @@ +import * as list from './list' +import * as dislike from './dislike' +// export * as theme from './theme' + + +export const callObj = Object.assign({}, + list.handler, + dislike.handler, +) + + +export const modules = { + list, + dislike, +} + +export const featureVersion = { + list: 1, + dislike: 1, +} as const diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/list/handler.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/list/handler.ts new file mode 100644 index 000000000..4a90452ae --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/list/handler.ts @@ -0,0 +1,72 @@ +// 这个文件导出的方法将暴露给服务端调用,第一个参数固定为当前 socket 对象 +import { + handleRemoteListAction, + getLocalListData, + setLocalListData, +} from '@main/modules/sync/listEvent' +import { toMD5 } from '@common/utils/nodejs' +import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain' +import log from '@main/modules/sync/log' +import { registerEvent, unregisterEvent } from './localEvent' + +const logInfo = (eventName: string, success = false) => { + log.info(`[${eventName}]${eventName.replace('list:sync:list_sync_', '').replaceAll('_', ' ')}${success ? ' success' : ''}`) +} +// const logError = (eventName: string, err: Error) => { +// log.error(`[${eventName}]${eventName.replace('list:sync:list_sync_', '').replaceAll('_', ' ')} error: ${err.message}`) +// } +const getSyncMode = async(socket: LX.Sync.Client.Socket): Promise => new Promise((resolve, reject) => { + const handleDisconnect = (err: Error) => { + sendCloseSelectMode() + removeSelectModeListener() + reject(err) + } + let removeEventClose = socket.onClose(handleDisconnect) + sendSelectMode(socket.data.keyInfo.serverName, 'list', (mode) => { + if (mode == null) { + reject(new Error('cancel')) + return + } + resolve(mode) + removeSelectModeListener() + removeEventClose() + }) +}) + +const handler: LX.Sync.ClientSyncHandlerListActions = { + async onListSyncAction(socket, action) { + if (!socket.moduleReadys?.list) return + await handleRemoteListAction(action) + }, + + async list_sync_get_md5(socket) { + logInfo('list:sync:list_sync_get_md5') + return toMD5(JSON.stringify(await getLocalListData())) + }, + + + async list_sync_get_sync_mode(socket) { + return getSyncMode(socket) + }, + + async list_sync_get_list_data(socket) { + logInfo('list:sync:list_sync_get_list_data') + return getLocalListData() + }, + + async list_sync_set_list_data(socket, data) { + logInfo('list:sync:list_sync_set_list_data') + await setLocalListData(data) + }, + + async list_sync_finished(socket) { + logInfo('list:sync:finished') + socket.moduleReadys.list = true + registerEvent(socket) + socket.onClose(() => { + unregisterEvent() + }) + }, +} + +export default handler diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/list/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/list/index.ts new file mode 100644 index 000000000..ae2f08140 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/list/index.ts @@ -0,0 +1,4 @@ + +export { default as handler } from './handler' + +export * from './localEvent' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/list/localEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/list/localEvent.ts new file mode 100644 index 000000000..0b94e637c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/modules/list/localEvent.ts @@ -0,0 +1,27 @@ +import { SYNC_CLOSE_CODE } from '@common/constants_sync' +import { registerListActionEvent } from '@main/modules/sync/listEvent' + +let unregisterLocalListAction: (() => void) | null + +export const registerEvent = (socket: LX.Sync.Client.Socket) => { + // socket = _socket + // socket.onClose(() => { + // unregisterLocalListAction?.() + // unregisterLocalListAction = null + // }) + unregisterEvent() + unregisterLocalListAction = registerListActionEvent((action) => { + if (!socket.moduleReadys?.list) return + void socket.remoteQueueList.onListSyncAction(action).catch(err => { + // TODO send status + socket.moduleReadys.list = false + socket.close(SYNC_CLOSE_CODE.failed) + console.log(err.message) + }) + }) +} + +export const unregisterEvent = () => { + unregisterLocalListAction?.() + unregisterLocalListAction = null +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/sync/handler.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/sync/handler.ts new file mode 100644 index 000000000..92869f618 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/sync/handler.ts @@ -0,0 +1,34 @@ +// 这个文件导出的方法将暴露给服务端调用,第一个参数固定为当前 socket 对象 + +// import { getUserSpace } from '@/user' +// import { modules } from '../modules' + +import { featureVersion } from '../modules' + +const handler: Omit, 'finished'> = { + async getEnabledFeatures(socket, serverType, supportedFeatures) { + // const userSpace = getUserSpace(socket.userInfo.name) + const features: LX.Sync.EnabledFeatures = {} + switch (serverType) { + case 'server': + if (featureVersion.list == supportedFeatures.list) { + features.list = { skipSnapshot: false } + } + if (featureVersion.dislike == supportedFeatures.dislike) { + features.dislike = { skipSnapshot: false } + } + return features + case 'desktop-app': + default: + if (featureVersion.list == supportedFeatures.list) { + features.list = { skipSnapshot: false } + } + if (featureVersion.dislike == supportedFeatures.dislike) { + features.dislike = { skipSnapshot: false } + } + return features + } + }, +} + +export default handler diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/sync/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/sync/index.ts new file mode 100644 index 000000000..1dce9ae89 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/sync/index.ts @@ -0,0 +1,8 @@ +import handler from './handler' +import { callObj as _callObj } from '../modules' +export { modules } from '../modules' + +export const callObj = { + ...handler, + ..._callObj, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/utils.ts new file mode 100644 index 000000000..42270f795 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/client/utils.ts @@ -0,0 +1,115 @@ +import { generateKeyPair } from 'node:crypto' +import { httpFetch, type RequestOptions } from '@main/utils/request' +import { decodeData, encodeData } from '../utils' + +export const request = async(url: string, options: RequestOptions = { }) => { + return httpFetch(url, { + ...options, + timeout: options.timeout ?? 10000, + }).then(response => { + return { + text: response.body, + code: response.statusCode, + } + }) + // const controller = new AbortController() + // let id: number | null = setTimeout(() => { + // id = null + // controller.abort() + // }, timeout) + // return fetch(url, { + // ...options, + // signal: controller.signal, + // // eslint-disable-next-line @typescript-eslint/promise-function-async + // }).then(async(response) => { + // const text = await response.text() + // return { + // text, + // code: response.status, + // } + // }).catch(err => { + // // console.log(err, err.code, err.message) + // throw err + // }).finally(() => { + // if (id == null) return + // clearTimeout(id) + // }) +} + +// export const aesEncrypt = (text: string, key: string, iv: string) => { +// const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64')) +// return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64') +// } + +// export const aesDecrypt = (text: string, key: string, iv: string) => { +// const decipher = createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64')) +// return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString() +// } + +export const generateRsaKey = async() => new Promise<{ publicKey: string, privateKey: string }>((resolve, reject) => { + generateKeyPair( + 'rsa', + { + modulusLength: 2048, // It holds a number. It is the key size in bits and is applicable for RSA, and DSA algorithm only. + publicKeyEncoding: { + type: 'spki', // Note the type is pkcs1 not spki + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', // Note again the type is set to pkcs1 + format: 'pem', + // cipher: "aes-256-cbc", //Optional + // passphrase: "", //Optional + }, + }, + (err, publicKey, privateKey) => { + if (err) { + reject(err) + return + } + resolve({ + publicKey, + privateKey, + }) + }, + ) +}) + +export const encryptMsg = async(keyInfo: LX.Sync.ClientKeyInfo, msg: string): Promise => { + return encodeData(msg) + // if (!keyInfo) return '' + // return aesEncrypt(msg, keyInfo.key, keyInfo.iv) +} + +export const decryptMsg = async(keyInfo: LX.Sync.ClientKeyInfo, enMsg: string): Promise => { + return decodeData(enMsg) + // if (!keyInfo) return '' + // let msg = '' + // try { + // msg = aesDecrypt(enMsg, keyInfo.key, keyInfo.iv) + // } catch (err) { + // console.log(err) + // } + // return msg +} + + +export const parseUrl = (host: string): LX.Sync.Client.UrlInfo => { + const url = new URL(host) + let hostPath = url.host + url.pathname + let href = url.href + if (hostPath.endsWith('/')) hostPath = hostPath.replace(/\/$/, '') + if (href.endsWith('/')) href = href.replace(/\/$/, '') + + return { + wsProtocol: url.protocol == 'https:' ? 'wss:' : 'ws:', + httpProtocol: url.protocol, + hostPath, + href, + } +} + + +export const sendStatus = (status: LX.Sync.ClientStatus) => { + // syncLog.log(JSON.stringify(status)) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/dislikeEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/dislikeEvent.ts new file mode 100644 index 000000000..65f13c825 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/dislikeEvent.ts @@ -0,0 +1,50 @@ + +export const getLocalDislikeData = async(): Promise => { + return (await global.lx.worker.dbService.getDislikeListInfo()).rules +} + +export const setLocalDislikeData = async(listData: LX.Dislike.DislikeRules) => { + await global.lx.event_dislike.dislike_data_overwrite(listData, true) +} + +export const registerDislikeActionEvent = (sendDislikeAction: (action: LX.Sync.Dislike.ActionList) => (void | Promise)) => { + const dislike_music_add = async(listData: LX.Dislike.DislikeMusicInfo[], isRemote: boolean = false) => { + if (isRemote) return + await sendDislikeAction({ action: 'dislike_music_add', data: listData }) + } + const dislike_data_overwrite = async(listInfos: LX.Dislike.DislikeRules, isRemote: boolean = false) => { + if (isRemote) return + await sendDislikeAction({ action: 'dislike_data_overwrite', data: listInfos }) + } + const dislike_music_clear = async(isRemote: boolean = false) => { + if (isRemote) return + await sendDislikeAction({ action: 'dislike_music_clear' }) + } + + global.lx.event_dislike.on('dislike_music_add', dislike_music_add) + global.lx.event_dislike.on('dislike_data_overwrite', dislike_data_overwrite) + global.lx.event_dislike.on('dislike_music_clear', dislike_music_clear) + return () => { + global.lx.event_dislike.off('dislike_music_add', dislike_music_add) + global.lx.event_dislike.off('dislike_data_overwrite', dislike_data_overwrite) + global.lx.event_dislike.off('dislike_music_clear', dislike_music_clear) + } +} + +export const handleRemoteDislikeAction = async(event: LX.Sync.Dislike.ActionList) => { + // console.log('handleRemoteDislikeAction', event) + + switch (event.action) { + case 'dislike_music_add': + await global.lx.event_dislike.dislike_music_add(event.data, true) + break + case 'dislike_data_overwrite': + await global.lx.event_dislike.dislike_data_overwrite(event.data, true) + break + case 'dislike_music_clear': + await global.lx.event_dislike.dislike_music_clear(true) + break + default: + throw new Error('unknown list sync action') + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/index.ts new file mode 100644 index 000000000..7ce3a5b9d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/index.ts @@ -0,0 +1,30 @@ +// import Event from './event/event' + +import { disconnectServer } from './client' +import { stopServer } from './server' + +// import eventNames from './event/name' +export { + startServer, + stopServer, + getStatus as getServerStatus, + generateCode, + getDevices as getServerDevices, + removeDevice as removeServerDevice, +} from './server' + +export { + connectServer, + disconnectServer, + getStatus as getClientStatus, +} from './client' + +export default () => { + global.lx.event_app.on('main_window_close', () => { + if (global.lx.appSetting['sync.mode'] == 'server') { + void stopServer() + } else { + void disconnectServer() + } + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/listEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/listEvent.ts new file mode 100644 index 000000000..909337582 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/listEvent.ts @@ -0,0 +1,157 @@ +import { LIST_IDS } from '@common/constants' + +// 构建列表信息对象,用于统一字段位置顺序 +export const buildUserListInfoFull = ({ id, name, source, sourceListId, list, locationUpdateTime }: LX.List.UserListInfoFull) => { + return { + id, + name, + source, + sourceListId, + locationUpdateTime, + list, + } +} + +export const getLocalListData = async(): Promise => { + const lists: LX.Sync.List.ListData = { + defaultList: await global.lx.worker.dbService.getListMusics(LIST_IDS.DEFAULT), + loveList: await global.lx.worker.dbService.getListMusics(LIST_IDS.LOVE), + userList: [], + } + + const userListInfos = await global.lx.worker.dbService.getAllUserList() + for await (const list of userListInfos) { + lists.userList.push(await global.lx.worker.dbService.getListMusics(list.id) + .then(musics => buildUserListInfoFull({ ...list, list: musics }))) + } + + return lists +} + +export const setLocalListData = async(listData: LX.Sync.List.ListData) => { + await global.lx.event_list.list_data_overwrite(listData, true) +} + + +export const registerListActionEvent = (sendListAction: (action: LX.Sync.List.ActionList) => (void | Promise)) => { + const list_data_overwrite = async(listData: MakeOptional, isRemote: boolean = false) => { + if (isRemote) return + await sendListAction({ action: 'list_data_overwrite', data: listData }) + } + const list_create = async(position: number, listInfos: LX.List.UserListInfo[], isRemote: boolean = false) => { + if (isRemote) return + await sendListAction({ action: 'list_create', data: { position, listInfos } }) + } + const list_remove = async(ids: string[], isRemote: boolean = false) => { + if (isRemote) return + await sendListAction({ action: 'list_remove', data: ids }) + } + const list_update = async(lists: LX.List.UserListInfo[], isRemote: boolean = false) => { + if (isRemote) return + await sendListAction({ action: 'list_update', data: lists }) + } + const list_update_position = async(position: number, ids: string[], isRemote: boolean = false) => { + if (isRemote) return + await sendListAction({ action: 'list_update_position', data: { position, ids } }) + } + const list_music_overwrite = async(listId: string, musicInfos: LX.Music.MusicInfo[], isRemote: boolean = false) => { + if (isRemote || listId == LIST_IDS.TEMP) return + await sendListAction({ action: 'list_music_overwrite', data: { listId, musicInfos } }) + } + const list_music_add = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) => { + if (isRemote) return + await sendListAction({ action: 'list_music_add', data: { id, musicInfos, addMusicLocationType } }) + } + const list_music_move = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) => { + if (isRemote) return + await sendListAction({ action: 'list_music_move', data: { fromId, toId, musicInfos, addMusicLocationType } }) + } + const list_music_remove = async(listId: string, ids: string[], isRemote: boolean = false) => { + if (isRemote || listId == LIST_IDS.TEMP) return + await sendListAction({ action: 'list_music_remove', data: { listId, ids } }) + } + const list_music_update = async(musicInfos: LX.List.ListActionMusicUpdate, isRemote: boolean = false) => { + musicInfos = musicInfos.filter(item => item.id != LIST_IDS.TEMP) + if (isRemote || !musicInfos.length) return + await sendListAction({ action: 'list_music_update', data: musicInfos }) + } + const list_music_clear = async(ids: string[], isRemote: boolean = false) => { + if (isRemote) return + await sendListAction({ action: 'list_music_clear', data: ids }) + } + const list_music_update_position = async(listId: string, position: number, ids: string[], isRemote: boolean = false) => { + if (isRemote || listId == LIST_IDS.TEMP) return + await sendListAction({ action: 'list_music_update_position', data: { listId, position, ids } }) + } + global.lx.event_list.on('list_data_overwrite', list_data_overwrite) + global.lx.event_list.on('list_create', list_create) + global.lx.event_list.on('list_remove', list_remove) + global.lx.event_list.on('list_update', list_update) + global.lx.event_list.on('list_update_position', list_update_position) + global.lx.event_list.on('list_music_overwrite', list_music_overwrite) + global.lx.event_list.on('list_music_add', list_music_add) + global.lx.event_list.on('list_music_move', list_music_move) + global.lx.event_list.on('list_music_remove', list_music_remove) + global.lx.event_list.on('list_music_update', list_music_update) + global.lx.event_list.on('list_music_clear', list_music_clear) + global.lx.event_list.on('list_music_update_position', list_music_update_position) + return () => { + global.lx.event_list.off('list_data_overwrite', list_data_overwrite) + global.lx.event_list.off('list_create', list_create) + global.lx.event_list.off('list_remove', list_remove) + global.lx.event_list.off('list_update', list_update) + global.lx.event_list.off('list_update_position', list_update_position) + global.lx.event_list.off('list_music_overwrite', list_music_overwrite) + global.lx.event_list.off('list_music_add', list_music_add) + global.lx.event_list.off('list_music_move', list_music_move) + global.lx.event_list.off('list_music_remove', list_music_remove) + global.lx.event_list.off('list_music_update', list_music_update) + global.lx.event_list.off('list_music_clear', list_music_clear) + global.lx.event_list.off('list_music_update_position', list_music_update_position) + } +} + +export const handleRemoteListAction = async({ action, data }: LX.Sync.List.ActionList) => { + // console.log('handleRemoteListAction', action) + + switch (action) { + case 'list_data_overwrite': + await global.lx.event_list.list_data_overwrite(data, true) + break + case 'list_create': + await global.lx.event_list.list_create(data.position, data.listInfos, true) + break + case 'list_remove': + await global.lx.event_list.list_remove(data, true) + break + case 'list_update': + await global.lx.event_list.list_update(data, true) + break + case 'list_update_position': + await global.lx.event_list.list_update_position(data.position, data.ids, true) + break + case 'list_music_add': + await global.lx.event_list.list_music_add(data.id, data.musicInfos, data.addMusicLocationType, true) + break + case 'list_music_move': + await global.lx.event_list.list_music_move(data.fromId, data.toId, data.musicInfos, data.addMusicLocationType, true) + break + case 'list_music_remove': + await global.lx.event_list.list_music_remove(data.listId, data.ids, true) + break + case 'list_music_update': + await global.lx.event_list.list_music_update(data, true) + break + case 'list_music_update_position': + await global.lx.event_list.list_music_update_position(data.listId, data.position, data.ids, true) + break + case 'list_music_overwrite': + await global.lx.event_list.list_music_overwrite(data.listId, data.musicInfos, true) + break + case 'list_music_clear': + await global.lx.event_list.list_music_clear(data, true) + break + default: + throw new Error('unknown list sync action') + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/log.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/log.ts new file mode 100644 index 000000000..58377e51b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/log.ts @@ -0,0 +1,25 @@ +import { log as writeLog } from '@common/utils' + +export default { + r_info(...params: any[]) { + writeLog.info(...params) + }, + r_warn(...params: any[]) { + writeLog.warn(...params) + }, + r_error(...params: any[]) { + writeLog.error(...params) + }, + info(...params: any[]) { + // if (global.lx.isEnableSyncLog) writeLog.info(...params) + console.log(...params) + }, + warn(...params: any[]) { + // if (global.lx.isEnableSyncLog) writeLog.warn(...params) + console.warn(...params) + }, + error(...params: any[]) { + // if (global.lx.isEnableSyncLog) writeLog.error(...params) + console.warn(...params) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/migrate.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/migrate.ts new file mode 100644 index 000000000..e17aca931 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/migrate.ts @@ -0,0 +1,76 @@ +import { File } from '../../../common/constants_sync' +import fs from 'node:fs' +import path from 'node:path' +import { exists } from './utils' + +interface ServerKeyInfo { + clientId: string + key: string + deviceName: string + lastSyncDate?: number + snapshotKey?: string + lastConnectDate?: number + isMobile: boolean +} + + +// 迁移 v2 sync 数据 +export default async(dataPath: string) => { + const syncDataPath = path.join(dataPath, 'sync') + // console.log(syncDataPath) + if (await exists(syncDataPath)) return + const oldInfoPath = path.join(dataPath, 'sync.json') + // console.log(oldInfoPath) + if (!await exists(oldInfoPath)) return + const serverSyncDataPath = path.join(dataPath, File.serverDataPath) + const clientSyncDataPath = path.join(dataPath, File.clientDataPath) + + await fs.promises.mkdir(serverSyncDataPath, { recursive: true }) + await fs.promises.mkdir(clientSyncDataPath, { recursive: true }) + const info = JSON.parse((await fs.promises.readFile(oldInfoPath)).toString()) + + + const serverInfoPath = path.join(serverSyncDataPath, File.serverInfoJSON) + const devicesInfoPath = path.join(serverSyncDataPath, File.userDevicesJSON) + const listDir = path.join(serverSyncDataPath, File.listDir) + await fs.promises.mkdir(listDir) + + + const snapshotInfo = info.snapshotInfo + delete info.snapshotInfo + snapshotInfo.clients = {} + for (const device of Object.values(info.clients)) { + snapshotInfo.clients[device.clientId] = { + snapshotKey: device.snapshotKey, + lastSyncDate: device.lastSyncDate, + } + device.lastConnectDate = device.lastSyncDate + delete device.lastSyncDate + delete device.snapshotKey + } + const devicesInfo = { + userName: 'default', + clients: info.clients, + } + await fs.promises.writeFile(serverInfoPath, JSON.stringify({ serverId: info.serverId, version: 2 })) + await fs.promises.writeFile(devicesInfoPath, JSON.stringify(devicesInfo)) + await fs.promises.writeFile(path.join(listDir, File.listSnapshotInfoJSON), JSON.stringify(snapshotInfo)) + + const snapshotPath = path.join(listDir, File.listSnapshotDir) + await fs.promises.mkdir(snapshotPath) + const snapshots = (await fs.promises.readdir(dataPath)).filter(name => name.startsWith('snapshot_')) + if (snapshots.length) { + for (const file of snapshots) { + await fs.promises.copyFile(path.join(dataPath, file), path.join(snapshotPath, file)) + } + } + + + await fs.promises.writeFile(path.join(clientSyncDataPath, File.syncAuthKeysJSON), JSON.stringify(info.syncAuthKey)) + + for (const file of snapshots) { + await fs.promises.unlink(path.join(dataPath, file)) + } + await fs.promises.unlink(oldInfoPath) +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/index.ts new file mode 100644 index 000000000..789db3695 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/index.ts @@ -0,0 +1,8 @@ +export { + startServer, + stopServer, + getStatus, + generateCode, + getDevices, + removeDevice, +} from './server' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/index.ts new file mode 100644 index 000000000..648ebf242 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/index.ts @@ -0,0 +1,3 @@ +export * as sync from './sync' +export { DislikeManage } from './manage' + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/manage.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/manage.ts new file mode 100644 index 000000000..e60fcd4cc --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/manage.ts @@ -0,0 +1,56 @@ +import { type UserDataManage } from '../../user' +import { SnapshotDataManage } from './snapshotDataManage' +import { toMD5 } from '../../utils' +import { getLocalDislikeData } from '@main/modules/sync/dislikeEvent' + +export class DislikeManage { + snapshotDataManage: SnapshotDataManage + + constructor(userDataManage: UserDataManage) { + this.snapshotDataManage = new SnapshotDataManage(userDataManage) + } + + createSnapshot = async() => { + const listData = await this.getDislikeRules() + const md5 = toMD5(listData.trim()) + const snapshotInfo = await this.snapshotDataManage.getSnapshotInfo() + console.log(md5, snapshotInfo.latest) + if (snapshotInfo.latest == md5) return md5 + if (snapshotInfo.list.includes(md5)) { + snapshotInfo.list.splice(snapshotInfo.list.indexOf(md5), 1) + } else await this.snapshotDataManage.saveSnapshot(md5, listData) + if (snapshotInfo.latest) snapshotInfo.list.unshift(snapshotInfo.latest) + snapshotInfo.latest = md5 + snapshotInfo.time = Date.now() + this.snapshotDataManage.saveSnapshotInfo(snapshotInfo) + return md5 + } + + getCurrentListInfoKey = async() => { + // const snapshotInfo = await this.snapshotDataManage.getSnapshotInfo() + // if (snapshotInfo.latest) { + // return snapshotInfo.latest + // } + // snapshotInfo.latest = toMD5((await this.getDislikeRules()).trim()) + // this.snapshotDataManage.saveSnapshotInfo(snapshotInfo) + // return snapshotInfo.latest + return this.createSnapshot() + } + + getDeviceCurrentSnapshotKey = async(clientId: string) => { + return this.snapshotDataManage.getDeviceCurrentSnapshotKey(clientId) + } + + updateDeviceSnapshotKey = async(clientId: string, key: string) => { + await this.snapshotDataManage.updateDeviceSnapshotKey(clientId, key) + } + + removeDevice = async(clientId: string) => { + this.snapshotDataManage.removeSnapshotInfo(clientId) + } + + getDislikeRules = async() => { + return getLocalDislikeData() + } +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/snapshotDataManage.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/snapshotDataManage.ts new file mode 100644 index 000000000..e60bbdbab --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/snapshotDataManage.ts @@ -0,0 +1,143 @@ +import { throttle } from '@common/utils/common' +import fs from 'node:fs' +import path from 'node:path' +import syncLog from '../../../log' +import { getUserConfig, type UserDataManage } from '../../user/data' +import { File } from '../../../../../../common/constants_sync' +import { checkAndCreateDirSync } from '../../utils' + + +interface SnapshotInfo { + latest: string | null + time: number + list: string[] + clients: Record +} +export class SnapshotDataManage { + userDataManage: UserDataManage + dislikeDir: string + snapshotDir: string + snapshotInfoFilePath: string + snapshotInfo: SnapshotInfo + clientSnapshotKeys: string[] + private readonly saveSnapshotInfoThrottle: () => void + + isIncluedsDevice = (key: string) => { + return this.clientSnapshotKeys.includes(key) + } + + clearOldSnapshot = async() => { + if (!this.snapshotInfo) return + const snapshotList = this.snapshotInfo.list.filter(key => !this.isIncluedsDevice(key)) + // console.log(snapshotList.length, lx.config.maxSnapshotNum) + const userMaxSnapshotNum = getUserConfig(this.userDataManage.userName).maxSnapshotNum + let requiredSave = snapshotList.length > userMaxSnapshotNum + while (snapshotList.length > userMaxSnapshotNum) { + const name = snapshotList.pop() + if (name) { + await this.removeSnapshot(name) + this.snapshotInfo.list.splice(this.snapshotInfo.list.indexOf(name), 1) + } else break + } + if (requiredSave) this.saveSnapshotInfo(this.snapshotInfo) + } + + updateDeviceSnapshotKey = async(clientId: string, key: string) => { + // console.log('updateDeviceSnapshotKey', key) + let client = this.snapshotInfo.clients[clientId] + if (!client) client = this.snapshotInfo.clients[clientId] = { snapshotKey: '', lastSyncDate: 0 } + if (client.snapshotKey) this.clientSnapshotKeys.splice(this.clientSnapshotKeys.indexOf(client.snapshotKey), 1) + client.snapshotKey = key + client.lastSyncDate = Date.now() + this.clientSnapshotKeys.push(key) + this.saveSnapshotInfoThrottle() + } + + getDeviceCurrentSnapshotKey = async(clientId: string) => { + // console.log('updateDeviceSnapshotKey', key) + const client = this.snapshotInfo.clients[clientId] + return client?.snapshotKey + } + + getSnapshotInfo = async(): Promise => { + return this.snapshotInfo + } + + saveSnapshotInfo = (info: SnapshotInfo) => { + this.snapshotInfo = info + this.saveSnapshotInfoThrottle() + } + + removeSnapshotInfo = (clientId: string) => { + let client = this.snapshotInfo.clients[clientId] + if (!client) return + if (client.snapshotKey) this.clientSnapshotKeys.splice(this.clientSnapshotKeys.indexOf(client.snapshotKey), 1) + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.snapshotInfo.clients[clientId] + this.saveSnapshotInfoThrottle() + } + + getSnapshot = async(name: string) => { + const filePath = path.join(this.snapshotDir, `snapshot_${name}`) + let listData: LX.Dislike.DislikeRules + try { + listData = (await fs.promises.readFile(filePath)).toString('utf-8') + } catch (err) { + syncLog.warn(err) + return null + } + return listData + } + + saveSnapshot = async(name: string, data: string) => { + syncLog.info('saveSnapshot', this.userDataManage.userName, name) + const filePath = path.join(this.snapshotDir, `snapshot_${name}`) + try { + fs.writeFileSync(filePath, data) + } catch (err) { + syncLog.error(err) + throw err + } + } + + removeSnapshot = async(name: string) => { + syncLog.info('removeSnapshot', this.userDataManage.userName, name) + const filePath = path.join(this.snapshotDir, `snapshot_${name}`) + try { + fs.unlinkSync(filePath) + } catch (err) { + syncLog.error(err) + } + } + + + constructor(userDataManage: UserDataManage) { + this.userDataManage = userDataManage + + this.dislikeDir = path.join(userDataManage.userDir, File.dislikeDir) + checkAndCreateDirSync(this.dislikeDir) + + this.snapshotDir = path.join(this.dislikeDir, File.dislikeSnapshotDir) + checkAndCreateDirSync(this.snapshotDir) + + this.snapshotInfoFilePath = path.join(this.dislikeDir, File.dislikeSnapshotInfoJSON) + this.snapshotInfo = fs.existsSync(this.snapshotInfoFilePath) + ? JSON.parse(fs.readFileSync(this.snapshotInfoFilePath).toString()) + : { latest: null, time: 0, list: [], clients: {} } + + this.saveSnapshotInfoThrottle = throttle(() => { + fs.writeFile(this.snapshotInfoFilePath, JSON.stringify(this.snapshotInfo), 'utf8', (err) => { + if (err) console.error(err) + void this.clearOldSnapshot() + }) + }) + + this.clientSnapshotKeys = Object.values(this.snapshotInfo.clients).map(device => device.snapshotKey).filter(k => k) + } +} +// type UserDataManages = Map + +// export const createUserDataManage = (user: LX.UserConfig) => { +// const manage = Object.create(userDataManage) as typeof userDataManage +// manage.userDir = user.dataPath +// } diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/handler.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/handler.ts new file mode 100644 index 000000000..2807c6fda --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/handler.ts @@ -0,0 +1,35 @@ +// 这个文件导出的方法将暴露给客户端调用,第一个参数固定为当前 socket 对象 +// import { throttle } from '@common/utils/common' +// import { sendSyncActionList } from '@main/modules/winMain' +// import { SYNC_CLOSE_CODE } from '@/constants' +// import { SYNC_CLOSE_CODE } from '@common/constants_sync' +import { SYNC_CLOSE_CODE } from '@common/constants_sync' +import { getUserSpace } from '@main/modules/sync/server/user' +import { handleRemoteDislikeAction } from '@main/modules/sync/dislikeEvent' +// import { encryptMsg } from '@/utils/tools' + + +const handler: LX.Sync.ServerSyncHandlerDislikeActions = { + async onDislikeSyncAction(socket, action) { + if (!socket.moduleReadys.dislike) return + await handleRemoteDislikeAction(action) + const userSpace = getUserSpace(socket.userInfo.name) + const key = await userSpace.dislikeManage.createSnapshot() + userSpace.dislikeManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key) + const currentUserName = socket.userInfo.name + const currentId = socket.keyInfo.clientId + socket.broadcast((client) => { + if (client.keyInfo.clientId == currentId || !client.moduleReadys?.dislike || client.userInfo.name != currentUserName) return + void client.remoteQueueDislike.onDislikeSyncAction(action).then(async() => { + return userSpace.dislikeManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key) + }).catch(err => { + // TODO send status + client.close(SYNC_CLOSE_CODE.failed) + // client.moduleReadys.dislike = false + console.log(err.message) + }) + }) + }, +} + +export default handler diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/index.ts new file mode 100644 index 000000000..0dbd6bef9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/index.ts @@ -0,0 +1,3 @@ +export { default as handler } from './handler' +export { sync } from './sync' +export * from './localEvent' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/localEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/localEvent.ts new file mode 100644 index 000000000..e542e1930 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/localEvent.ts @@ -0,0 +1,43 @@ +import { SYNC_CLOSE_CODE } from '@common/constants_sync' +import { registerDislikeActionEvent } from '../../../../dislikeEvent' +import { getUserSpace } from '../../../user' + +// let socket: LX.Sync.Server.Socket | null +let unregisterLocalListAction: (() => void) | null + + +const sendListAction = async(wss: LX.Sync.Server.SocketServer, action: LX.Sync.Dislike.ActionList) => { + // console.log('sendListAction', action.action) + const userSpace = getUserSpace() + let key = '' + for (const client of wss.clients) { + if (!client.moduleReadys?.dislike) continue + // eslint-disable-next-line require-atomic-updates + if (!key) key = await userSpace.dislikeManage.createSnapshot() + void client.remoteQueueDislike.onDislikeSyncAction(action).then(async() => { + return userSpace.dislikeManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key) + }).catch(err => { + // TODO send status + client.close(SYNC_CLOSE_CODE.failed) + // client.moduleReadys.dislike = false + console.log(err.message) + }) + } +} + +export const registerEvent = (wss: LX.Sync.Server.SocketServer) => { + // socket = _socket + // socket.onClose(() => { + // unregisterLocalListAction?.() + // unregisterLocalListAction = null + // }) + unregisterEvent() + unregisterLocalListAction = registerDislikeActionEvent((action) => { + void sendListAction(wss, action) + }) +} + +export const unregisterEvent = () => { + unregisterLocalListAction?.() + unregisterLocalListAction = null +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/sync.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/sync.ts new file mode 100644 index 000000000..a44d29757 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/sync/sync.ts @@ -0,0 +1,235 @@ +// import { SYNC_CLOSE_CODE } from '../../../../constants' +import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain' +import { getUserSpace } from '../../../user' +import { getLocalDislikeData, setLocalDislikeData } from '@main/modules/sync/dislikeEvent' +import { SYNC_CLOSE_CODE } from '@common/constants_sync' +import { filterRules } from '../utils' +// import { LIST_IDS } from '@common/constants' + +// type ListInfoType = LX.Dislike.UserListInfoFull | LX.Dislike.MyDefaultListInfoFull | LX.Dislike.MyLoveListInfoFull + +// let wss: LX.Sync.Server.SocketServer | null +let syncingId: string | null = null +const wait = async(time = 1000) => await new Promise((resolve, reject) => setTimeout(resolve, time)) + + +const getRemoteListData = async(socket: LX.Sync.Server.Socket): Promise => { + console.log('getRemoteListData') + return (await socket.remoteQueueDislike.dislike_sync_get_list_data()) ?? '' +} + +const getRemoteDataMD5 = async(socket: LX.Sync.Server.Socket): Promise => { + return socket.remoteQueueDislike.dislike_sync_get_md5() +} + +// const getLocalDislikeData async(socket: LX.Sync.Server.Socket): Promise => { +// return getUserSpace(socket.userInfo.name).dislikeManage.getListData() +// } +const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise => new Promise((resolve, reject) => { + const handleDisconnect = (err: Error) => { + sendCloseSelectMode() + removeSelectModeListener() + reject(err) + } + let removeEventClose = socket.onClose(handleDisconnect) + sendSelectMode(socket.keyInfo.deviceName, 'dislike', (mode) => { + if (mode == null) { + reject(new Error('cancel')) + return + } + resolve(mode) + removeSelectModeListener() + removeEventClose() + }) +}) +// const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise => { +// return socket.remoteQueueDislike.list_sync_get_sync_mode() +// } + +const finishedSync = async(socket: LX.Sync.Server.Socket) => { + await socket.remoteQueueDislike.dislike_sync_finished() +} + + +const setLocalList = async(socket: LX.Sync.Server.Socket, listData: LX.Dislike.DislikeRules) => { + await setLocalDislikeData(listData) + const userSpace = getUserSpace(socket.userInfo.name) + return userSpace.dislikeManage.createSnapshot() +} + +const overwriteRemoteListData = async(socket: LX.Sync.Server.Socket, listData: LX.Dislike.DislikeRules, key: string, excludeIds: string[] = []) => { + const action = { action: 'dislike_data_overwrite', data: listData } as const + const tasks: Array> = [] + const userSpace = getUserSpace(socket.userInfo.name) + socket.broadcast((client) => { + if (excludeIds.includes(client.keyInfo.clientId) || client.userInfo?.name != socket.userInfo.name || !client.moduleReadys?.dislike) return + tasks.push(client.remoteQueueDislike.onDislikeSyncAction(action).then(async() => { + return userSpace.dislikeManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key) + }).catch(err => { + // TODO send status + client.close(SYNC_CLOSE_CODE.failed) + // client.moduleReadys.list = false + console.log(err.message) + })) + }) + if (!tasks.length) return + await Promise.all(tasks) +} +const setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Dislike.DislikeRules, key: string): Promise => { + await socket.remoteQueueDislike.dislike_sync_set_list_data(listData) + const userSpace = getUserSpace(socket.userInfo.name) + await userSpace.dislikeManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key) +} + + +const mergeList = (socket: LX.Sync.Server.Socket, sourceListData: LX.Dislike.DislikeRules, targetListData: LX.Dislike.DislikeRules): LX.Dislike.DislikeRules => { + return Array.from(filterRules(sourceListData + '\n' + targetListData)).join('\n') +} + +const handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Dislike.DislikeRules, boolean, boolean]> => { + const mode: LX.Sync.Dislike.SyncMode = await getSyncMode(socket) + + if (mode == 'cancel') throw new Error('cancel') + const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalDislikeData()]) + console.log('handleMergeListData', 'remoteListData, localListData') + let listData: LX.Dislike.DislikeRules + let requiredUpdateLocalListData = true + let requiredUpdateRemoteListData = true + switch (mode) { + case 'merge_local_remote': + listData = mergeList(socket, localListData, remoteListData) + break + case 'merge_remote_local': + listData = mergeList(socket, remoteListData, localListData) + break + case 'overwrite_local_remote': + listData = localListData + requiredUpdateLocalListData = false + break + case 'overwrite_remote_local': + listData = remoteListData + requiredUpdateRemoteListData = false + break + // case 'none': return null + // case 'cancel': + default: throw new Error('cancel') + } + return [listData, requiredUpdateLocalListData, requiredUpdateRemoteListData] +} + +const handleSyncList = async(socket: LX.Sync.Server.Socket) => { + const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalDislikeData()]) + console.log('handleSyncList', 'remoteListData, localListData') + console.log('localListData', localListData.length) + console.log('remoteListData', remoteListData.length) + const userSpace = getUserSpace(socket.userInfo.name) + const clientId = socket.keyInfo.clientId + if (localListData.length) { + if (remoteListData.length) { + const [mergedList, requiredUpdateLocalListData, requiredUpdateRemoteListData] = await handleMergeListData(socket) + console.log('handleMergeListData', 'mergedList', requiredUpdateLocalListData, requiredUpdateRemoteListData) + let key + if (requiredUpdateLocalListData) { + key = await setLocalList(socket, mergedList) + await overwriteRemoteListData(socket, mergedList, key, [clientId]) + if (!requiredUpdateRemoteListData) await userSpace.dislikeManage.updateDeviceSnapshotKey(clientId, key) + } + if (requiredUpdateRemoteListData) { + if (!key) key = await userSpace.dislikeManage.getCurrentListInfoKey() + await setRemotelList(socket, mergedList, key) + } + } else { + await setRemotelList(socket, localListData, await userSpace.dislikeManage.getCurrentListInfoKey()) + } + } else { + let key: string + if (remoteListData.length) { + key = await setLocalList(socket, remoteListData) + await overwriteRemoteListData(socket, remoteListData, key, [clientId]) + } + key ??= await userSpace.dislikeManage.getCurrentListInfoKey() + await userSpace.dislikeManage.updateDeviceSnapshotKey(clientId, key) + } +} + +const mergeDataFromSnapshot = ( + sourceList: LX.Dislike.DislikeRules, + targetList: LX.Dislike.DislikeRules, + snapshotList: LX.Dislike.DislikeRules, +): LX.Dislike.DislikeRules => { + const removedRules = new Set() + const sourceRules = filterRules(sourceList) + const targetRules = filterRules(targetList) + + if (snapshotList) { + const snapshotRules = filterRules(snapshotList) + for (const m of snapshotRules.values()) { + if (!sourceRules.has(m) || !targetRules.has(m)) removedRules.add(m) + } + } + return Array.from(new Set(Array.from([...sourceRules, ...targetRules]).filter((rule) => { + return !removedRules.has(rule) + }))).join('\n') +} +const checkListLatest = async(socket: LX.Sync.Server.Socket) => { + const remoteListMD5 = await getRemoteDataMD5(socket) + const userSpace = getUserSpace(socket.userInfo.name) + const userCurrentListInfoKey = await userSpace.dislikeManage.getDeviceCurrentSnapshotKey(socket.keyInfo.clientId) + const currentListInfoKey = await userSpace.dislikeManage.getCurrentListInfoKey() + const latest = remoteListMD5 == currentListInfoKey + if (latest && userCurrentListInfoKey != currentListInfoKey) await userSpace.dislikeManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, currentListInfoKey) + return latest +} + +const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, snapshot: LX.Dislike.DislikeRules) => { + if (await checkListLatest(socket)) return + + const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalDislikeData()]) + const newDislikeData = mergeDataFromSnapshot(localListData, remoteListData, snapshot) + + const key = await setLocalList(socket, newDislikeData) + const err = await setRemotelList(socket, newDislikeData, key).catch(err => err) + await overwriteRemoteListData(socket, newDislikeData, key, [socket.keyInfo.clientId]) + if (err) throw err +} + +const syncDislike = async(socket: LX.Sync.Server.Socket) => { + // socket.data.snapshotFilePath = getSnapshotFilePath(socket.keyInfo) + // console.log(socket.keyInfo) + if (!socket.feature.dislike) throw new Error('dislike feature options not available') + if (!socket.feature.dislike.skipSnapshot) { + const user = getUserSpace(socket.userInfo.name) + const userCurrentDislikeInfoKey = await user.dislikeManage.getDeviceCurrentSnapshotKey(socket.keyInfo.clientId) + if (userCurrentDislikeInfoKey) { + const listData = await user.dislikeManage.snapshotDataManage.getSnapshot(userCurrentDislikeInfoKey) + if (listData) { + console.log('handleMergeDislikeDataFromSnapshot') + await handleMergeListDataFromSnapshot(socket, listData) + return + } + } + } + await handleSyncList(socket) +} + +export const sync = async(socket: LX.Sync.Server.Socket) => { + let disconnected = false + socket.onClose(() => { + disconnected = true + if (syncingId == socket.keyInfo.clientId) syncingId = null + }) + + while (true) { + if (disconnected) throw new Error('disconnected') + if (!syncingId) break + await wait() + } + + syncingId = socket.keyInfo.clientId + await syncDislike(socket).then(async() => { + await finishedSync(socket) + socket.moduleReadys.dislike = true + }).finally(() => { + syncingId = null + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/utils.ts new file mode 100644 index 000000000..3dd37247c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/dislike/utils.ts @@ -0,0 +1,23 @@ +import { SPLIT_CHAR } from '@common/constants' + + +export const filterRules = (rules: string) => { + const list: string[] = [] + for (const item of rules.split('\n')) { + if (!item) continue + let [name, singer] = item.split(SPLIT_CHAR.DISLIKE_NAME) + if (name) { + name = name.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + if (singer) { + singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + list.push(`${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}`) + } else { + list.push(name) + } + } else if (singer) { + singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + list.push(`${SPLIT_CHAR.DISLIKE_NAME}${singer}`) + } + } + return new Set(list) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/index.ts new file mode 100644 index 000000000..c61c8ec14 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/index.ts @@ -0,0 +1,22 @@ +import { sync as listSync } from './list' +import { sync as dislikeSync } from './dislike' + +export const callObj = Object.assign({}, + listSync.handler, + dislikeSync.handler, +) + +export const modules = { + list: listSync, + dislike: dislikeSync, +} + + +export { ListManage } from './list' + +export { DislikeManage } from './dislike' + +export const featureVersion = { + list: 1, + dislike: 1, +} as const diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/index.ts new file mode 100644 index 000000000..68ee8f0ab --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/index.ts @@ -0,0 +1,3 @@ +export * as sync from './sync' +export { ListManage } from './manage' + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/manage.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/manage.ts new file mode 100644 index 000000000..bc9f936af --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/manage.ts @@ -0,0 +1,56 @@ +import { type UserDataManage } from '../../user' +import { SnapshotDataManage } from './snapshotDataManage' +import { toMD5 } from '../../utils' +import { getLocalListData } from '@main/modules/sync/listEvent' + +export class ListManage { + snapshotDataManage: SnapshotDataManage + + constructor(userDataManage: UserDataManage) { + this.snapshotDataManage = new SnapshotDataManage(userDataManage) + } + + createSnapshot = async() => { + const listData = JSON.stringify(await this.getListData()) + const md5 = toMD5(listData) + const snapshotInfo = await this.snapshotDataManage.getSnapshotInfo() + console.log(md5, snapshotInfo.latest) + if (snapshotInfo.latest == md5) return md5 + if (snapshotInfo.list.includes(md5)) { + snapshotInfo.list.splice(snapshotInfo.list.indexOf(md5), 1) + } else await this.snapshotDataManage.saveSnapshot(md5, listData) + if (snapshotInfo.latest) snapshotInfo.list.unshift(snapshotInfo.latest) + snapshotInfo.latest = md5 + snapshotInfo.time = Date.now() + this.snapshotDataManage.saveSnapshotInfo(snapshotInfo) + return md5 + } + + getCurrentListInfoKey = async() => { + // const snapshotInfo = await this.snapshotDataManage.getSnapshotInfo() + // if (snapshotInfo.latest) { + // return snapshotInfo.latest + // } + // snapshotInfo.latest = toMD5(JSON.stringify(await this.getListData())) + // this.snapshotDataManage.saveSnapshotInfo(snapshotInfo) + // return snapshotInfo.latest + return this.createSnapshot() + } + + getDeviceCurrentSnapshotKey = async(clientId: string) => { + return this.snapshotDataManage.getDeviceCurrentSnapshotKey(clientId) + } + + updateDeviceSnapshotKey = async(clientId: string, key: string) => { + await this.snapshotDataManage.updateDeviceSnapshotKey(clientId, key) + } + + removeDevice = async(clientId: string) => { + this.snapshotDataManage.removeSnapshotInfo(clientId) + } + + getListData = async(): Promise => { + return getLocalListData() + } +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/snapshotDataManage.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/snapshotDataManage.ts new file mode 100644 index 000000000..c46134ae2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/snapshotDataManage.ts @@ -0,0 +1,143 @@ +import { throttle } from '@common/utils/common' +import fs from 'node:fs' +import path from 'node:path' +import syncLog from '../../../log' +import { getUserConfig, type UserDataManage } from '../../user/data' +import { File } from '../../../../../../common/constants_sync' +import { checkAndCreateDirSync } from '../../utils' + + +interface SnapshotInfo { + latest: string | null + time: number + list: string[] + clients: Record +} +export class SnapshotDataManage { + userDataManage: UserDataManage + listDir: string + snapshotDir: string + snapshotInfoFilePath: string + snapshotInfo: SnapshotInfo + clientSnapshotKeys: string[] + private readonly saveSnapshotInfoThrottle: () => void + + isIncluedsDevice = (key: string) => { + return this.clientSnapshotKeys.includes(key) + } + + clearOldSnapshot = async() => { + if (!this.snapshotInfo) return + const snapshotList = this.snapshotInfo.list.filter(key => !this.isIncluedsDevice(key)) + // console.log(snapshotList.length, lx.config.maxSnapshotNum) + const userMaxSnapshotNum = getUserConfig(this.userDataManage.userName).maxSnapshotNum + let requiredSave = snapshotList.length > userMaxSnapshotNum + while (snapshotList.length > userMaxSnapshotNum) { + const name = snapshotList.pop() + if (name) { + await this.removeSnapshot(name) + this.snapshotInfo.list.splice(this.snapshotInfo.list.indexOf(name), 1) + } else break + } + if (requiredSave) this.saveSnapshotInfo(this.snapshotInfo) + } + + updateDeviceSnapshotKey = async(clientId: string, key: string) => { + // console.log('updateDeviceSnapshotKey', key) + let client = this.snapshotInfo.clients[clientId] + if (!client) client = this.snapshotInfo.clients[clientId] = { snapshotKey: '', lastSyncDate: 0 } + if (client.snapshotKey) this.clientSnapshotKeys.splice(this.clientSnapshotKeys.indexOf(client.snapshotKey), 1) + client.snapshotKey = key + client.lastSyncDate = Date.now() + this.clientSnapshotKeys.push(key) + this.saveSnapshotInfoThrottle() + } + + getDeviceCurrentSnapshotKey = async(clientId: string) => { + // console.log('updateDeviceSnapshotKey', key) + const client = this.snapshotInfo.clients[clientId] + return client?.snapshotKey + } + + getSnapshotInfo = async(): Promise => { + return this.snapshotInfo + } + + saveSnapshotInfo = (info: SnapshotInfo) => { + this.snapshotInfo = info + this.saveSnapshotInfoThrottle() + } + + removeSnapshotInfo = (clientId: string) => { + let client = this.snapshotInfo.clients[clientId] + if (!client) return + if (client.snapshotKey) this.clientSnapshotKeys.splice(this.clientSnapshotKeys.indexOf(client.snapshotKey), 1) + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.snapshotInfo.clients[clientId] + this.saveSnapshotInfoThrottle() + } + + getSnapshot = async(name: string) => { + const filePath = path.join(this.snapshotDir, `snapshot_${name}`) + let listData: LX.Sync.List.ListData + try { + listData = JSON.parse((await fs.promises.readFile(filePath)).toString('utf-8')) + } catch (err) { + syncLog.warn(err) + return null + } + return listData + } + + saveSnapshot = async(name: string, data: string) => { + syncLog.info('saveSnapshot', this.userDataManage.userName, name) + const filePath = path.join(this.snapshotDir, `snapshot_${name}`) + try { + await fs.promises.writeFile(filePath, data) + } catch (err) { + syncLog.error(err) + throw err + } + } + + removeSnapshot = async(name: string) => { + syncLog.info('removeSnapshot', this.userDataManage.userName, name) + const filePath = path.join(this.snapshotDir, `snapshot_${name}`) + try { + await fs.promises.unlink(filePath) + } catch (err) { + syncLog.error(err) + } + } + + + constructor(userDataManage: UserDataManage) { + this.userDataManage = userDataManage + + this.listDir = path.join(userDataManage.userDir, File.listDir) + checkAndCreateDirSync(this.listDir) + + this.snapshotDir = path.join(this.listDir, File.listSnapshotDir) + checkAndCreateDirSync(this.snapshotDir) + + this.snapshotInfoFilePath = path.join(this.listDir, File.listSnapshotInfoJSON) + this.snapshotInfo = fs.existsSync(this.snapshotInfoFilePath) + ? JSON.parse(fs.readFileSync(this.snapshotInfoFilePath).toString()) + : { latest: null, time: 0, list: [], clients: {} } + + this.saveSnapshotInfoThrottle = throttle(() => { + fs.writeFile(this.snapshotInfoFilePath, JSON.stringify(this.snapshotInfo), 'utf8', (err) => { + if (err) console.error(err) + void this.clearOldSnapshot() + }) + }) + + this.clientSnapshotKeys = Object.values(this.snapshotInfo.clients).map(device => device.snapshotKey).filter(k => k) + } +} +// type UserDataManages = Map + +// export const createUserDataManage = (user: LX.UserConfig) => { +// const manage = Object.create(userDataManage) as typeof userDataManage +// manage.userDir = user.dataPath +// } diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/handler.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/handler.ts new file mode 100644 index 000000000..24ac62566 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/handler.ts @@ -0,0 +1,172 @@ +// 这个文件导出的方法将暴露给客户端调用,第一个参数固定为当前 socket 对象 +// import { throttle } from '@common/utils/common' +// import { sendSyncActionList } from '@main/modules/winMain' +// import { SYNC_CLOSE_CODE } from '@/constants' +// import { SYNC_CLOSE_CODE } from '@common/constants_sync' +import { SYNC_CLOSE_CODE } from '@common/constants_sync' +import { getUserSpace } from '@main/modules/sync/server/user' +import { handleRemoteListAction } from '@main/modules/sync/listEvent' +// import { encryptMsg } from '@/utils/tools' + +// let wss: LX.SocketServer | null +// let removeListener: (() => void) | null + +// type listAction = 'list:action' + +// const registerListActionEvent = () => { +// const list_data_overwrite = async(listData: MakeOptional, isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_data_overwrite', data: listData }) +// } +// const list_create = async(position: number, listInfos: LX.List.UserListInfo[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_create', data: { position, listInfos } }) +// } +// const list_remove = async(ids: string[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_remove', data: ids }) +// } +// const list_update = async(lists: LX.List.UserListInfo[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_update', data: lists }) +// } +// const list_update_position = async(position: number, ids: string[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_update_position', data: { position, ids } }) +// } +// const list_music_overwrite = async(listId: string, musicInfos: LX.Music.MusicInfo[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_overwrite', data: { listId, musicInfos } }) +// } +// const list_music_add = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_add', data: { id, musicInfos, addMusicLocationType } }) +// } +// const list_music_move = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_move', data: { fromId, toId, musicInfos, addMusicLocationType } }) +// } +// const list_music_remove = async(listId: string, ids: string[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_remove', data: { listId, ids } }) +// } +// const list_music_update = async(musicInfos: LX.List.ListActionMusicUpdate, isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_update', data: musicInfos }) +// } +// const list_music_clear = async(ids: string[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_clear', data: ids }) +// } +// const list_music_update_position = async(listId: string, position: number, ids: string[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_update_position', data: { listId, position, ids } }) +// } +// global.event_list.on('list_data_overwrite', list_data_overwrite) +// global.event_list.on('list_create', list_create) +// global.event_list.on('list_remove', list_remove) +// global.event_list.on('list_update', list_update) +// global.event_list.on('list_update_position', list_update_position) +// global.event_list.on('list_music_overwrite', list_music_overwrite) +// global.event_list.on('list_music_add', list_music_add) +// global.event_list.on('list_music_move', list_music_move) +// global.event_list.on('list_music_remove', list_music_remove) +// global.event_list.on('list_music_update', list_music_update) +// global.event_list.on('list_music_clear', list_music_clear) +// global.event_list.on('list_music_update_position', list_music_update_position) +// return () => { +// global.event_list.off('list_data_overwrite', list_data_overwrite) +// global.event_list.off('list_create', list_create) +// global.event_list.off('list_remove', list_remove) +// global.event_list.off('list_update', list_update) +// global.event_list.off('list_update_position', list_update_position) +// global.event_list.off('list_music_overwrite', list_music_overwrite) +// global.event_list.off('list_music_add', list_music_add) +// global.event_list.off('list_music_move', list_music_move) +// global.event_list.off('list_music_remove', list_music_remove) +// global.event_list.off('list_music_update', list_music_update) +// global.event_list.off('list_music_clear', list_music_clear) +// global.event_list.off('list_music_update_position', list_music_update_position) +// } +// } + +// const addMusic = (orderId, callback) => { +// // ... +// } + +// const broadcast = async(socket: LX.Socket, key: string, data: any, excludeIds: string[] = []) => { +// if (!wss) return +// const dataStr = JSON.stringify({ action: 'list:sync:action', data }) +// const userSpace = getUserSpace(socket.userInfo.name) +// for (const client of wss.clients) { +// if (excludeIds.includes(client.keyInfo.clientId) || !client.isReady || client.userInfo.name != socket.userInfo.name) continue +// client.send(encryptMsg(client.keyInfo, dataStr), (err) => { +// if (err) { +// client.close(SYNC_CLOSE_CODE.failed) +// return +// } +// userSpace.dataManage.updateDeviceSnapshotKey(client.keyInfo, key) +// }) +// } +// } + +// export const sendListAction = async(action: LX.Sync.List.ActionList) => { +// console.log('sendListAction', action.action) +// // io.sockets +// await broadcast('list:sync:action', action) +// } + +// export const registerListHandler = (_wss: LX.SocketServer, socket: LX.Socket) => { +// if (!wss) { +// wss = _wss +// // removeListener = registerListActionEvent() +// } + +// const userSpace = getUserSpace(socket.userInfo.name) +// socket.onRemoteEvent('list:sync:action', (action) => { +// if (!socket.isReady) return +// // console.log(msg) +// void handleListAction(socket.userInfo.name, action).then(key => { +// if (!key) return +// console.log(key) +// userSpace.dataManage.updateDeviceSnapshotKey(socket.keyInfo, key) +// void broadcast(socket, key, action, [socket.keyInfo.clientId]) +// }) +// // socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } }) +// }) + +// // socket.on('list:add', addMusic) +// } +// export const unregisterListHandler = () => { +// wss = null + +// // if (removeListener) { +// // removeListener() +// // removeListener = null +// // } +// } + +const handler: LX.Sync.ServerSyncHandlerListActions = { + async onListSyncAction(socket, action) { + if (!socket.moduleReadys.list) return + await handleRemoteListAction(action) + const userSpace = getUserSpace(socket.userInfo.name) + const key = await userSpace.listManage.createSnapshot() + userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key) + const currentUserName = socket.userInfo.name + const currentId = socket.keyInfo.clientId + socket.broadcast((client) => { + if (client.keyInfo.clientId == currentId || !client.moduleReadys?.list || client.userInfo.name != currentUserName) return + void client.remoteQueueList.onListSyncAction(action).then(async() => { + return userSpace.listManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key) + }).catch(err => { + // TODO send status + client.close(SYNC_CLOSE_CODE.failed) + // client.moduleReadys.list = false + console.log(err.message) + }) + }) + }, +} + +export default handler diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/index.ts new file mode 100644 index 000000000..0dbd6bef9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/index.ts @@ -0,0 +1,3 @@ +export { default as handler } from './handler' +export { sync } from './sync' +export * from './localEvent' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/localEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/localEvent.ts new file mode 100644 index 000000000..b996c9794 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/localEvent.ts @@ -0,0 +1,43 @@ +import { SYNC_CLOSE_CODE } from '@common/constants_sync' +import { registerListActionEvent } from '../../../../listEvent' +import { getUserSpace } from '../../../user' + +// let socket: LX.Sync.Server.Socket | null +let unregisterLocalListAction: (() => void) | null + + +const sendListAction = async(wss: LX.Sync.Server.SocketServer, action: LX.Sync.List.ActionList) => { + // console.log('sendListAction', action.action) + const userSpace = getUserSpace() + let key = '' + for (const client of wss.clients) { + if (!client.moduleReadys?.list) continue + // eslint-disable-next-line require-atomic-updates + if (!key) key = await userSpace.listManage.createSnapshot() + void client.remoteQueueList.onListSyncAction(action).then(async() => { + return userSpace.listManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key) + }).catch(err => { + // TODO send status + client.close(SYNC_CLOSE_CODE.failed) + // client.moduleReadys.list = false + console.log(err.message) + }) + } +} + +export const registerEvent = (wss: LX.Sync.Server.SocketServer) => { + // socket = _socket + // socket.onClose(() => { + // unregisterLocalListAction?.() + // unregisterLocalListAction = null + // }) + unregisterEvent() + unregisterLocalListAction = registerListActionEvent((action) => { + void sendListAction(wss, action) + }) +} + +export const unregisterEvent = () => { + unregisterLocalListAction?.() + unregisterLocalListAction = null +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/sync.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/sync.ts new file mode 100644 index 000000000..765e7e239 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/modules/list/sync/sync.ts @@ -0,0 +1,470 @@ +// import { SYNC_CLOSE_CODE } from '../../../../constants' +import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain' +import { getUserSpace, getUserConfig } from '../../../user' +import { buildUserListInfoFull, getLocalListData, setLocalListData } from '@main/modules/sync/listEvent' +import { SYNC_CLOSE_CODE } from '@common/constants_sync' +// import { LIST_IDS } from '@common/constants' + +// type ListInfoType = LX.List.UserListInfoFull | LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull + +// let wss: LX.Sync.Server.SocketServer | null +let syncingId: string | null = null +const wait = async(time = 1000) => await new Promise((resolve, reject) => setTimeout(resolve, time)) + +const patchListData = (listData: Partial): LX.Sync.List.ListData => { + return Object.assign({ + defaultList: [], + loveList: [], + userList: [], + }, listData) +} + +const getRemoteListData = async(socket: LX.Sync.Server.Socket): Promise => { + console.log('getRemoteListData') + return patchListData(await socket.remoteQueueList.list_sync_get_list_data()) +} + +const getRemoteListMD5 = async(socket: LX.Sync.Server.Socket): Promise => { + return socket.remoteQueueList.list_sync_get_md5() +} + +// const getLocalListData = async(socket: LX.Sync.Server.Socket): Promise => { +// return getUserSpace(socket.userInfo.name).listManage.getListData() +// } +const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise => new Promise((resolve, reject) => { + const handleDisconnect = (err: Error) => { + sendCloseSelectMode() + removeSelectModeListener() + reject(err) + } + let removeEventClose = socket.onClose(handleDisconnect) + sendSelectMode(socket.keyInfo.deviceName, 'list', (mode) => { + if (mode == null) { + reject(new Error('cancel')) + return + } + resolve(mode) + removeSelectModeListener() + removeEventClose() + }) +}) +// const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise => { +// return socket.remoteQueueList.list_sync_get_sync_mode() +// } + +const finishedSync = async(socket: LX.Sync.Server.Socket) => { + await socket.remoteQueueList.list_sync_finished() +} + + +const setLocalList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.List.ListData) => { + await setLocalListData(listData) + const userSpace = getUserSpace(socket.userInfo.name) + return userSpace.listManage.createSnapshot() +} + +const overwriteRemoteListData = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.List.ListData, key: string, excludeIds: string[] = []) => { + const action = { action: 'list_data_overwrite', data: listData } as const + const tasks: Array> = [] + const userSpace = getUserSpace(socket.userInfo.name) + socket.broadcast((client) => { + if (excludeIds.includes(client.keyInfo.clientId) || client.userInfo.name != socket.userInfo.name || !client.moduleReadys?.list) return + tasks.push(client.remoteQueueList.onListSyncAction(action).then(async() => { + return userSpace.listManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key) + }).catch(err => { + // TODO send status + client.close(SYNC_CLOSE_CODE.failed) + // client.moduleReadys.list = false + console.log(err.message) + })) + }) + if (!tasks.length) return + await Promise.all(tasks) +} +const setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.List.ListData, key: string): Promise => { + await socket.remoteQueueList.list_sync_set_list_data(listData) + const userSpace = getUserSpace(socket.userInfo.name) + await userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key) +} + +type UserDataObj = Map +const createUserListDataObj = (listData: LX.Sync.List.ListData): UserDataObj => { + const userListDataObj: UserDataObj = new Map() + for (const list of listData.userList) userListDataObj.set(list.id, list) + return userListDataObj +} + +const handleMergeList = ( + sourceList: LX.Music.MusicInfo[], + targetList: LX.Music.MusicInfo[], + addMusicLocationType: LX.AddMusicLocationType, +): LX.Music.MusicInfo[] => { + let newList + switch (addMusicLocationType) { + case 'top': + newList = [...targetList, ...sourceList] + break + case 'bottom': + default: + newList = [...sourceList, ...targetList] + break + } + const map = new Map() + const ids: Array = [] + switch (addMusicLocationType) { + case 'top': + newList = [...targetList, ...sourceList] + for (let i = newList.length - 1; i > -1; i--) { + const item = newList[i] + if (map.has(item.id)) continue + ids.unshift(item.id) + map.set(item.id, item) + } + break + case 'bottom': + default: + newList = [...sourceList, ...targetList] + for (const item of newList) { + if (map.has(item.id)) continue + ids.push(item.id) + map.set(item.id, item) + } + break + } + return ids.map(id => map.get(id)) as LX.Music.MusicInfo[] +} +const mergeList = (socket: LX.Sync.Server.Socket, sourceListData: LX.Sync.List.ListData, targetListData: LX.Sync.List.ListData): LX.Sync.List.ListData => { + const addMusicLocationType = getUserConfig(socket.userInfo.name)['list.addMusicLocationType'] + const newListData: LX.Sync.List.ListData = { + defaultList: [], + loveList: [], + userList: [], + } + newListData.defaultList = handleMergeList(sourceListData.defaultList, targetListData.defaultList, addMusicLocationType) + newListData.loveList = handleMergeList(sourceListData.loveList, targetListData.loveList, addMusicLocationType) + + const userListDataObj = createUserListDataObj(sourceListData) + newListData.userList = [...sourceListData.userList] + + targetListData.userList.forEach((list, index) => { + const targetUpdateTime = list?.locationUpdateTime ?? 0 + const sourceList = userListDataObj.get(list.id) + if (sourceList) { + sourceList.list = handleMergeList(sourceList.list, list.list, addMusicLocationType) + + const sourceUpdateTime = sourceList?.locationUpdateTime ?? 0 + if (targetUpdateTime >= sourceUpdateTime) return + // 调整位置 + const [newList] = newListData.userList.splice(newListData.userList.findIndex(l => l.id == list.id), 1) + newList.locationUpdateTime = targetUpdateTime + newListData.userList.splice(index, 0, newList) + } else { + if (targetUpdateTime) { + newListData.userList.splice(index, 0, list) + } else { + newListData.userList.push(list) + } + } + }) + + return newListData +} +const overwriteList = (sourceListData: LX.Sync.List.ListData, targetListData: LX.Sync.List.ListData): LX.Sync.List.ListData => { + const newListData: LX.Sync.List.ListData = { + defaultList: [], + loveList: [], + userList: [], + } + newListData.defaultList = sourceListData.defaultList + newListData.loveList = sourceListData.loveList + + const userListDataObj = createUserListDataObj(sourceListData) + newListData.userList = [...sourceListData.userList] + + targetListData.userList.forEach((list, index) => { + if (userListDataObj.has(list.id)) return + if (list?.locationUpdateTime) { + newListData.userList.splice(index, 0, list) + } else { + newListData.userList.push(list) + } + }) + + return newListData +} + +const handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Sync.List.ListData, boolean, boolean]> => { + const mode: LX.Sync.List.SyncMode = await getSyncMode(socket) + + if (mode == 'cancel') throw new Error('cancel') + const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()]) + console.log('handleMergeListData', 'remoteListData, localListData') + let listData: LX.Sync.List.ListData + let requiredUpdateLocalListData = true + let requiredUpdateRemoteListData = true + switch (mode) { + case 'merge_local_remote': + listData = mergeList(socket, localListData, remoteListData) + break + case 'merge_remote_local': + listData = mergeList(socket, remoteListData, localListData) + break + case 'overwrite_local_remote': + listData = overwriteList(localListData, remoteListData) + break + case 'overwrite_remote_local': + listData = overwriteList(remoteListData, localListData) + break + case 'overwrite_local_remote_full': + listData = localListData + requiredUpdateLocalListData = false + break + case 'overwrite_remote_local_full': + listData = remoteListData + requiredUpdateRemoteListData = false + break + // case 'none': return null + // case 'cancel': + default: throw new Error('cancel') + } + return [listData, requiredUpdateLocalListData, requiredUpdateRemoteListData] +} + +const handleSyncList = async(socket: LX.Sync.Server.Socket) => { + const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()]) + console.log('handleSyncList', 'remoteListData, localListData') + console.log('localListData', localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) + console.log('remoteListData', remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) + const userSpace = getUserSpace(socket.userInfo.name) + const clientId = socket.keyInfo.clientId + if (localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) { + if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) { + const [mergedList, requiredUpdateLocalListData, requiredUpdateRemoteListData] = await handleMergeListData(socket) + console.log('handleMergeListData', 'mergedList', requiredUpdateLocalListData, requiredUpdateRemoteListData) + let key + if (requiredUpdateLocalListData) { + key = await setLocalList(socket, mergedList) + await overwriteRemoteListData(socket, mergedList, key, [clientId]) + if (!requiredUpdateRemoteListData) await userSpace.listManage.updateDeviceSnapshotKey(clientId, key) + } + if (requiredUpdateRemoteListData) { + if (!key) key = await userSpace.listManage.getCurrentListInfoKey() + await setRemotelList(socket, mergedList, key) + } + } else { + await setRemotelList(socket, localListData, await userSpace.listManage.getCurrentListInfoKey()) + } + } else { + let key: string + if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) { + key = await setLocalList(socket, remoteListData) + await overwriteRemoteListData(socket, remoteListData, key, [clientId]) + } + key ??= await userSpace.listManage.getCurrentListInfoKey() + await userSpace.listManage.updateDeviceSnapshotKey(clientId, key) + } +} + +const mergeListDataFromSnapshot = ( + sourceList: LX.Music.MusicInfo[], + targetList: LX.Music.MusicInfo[], + snapshotList: LX.Music.MusicInfo[], + addMusicLocationType: LX.AddMusicLocationType, +): LX.Music.MusicInfo[] => { + const removedListIds = new Set() + const sourceListItemIds = new Set() + const targetListItemIds = new Set() + for (const m of sourceList) sourceListItemIds.add(m.id) + for (const m of targetList) targetListItemIds.add(m.id) + if (snapshotList) { + for (const m of snapshotList) { + if (!sourceListItemIds.has(m.id) || !targetListItemIds.has(m.id)) removedListIds.add(m.id) + } + } + + let newList + const map = new Map() + const ids = [] + switch (addMusicLocationType) { + case 'top': + newList = [...targetList, ...sourceList] + for (let i = newList.length - 1; i > -1; i--) { + const item = newList[i] + if (map.has(item.id) || removedListIds.has(item.id)) continue + ids.unshift(item.id) + map.set(item.id, item) + } + break + case 'bottom': + default: + newList = [...sourceList, ...targetList] + for (const item of newList) { + if (map.has(item.id) || removedListIds.has(item.id)) continue + ids.push(item.id) + map.set(item.id, item) + } + break + } + return ids.map(id => map.get(id)) as LX.Music.MusicInfo[] +} +const checkListLatest = async(socket: LX.Sync.Server.Socket) => { + const remoteListMD5 = await getRemoteListMD5(socket) + const userSpace = getUserSpace(socket.userInfo.name) + const userCurrentListInfoKey = await userSpace.listManage.getDeviceCurrentSnapshotKey(socket.keyInfo.clientId) + const currentListInfoKey = await userSpace.listManage.getCurrentListInfoKey() + const latest = remoteListMD5 == currentListInfoKey + if (latest && userCurrentListInfoKey != currentListInfoKey) await userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, currentListInfoKey) + return latest +} +const selectData = (snapshot: T | null, local: T, remote: T): T => { + return snapshot == local + ? remote + // ? (snapshot == remote ? snapshot as T : remote) + : local +} +const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, snapshot: LX.Sync.List.ListData) => { + if (await checkListLatest(socket)) return + + const addMusicLocationType = getUserConfig(socket.userInfo.name)['list.addMusicLocationType'] + const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()]) + const newListData: LX.Sync.List.ListData = { + defaultList: [], + loveList: [], + userList: [], + } + newListData.defaultList = mergeListDataFromSnapshot(localListData.defaultList, remoteListData.defaultList, snapshot.defaultList, addMusicLocationType) + newListData.loveList = mergeListDataFromSnapshot(localListData.loveList, remoteListData.loveList, snapshot.loveList, addMusicLocationType) + const localUserListData = createUserListDataObj(localListData) + const remoteUserListData = createUserListDataObj(remoteListData) + const snapshotUserListData = createUserListDataObj(snapshot) + const removedListIds = new Set() + const localUserListIds = new Set() + const remoteUserListIds = new Set() + + for (const l of localListData.userList) localUserListIds.add(l.id) + for (const l of remoteListData.userList) remoteUserListIds.add(l.id) + + for (const l of snapshot.userList) { + if (!localUserListIds.has(l.id) || !remoteUserListIds.has(l.id)) removedListIds.add(l.id) + } + + let newUserList: LX.List.UserListInfoFull[] = [] + for (const list of localListData.userList) { + if (removedListIds.has(list.id)) continue + const remoteList = remoteUserListData.get(list.id) + let newList: LX.List.UserListInfoFull + if (remoteList) { + const snapshotList = snapshotUserListData.get(list.id) ?? { name: null, source: null, sourceListId: null, list: [] } + newList = buildUserListInfoFull({ + id: list.id, + name: selectData(snapshotList.name, list.name, remoteList.name), + source: selectData(snapshotList.source, list.source, remoteList.source), + sourceListId: selectData(snapshotList.sourceListId, list.sourceListId, remoteList.sourceListId), + locationUpdateTime: list.locationUpdateTime, + list: mergeListDataFromSnapshot(list.list, remoteList.list, snapshotList.list, addMusicLocationType), + }) + } else { + newList = { ...list } + } + newUserList.push(newList) + } + + remoteListData.userList.forEach((list, index) => { + if (removedListIds.has(list.id)) return + const remoteUpdateTime = list?.locationUpdateTime ?? 0 + if (localUserListData.has(list.id)) { + const localUpdateTime = localUserListData.get(list.id)?.locationUpdateTime ?? 0 + if (localUpdateTime >= remoteUpdateTime) return + // 调整位置 + const [newList] = newUserList.splice(newUserList.findIndex(l => l.id == list.id), 1) + newList.locationUpdateTime = localUpdateTime + newUserList.splice(index, 0, newList) + } else { + if (remoteUpdateTime) { + newUserList.splice(index, 0, { ...list }) + } else { + newUserList.push({ ...list }) + } + } + }) + + newListData.userList = newUserList + const key = await setLocalList(socket, newListData) + const err = await setRemotelList(socket, newListData, key).catch(err => err) + await overwriteRemoteListData(socket, newListData, key, [socket.keyInfo.clientId]) + if (err) throw err +} + +const syncList = async(socket: LX.Sync.Server.Socket) => { + // socket.data.snapshotFilePath = getSnapshotFilePath(socket.keyInfo) + // console.log(socket.keyInfo) + if (!socket.feature.list) throw new Error('list feature options not available') + if (!socket.feature.list.skipSnapshot) { + const user = getUserSpace(socket.userInfo.name) + const userCurrentListInfoKey = await user.listManage.getDeviceCurrentSnapshotKey(socket.keyInfo.clientId) + if (userCurrentListInfoKey) { + const listData = await user.listManage.snapshotDataManage.getSnapshot(userCurrentListInfoKey) + if (listData) { + console.log('handleMergeListDataFromSnapshot') + await handleMergeListDataFromSnapshot(socket, listData) + return + } + } + } + await handleSyncList(socket) +} + +// export default async(_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => { +// if (!wss) { +// wss = _wss +// _wss.addListener('close', () => { +// wss = null +// }) +// } + +// let disconnected = false +// socket.onClose(() => { +// disconnected = true +// if (syncingId == socket.keyInfo.clientId) syncingId = null +// }) + +// while (true) { +// if (disconnected) throw new Error('disconnected') +// if (!syncingId) break +// await wait() +// } + +// syncingId = socket.keyInfo.clientId +// await syncList(socket).then(async() => { +// return finishedSync(socket) +// }).finally(() => { +// syncingId = null +// }) +// } + +// const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => { +// const filePath = getSnapshotFilePath(keyInfo) +// await fsPromises.unlink(filePath) +// } + +export const sync = async(socket: LX.Sync.Server.Socket) => { + let disconnected = false + socket.onClose(() => { + disconnected = true + if (syncingId == socket.keyInfo.clientId) syncingId = null + }) + + while (true) { + if (disconnected) throw new Error('disconnected') + if (!syncingId) break + await wait() + } + + syncingId = socket.keyInfo.clientId + await syncList(socket).then(async() => { + await finishedSync(socket) + socket.moduleReadys.list = true + }).finally(() => { + syncingId = null + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/auth.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/auth.ts new file mode 100644 index 000000000..df15d0d2c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/auth.ts @@ -0,0 +1,130 @@ +import type http from 'http' +import { + aesEncrypt, + aesDecrypt, + rsaEncrypt, + getIP, +} from '../utils/tools' +import querystring from 'node:querystring' +import { getUserSpace, createClientKeyInfo } from '../user' +import { toMD5 } from '../utils' +import { getComputerName } from '../../utils' +import { SYNC_CODE } from '@common/constants_sync' + +const requestIps = new Map() + +const getAvailableIP = (req: http.IncomingMessage) => { + let ip = getIP(req) + return ip && (requestIps.get(ip) ?? 0) < 10 ? ip : null +} + +const verifyByKey = (encryptMsg: string, userId: string) => { + const userSpace = getUserSpace() + const keyInfo = userSpace.dataManage.getClientKeyInfo(userId) + if (!keyInfo) return null + let text + try { + text = aesDecrypt(encryptMsg, keyInfo.key) + } catch (err) { + return null + } + // console.log(text) + if (text.startsWith(SYNC_CODE.authMsg)) { + const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown' + if (deviceName != keyInfo.deviceName) { + keyInfo.deviceName = deviceName + userSpace.dataManage.saveClientKeyInfo(keyInfo) + } + return aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key) + } + return null +} + +const verifyByCode = (encryptMsg: string, password: string) => { + let key = toMD5(password).substring(0, 16) + // const iv = Buffer.from(key.split('').reverse().join('')).toString('base64') + key = Buffer.from(key).toString('base64') + // console.log(req.headers.m, authCode, key) + let text + try { + text = aesDecrypt(encryptMsg, key) + } catch { + return null + } + // console.log(text) + if (text.startsWith(SYNC_CODE.authMsg)) { + const data = text.split('\n') + const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----` + const deviceName = data[2] || 'Unknown' + const isMobile = data[3] == 'lx_music_mobile' + const keyInfo = createClientKeyInfo(deviceName, isMobile) + const userSpace = getUserSpace() + userSpace.dataManage.saveClientKeyInfo(keyInfo) + return rsaEncrypt(Buffer.from(JSON.stringify({ + clientId: keyInfo.clientId, + key: keyInfo.key, + serverName: getComputerName(), + })), publicKey) + } + return null +} + +export const authCode = async(req: http.IncomingMessage, res: http.ServerResponse, password: string) => { + let code = 401 + let msg: string = SYNC_CODE.msgAuthFailed + + let ip = getAvailableIP(req) + if (ip) { + if (typeof req.headers.m == 'string' && req.headers.m) { + const userId = req.headers.i + const _msg = typeof userId == 'string' && userId + ? verifyByKey(req.headers.m, userId) + : verifyByCode(req.headers.m, password) + if (_msg != null) { + msg = _msg + code = 200 + } + } + + if (code != 200) { + const num = requestIps.get(ip) ?? 0 + // if (num > 20) return + requestIps.set(ip, num + 1) + } + } else { + code = 403 + msg = SYNC_CODE.msgBlockedIp + } + // console.log(req.headers) + + res.writeHead(code) + res.end(msg) +} + +const verifyConnection = (encryptMsg: string, userId: string) => { + const userSpace = getUserSpace() + const keyInfo = userSpace.dataManage.getClientKeyInfo(userId) + if (!keyInfo) return false + let text + try { + text = aesDecrypt(encryptMsg, keyInfo.key) + } catch (err) { + return false + } + // console.log(text) + return text == SYNC_CODE.msgConnect +} +export const authConnect = async(req: http.IncomingMessage) => { + let ip = getAvailableIP(req) + if (ip) { + const query = querystring.parse((req.url!).split('?')[1]) + const i = query.i + const t = query.t + if (typeof i == 'string' && typeof t == 'string' && verifyConnection(t, i)) return + + const num = requestIps.get(ip) ?? 0 + requestIps.set(ip, num + 1) + } + throw new Error('failed') +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/index.ts new file mode 100644 index 000000000..789db3695 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/index.ts @@ -0,0 +1,8 @@ +export { + startServer, + stopServer, + getStatus, + generateCode, + getDevices, + removeDevice, +} from './server' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/server.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/server.ts new file mode 100644 index 000000000..a1cdcaf57 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/server.ts @@ -0,0 +1,415 @@ +import http, { type IncomingMessage } from 'node:http' +import { WebSocketServer } from 'ws' +import { registerLocalSyncEvent, callObj, sync, unregisterLocalSyncEvent } from './sync' +import { authCode, authConnect } from './auth' +import { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants_sync' +import { getUserSpace, releaseUserSpace, getServerId, initServerInfo } from '../user' +import { createMsg2call } from 'message2call' +import log from '../../log' +import { sendServerStatus } from '@main/modules/winMain' +import { decryptMsg, encryptMsg, generateCode as handleGenerateCode } from '../utils/tools' +import migrateData from '../../migrate' +import type { Socket } from 'node:net' +import { getAddress } from '@common/utils/nodejs' + + +let status: LX.Sync.ServerStatus = { + status: false, + message: '', + address: [], + code: '', + devices: [], +} + +let stopingServer = false + +let host = 'http://localhost' + +const codeTools: { + timeout: NodeJS.Timeout | null + start: () => void + stop: () => void +} = { + timeout: null, + start() { + this.stop() + this.timeout = setInterval(() => { + void handleGenerateCode() + }, 60 * 3 * 1000) + }, + stop() { + if (!this.timeout) return + clearInterval(this.timeout) + this.timeout = null + }, +} + +const checkDuplicateClient = (newSocket: LX.Sync.Server.Socket) => { + for (const client of [...wss!.clients]) { + if (client === newSocket || client.keyInfo.clientId != newSocket.keyInfo.clientId) continue + log.info('duplicate client', client.userInfo.name, client.keyInfo.deviceName) + client.isReady = false + for (const name of Object.keys(client.moduleReadys) as Array) { + client.moduleReadys[name] = false + } + client.close(SYNC_CLOSE_CODE.normal) + } +} + +const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingMessage) => { + const queryData = new URL(request.url!, host).searchParams + const clientId = queryData.get('i') + + // // if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true) + const userSpace = getUserSpace() + const keyInfo = userSpace.dataManage.getClientKeyInfo(clientId) + if (!keyInfo) { + socket.close(SYNC_CLOSE_CODE.failed) + return + } + keyInfo.lastConnectDate = Date.now() + userSpace.dataManage.saveClientKeyInfo(keyInfo) + // // socket.lx_keyInfo = keyInfo + socket.keyInfo = keyInfo + socket.userInfo = { name: 'default' } + + checkDuplicateClient(socket) + + try { + await sync(socket) + } catch (err) { + // console.log(err) + log.warn(err) + return + } + status.devices.push(keyInfo) + // handleConnection(io, socket) + sendServerStatus(status) + socket.onClose(() => { + status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo.clientId), 1) + sendServerStatus(status) + }) + + // console.log('connection', keyInfo.deviceName) + log.info('connection', keyInfo.deviceName) + // console.log(socket.handshake.query) + + socket.isReady = true +} + +const handleUnconnection = () => { + // console.log('unconnection') + releaseUserSpace() +} + +const authConnection = (req: http.IncomingMessage, callback: (err: string | null | undefined, success: boolean) => void) => { + // console.log(req.headers) + // // console.log(req.auth) + // console.log(req._query.authCode) + authConnect(req).then(() => { + callback(null, true) + }).catch(err => { + callback(err, false) + }) +} + +let wss: LX.Sync.Server.SocketServer | null +let httpServer: http.Server +let sockets = new Set() + +function noop() {} +function onSocketError(err: Error) { + console.error(err) +} + +const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promise((resolve, reject) => { + httpServer = http.createServer((req, res) => { + // console.log(req.url) + const endUrl = `/${req.url?.split('/').at(-1) ?? ''}` + let code + let msg + switch (endUrl) { + case '/hello': + code = 200 + msg = SYNC_CODE.helloMsg + break + case '/id': + code = 200 + msg = SYNC_CODE.idPrefix + getServerId() + break + case '/ah': + void authCode(req, res, status.code) + break + default: + code = 401 + msg = 'Forbidden' + break + } + if (!code) return + res.writeHead(code) + res.end(msg) + }) + + wss = new WebSocketServer({ + noServer: true, + }) + + wss.on('connection', function(socket, request) { + socket.isReady = false + socket.moduleReadys = { + list: false, + dislike: false, + } + socket.feature = { + list: false, + dislike: false, + } + socket.on('pong', () => { + socket.isAlive = true + }) + + // const events = new Map void>>() + // const events = new Map void>>() + // let events: Partial<{ [K in keyof LX.Sync.ActionSyncType]: Array<(data: LX.Sync.ActionSyncType[K]) => void> }> = {} + let closeEvents: Array<(err: Error) => (void | Promise)> = [] + let disconnected = false + const msg2call = createMsg2call({ + funcsObj: callObj, + timeout: 120 * 1000, + sendMessage(data) { + if (disconnected) throw new Error('disconnected') + void encryptMsg(socket.keyInfo, JSON.stringify(data)).then((data) => { + // console.log('sendData', eventName) + socket.send(data) + }).catch(err => { + log.error('encrypt message error:', err) + log.error(err.message) + socket.close(SYNC_CLOSE_CODE.failed) + }) + }, + onCallBeforeParams(rawArgs) { + return [socket, ...rawArgs] + }, + onError(error, path, groupName) { + const name = groupName ?? '' + const deviceName = socket.keyInfo?.deviceName ?? '' + log.error(`sync call ${deviceName} ${name} ${path.join('.')} error:`, error) + // if (groupName == null) return + // socket.close(SYNC_CLOSE_CODE.failed) + }, + }) + socket.remote = msg2call.remote + socket.remoteQueueList = msg2call.createQueueRemote('list') + socket.remoteQueueDislike = msg2call.createQueueRemote('dislike') + socket.addEventListener('message', ({ data }) => { + if (typeof data != 'string') return + void decryptMsg(socket.keyInfo, data).then((data) => { + let syncData: any + try { + syncData = JSON.parse(data) + } catch (err) { + log.error('parse message error:', err) + socket.close(SYNC_CLOSE_CODE.failed) + return + } + msg2call.message(syncData) + }).catch(err => { + log.error('decrypt message error:', err) + log.error(err.message) + socket.close(SYNC_CLOSE_CODE.failed) + }) + }) + socket.addEventListener('close', () => { + const err = new Error('closed') + try { + for (const handler of closeEvents) void handler(err) + } catch (err: any) { + log.error(err?.message) + } + closeEvents = [] + disconnected = true + msg2call.destroy() + if (socket.isReady) { + log.info('deconnection', socket.userInfo.name, socket.keyInfo.deviceName) + // events = {} + if (!status.devices.length) handleUnconnection() + } else { + const queryData = new URL(request.url!, host).searchParams + log.info('deconnection', queryData.get('i')) + } + }) + socket.onClose = function(handler: typeof closeEvents[number]) { + closeEvents.push(handler) + return () => { + closeEvents.splice(closeEvents.indexOf(handler), 1) + } + } + socket.broadcast = function(handler) { + if (!wss) return + for (const client of wss.clients) handler(client) + } + + void handleConnection(socket, request) + }) + + httpServer.on('upgrade', function upgrade(request, socket, head) { + socket.addListener('error', onSocketError) + // This function is not defined on purpose. Implement it with your own logic. + authConnection(request, err => { + if (err) { + console.log(err) + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n') + socket.destroy() + return + } + socket.removeListener('error', onSocketError) + + wss?.handleUpgrade(request, socket, head, function done(ws) { + wss?.emit('connection', ws, request) + }) + }) + }) + + const interval = setInterval(() => { + wss?.clients.forEach(socket => { + if (socket.isAlive == false) { + log.info('alive check false:', socket.userInfo.name, socket.keyInfo.deviceName) + socket.terminate() + return + } + + socket.isAlive = false + socket.ping(noop) + if (socket.keyInfo.isMobile) socket.send('ping', noop) + }) + }, 30000) + + wss.on('close', function close() { + clearInterval(interval) + }) + + httpServer.on('error', error => { + console.log(error) + reject(error) + }) + httpServer.on('connection', (socket) => { + sockets.add(socket) + socket.once('close', () => { + sockets.delete(socket) + }) + }) + + httpServer.on('listening', () => { + const addr = httpServer.address() + // console.log(addr) + if (!addr) { + reject(new Error('address is null')) + return + } + const bind = typeof addr == 'string' ? `pipe ${addr}` : `port ${addr.port}` + log.info(`Listening on ${ip} ${bind}`) + resolve(null) + void registerLocalSyncEvent(wss!) + }) + + host = `http://${ip}:${port}` + httpServer.listen(port, ip) +}) + +const handleStopServer = async() => new Promise((resolve, reject) => { + if (!wss) return + for (const client of wss.clients) client.close(SYNC_CLOSE_CODE.normal) + unregisterLocalSyncEvent() + wss.close() + wss = null + httpServer.close((err) => { + if (err) { + reject(err) + return + } + resolve() + }) + for (const socket of sockets) socket.destroy() + sockets.clear() +}) + +export const stopServer = async() => { + codeTools.stop() + if (!status.status) { + status.status = false + status.message = '' + status.address = [] + status.code = '' + sendServerStatus(status) + return + } + console.log('stoping sync server...') + status.message = 'stoping...' + sendServerStatus(status) + stopingServer = true + await handleStopServer().then(() => { + console.log('sync server stoped') + status.status = false + status.message = '' + status.address = [] + status.code = '' + }).catch(err => { + console.log(err) + status.message = err.message + }).finally(() => { + sendServerStatus(status) + stopingServer = false + }) +} + +export const startServer = async(port: number) => { + // if (status.status) await handleStopServer() + console.log('status.status', status.status, stopingServer) + if (stopingServer) return + if (status.status) await handleStopServer() + + await migrateData(global.lxDataPath) + await initServerInfo() + + log.info('starting sync server') + await handleStartServer(port).then(() => { + console.log('sync server started') + status.status = true + status.message = '' + status.address = getAddress() + + void generateCode() + codeTools.start() + }).catch(err => { + console.log(err) + status.status = false + status.message = err.message + status.address = [] + status.code = '' + }).finally(() => { + sendServerStatus(status) + }) +} + +export const getStatus = (): LX.Sync.ServerStatus => status + +export const generateCode = async() => { + status.code = handleGenerateCode() + sendServerStatus(status) + return status.code +} + +export const getDevices = async() => { + const userSpace = getUserSpace() + return userSpace.getDecices() +} + +export const removeDevice = async(clientId: string) => { + if (wss) { + for (const client of wss.clients) { + if (client.keyInfo.clientId == clientId) client.close(SYNC_CLOSE_CODE.normal) + } + } + const userSpace = getUserSpace() + await userSpace.removeDevice(clientId) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/event.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/event.ts new file mode 100644 index 000000000..b8d4cc6c0 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/event.ts @@ -0,0 +1,14 @@ +import { modules } from '../../modules' + +export const registerLocalSyncEvent = async(wss: LX.Sync.Server.SocketServer) => { + unregisterLocalSyncEvent() + for (const module of Object.values(modules)) { + module.registerEvent(wss) + } +} + +export const unregisterLocalSyncEvent = () => { + for (const module of Object.values(modules)) { + module.unregisterEvent() + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/handler.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/handler.ts new file mode 100644 index 000000000..d420f8a90 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/handler.ts @@ -0,0 +1,21 @@ +// 这个文件导出的方法将暴露给客户端调用,第一个参数固定为当前 socket 对象 +// import { getUserSpace } from '@/user' +import { FeaturesList } from '../../../../../../common/constants_sync' +import { modules } from '../../modules' + +const handler: LX.Sync.ServerSyncHandlerActions = { + async onFeatureChanged(socket, feature) { + // const userSpace = getUserSpace(socket.userInfo.name) + const beforeFeature = socket.feature + + for (const name of FeaturesList) { + const newStatus = feature[name] + if (newStatus == null) continue + beforeFeature[name] = feature[name] + socket.moduleReadys[name] = false + if (feature[name]) await modules[name].sync(socket).catch(_ => _) + } + }, +} + +export default handler diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/index.ts new file mode 100644 index 000000000..1471f76a7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/index.ts @@ -0,0 +1,10 @@ +import handler from './handler' +import { callObj as _callObj } from '../../modules' +export { sync } from './sync' +export { modules } from '../../modules' +export * from './event' + +export const callObj = { + ...handler, + ..._callObj, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/sync.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/sync.ts new file mode 100644 index 000000000..629e7135f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/server/sync/sync.ts @@ -0,0 +1,21 @@ +import { FeaturesList } from '../../../../../../common/constants_sync' +import { featureVersion, modules } from '../../modules' + + +export const sync = async(socket: LX.Sync.Server.Socket) => { + let disconnected = false + socket.onClose(() => { + disconnected = true + }) + const enabledFeatures = await socket.remote.getEnabledFeatures('desktop-app', featureVersion) + + if (disconnected) throw new Error('disconnected') + for (const moduleName of FeaturesList) { + if (enabledFeatures[moduleName]) { + socket.feature[moduleName] = enabledFeatures[moduleName] + await modules[moduleName].sync(socket).catch(_ => _) + } + if (disconnected) throw new Error('disconnected') + } + await socket.remote.finished() +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/user/data.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/user/data.ts new file mode 100644 index 000000000..0938ff48c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/user/data.ts @@ -0,0 +1,154 @@ +import fs from 'node:fs' +import path from 'node:path' +import { randomBytes } from 'node:crypto' +import { throttle } from '@common/utils/common' +import { filterFileName, toMD5 } from '../utils' +import { File } from '@common/constants_sync' +import { exists } from '../../utils' + + +interface ServerInfo { + serverId: string + version: number +} +interface DevicesInfo { + userName: string + clients: Record +} +const saveServerInfoThrottle = throttle(() => { + fs.writeFile(path.join(global.lxDataPath, File.serverDataPath, File.serverInfoJSON), JSON.stringify(serverInfo), (err) => { + if (err) console.error(err) + }) +}) +let serverInfo: ServerInfo +export const initServerInfo = async() => { + if (serverInfo != null) return + const serverInfoFilePath = path.join(global.lxDataPath, File.serverDataPath, File.serverInfoJSON) + if (await exists(serverInfoFilePath)) { + // eslint-disable-next-line require-atomic-updates + serverInfo = JSON.parse((await fs.promises.readFile(serverInfoFilePath)).toString()) + } else { + // eslint-disable-next-line require-atomic-updates + serverInfo = { + serverId: randomBytes(4 * 4).toString('base64'), + version: 2, + } + const syncDataPath = path.join(global.lxDataPath, File.serverDataPath) + if (!await exists(syncDataPath)) { + await fs.promises.mkdir(syncDataPath, { recursive: true }) + } + saveServerInfoThrottle() + } +} +export const getServerId = () => { + return serverInfo.serverId +} +export const getVersion = async() => { + await initServerInfo() + return serverInfo.version ?? 1 +} +export const setVersion = async(version: number) => { + await initServerInfo() + serverInfo.version = version + saveServerInfoThrottle() +} + +export const getUserDirname = (userName: string) => `${filterFileName(userName)}_${toMD5(userName).substring(0, 6)}` + +export const getUserConfig = (userName: string) => { + return { + maxSnapshotNum: global.lx.appSetting['sync.server.maxSsnapshotNum'], + 'list.addMusicLocationType': global.lx.appSetting['list.addMusicLocationType'], + } +} + + +// 读取所有用户目录下的devicesInfo信息,建立clientId与用户的对应关系,用于非首次连接 +// let deviceUserMap: Map = new Map() +// const init +// for (const deviceInfo of fs.readdirSync(syncDataPath).map(dirname => { +// const devicesFilePath = path.join(syncDataPath, dirname, File.userDevicesJSON) +// if (fs.existsSync(devicesFilePath)) { +// const devicesInfo = JSON.parse(fs.readFileSync(devicesFilePath).toString()) as DevicesInfo +// if (getUserDirname(devicesInfo.userName) == dirname) return { userName: devicesInfo.userName, devices: devicesInfo.clients } +// } +// return { userName: '', devices: {} } +// })) { +// for (const device of Object.values(deviceInfo.devices)) { +// if (deviceInfo.userName) deviceUserMap.set(device.clientId, deviceInfo.userName) +// } +// } +// export const getUserName = (clientId: string): string | null => { +// if (!clientId) return null +// return deviceUserMap.get(clientId) ?? null +// } +// export const setUserName = (clientId: string, dir: string) => { +// deviceUserMap.set(clientId, dir) +// } +// export const deleteUserName = (clientId: string) => { +// deviceUserMap.delete(clientId) +// } + +export const createClientKeyInfo = (deviceName: string, isMobile: boolean): LX.Sync.ServerKeyInfo => { + const keyInfo: LX.Sync.ServerKeyInfo = { + clientId: randomBytes(4 * 4).toString('base64'), + key: randomBytes(16).toString('base64'), + deviceName, + isMobile, + lastConnectDate: 0, + } + return keyInfo +} + +export class UserDataManage { + userName: string + userDir: string + devicesFilePath: string + devicesInfo: DevicesInfo + private readonly saveDevicesInfoThrottle: () => void + + getAllClientKeyInfo = () => { + return Object.values(this.devicesInfo.clients).sort((a, b) => (b.lastConnectDate ?? 0) - (a.lastConnectDate ?? 0)) + } + + saveClientKeyInfo = (keyInfo: LX.Sync.ServerKeyInfo) => { + if (this.devicesInfo.clients[keyInfo.clientId] == null && Object.keys(this.devicesInfo.clients).length > 101) throw new Error('max keys') + this.devicesInfo.clients[keyInfo.clientId] = keyInfo + this.saveDevicesInfoThrottle() + } + + getClientKeyInfo = (clientId?: string | null): LX.Sync.ServerKeyInfo | null => { + if (!clientId) return null + return this.devicesInfo.clients[clientId] ?? null + } + + removeClientKeyInfo = async(clientId: string) => { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.devicesInfo.clients[clientId] + this.saveDevicesInfoThrottle() + } + + isIncluedsClient = (clientId: string) => { + return Object.values(this.devicesInfo.clients).some(client => client.clientId == clientId) + } + + constructor(userName: string) { + this.userName = userName + const syncDataPath = path.join(global.lxDataPath, File.serverDataPath) + this.userDir = syncDataPath + this.devicesFilePath = path.join(this.userDir, File.userDevicesJSON) + this.devicesInfo = fs.existsSync(this.devicesFilePath) ? JSON.parse(fs.readFileSync(this.devicesFilePath).toString()) : { userName, clients: {} } + + this.saveDevicesInfoThrottle = throttle(() => { + fs.writeFile(this.devicesFilePath, JSON.stringify(this.devicesInfo), 'utf8', (err) => { + if (err) console.error(err) + }) + }) + } +} +// type UserDataManages = Map + +// export const createUserDataManage = (user: LX.UserConfig) => { +// const manage = Object.create(userDataManage) as typeof userDataManage +// manage.userDir = user.dataPath +// } diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/user/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/user/index.ts new file mode 100644 index 000000000..f98c8ea46 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/user/index.ts @@ -0,0 +1,64 @@ +import { UserDataManage } from './data' +import { + ListManage, + DislikeManage, +} from '../modules' + +export interface UserSpace { + dataManage: UserDataManage + listManage: ListManage + dislikeManage: DislikeManage + getDecices: () => Promise + removeDevice: (clientId: string) => Promise +} +const users = new Map() + +const delayTime = 10 * 1000 +const delayReleaseTimeouts = new Map() +const clearDelayReleaseTimeout = (userName: string) => { + if (!delayReleaseTimeouts.has(userName)) return + + clearTimeout(delayReleaseTimeouts.get(userName)) + delayReleaseTimeouts.delete(userName) +} +const seartDelayReleaseTimeout = (userName: string) => { + clearDelayReleaseTimeout(userName) + delayReleaseTimeouts.set(userName, setTimeout(() => { + users.delete(userName) + }, delayTime)) +} + +export const getUserSpace = (userName = 'default') => { + clearDelayReleaseTimeout(userName) + + let user = users.get(userName) + if (!user) { + console.log('new user data manage:', userName) + const dataManage = new UserDataManage(userName) + const listManage = new ListManage(dataManage) + const dislikeManage = new DislikeManage(dataManage) + users.set(userName, user = { + dataManage, + listManage, + dislikeManage, + async getDecices() { + return this.dataManage.getAllClientKeyInfo() + }, + async removeDevice(clientId) { + await listManage.removeDevice(clientId) + await dataManage.removeClientKeyInfo(clientId) + }, + }) + } + return user +} + +export const releaseUserSpace = (userName = 'default', force = false) => { + if (force) { + clearDelayReleaseTimeout(userName) + users.delete(userName) + } else seartDelayReleaseTimeout(userName) +} + + +export * from './data' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/utils/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/utils/index.ts new file mode 100644 index 000000000..341f1d62b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/utils/index.ts @@ -0,0 +1,31 @@ +import fs from 'node:fs' +import crypto from 'node:crypto' + + +export const createDirSync = (path: string) => { + if (!fs.existsSync(path)) { + try { + fs.mkdirSync(path, { recursive: true }) + } catch (e: any) { + if (e.code !== 'EEXIST') { + console.error('Could not set up log directory, error was: ', e) + process.exit(1) + } + } + } +} + +const fileNameRxp = /[\\/:*?#"<>|]/g +export const filterFileName = (name: string): string => name.replace(fileNameRxp, '') + +/** + * 创建 MD5 hash + * @param {*} str + */ +export const toMD5 = (str: string) => crypto.createHash('md5').update(str).digest('hex') + +export const checkAndCreateDirSync = (path: string) => { + if (!fs.existsSync(path)) { + fs.mkdirSync(path, { recursive: true }) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/utils/tools.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/utils/tools.ts new file mode 100644 index 000000000..eb1a9694c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/server/utils/tools.ts @@ -0,0 +1,87 @@ +import { createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'node:crypto' +// import { join } from 'node:path' +import zlib from 'node:zlib' +import type http from 'node:http' +// import getStore from '@/utils/store' +// import syncLog from '../../log' +// import { getUserName } from '../user/data' +// import { saveClientKeyInfo } from './data' + +export const generateCode = (): string => { + return Math.random().toString().substring(2, 8) +} + +export const getIP = (request: http.IncomingMessage) => { + return request.socket.remoteAddress +} + + +export const aesEncrypt = (buffer: string | Buffer, key: string): string => { + const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '') + return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64') +} + +export const aesDecrypt = (text: string, key: string): string => { + const decipher = createDecipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '') + return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString() +} + +export const rsaEncrypt = (buffer: Buffer, key: string): string => { + return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64') +} +export const rsaDecrypt = (buffer: Buffer, key: string): Buffer => { + return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer) +} + + +const gzip = async(data: string) => new Promise((resolve, reject) => { + zlib.gzip(data, (err, buf) => { + if (err) { + reject(err) + return + } + resolve(buf.toString('base64')) + }) +}) +const unGzip = async(data: string) => new Promise((resolve, reject) => { + zlib.gunzip(Buffer.from(data, 'base64'), (err, buf) => { + if (err) { + reject(err) + return + } + resolve(buf.toString()) + }) +}) + +export const encryptMsg = async(keyInfo: LX.Sync.ServerKeyInfo | null, msg: string): Promise => { + return msg.length > 1024 + ? 'cg_' + await gzip(msg) + : msg + // if (!keyInfo) return '' + // return aesEncrypt(msg, keyInfo.key, keyInfo.iv) +} + +export const decryptMsg = async(keyInfo: LX.Sync.ServerKeyInfo | null, enMsg: string): Promise => { + return enMsg.substring(0, 3) == 'cg_' + ? await unGzip(enMsg.replace('cg_', '')) + : enMsg + // console.log('decmsg raw: ', len.length, 'en: ', enMsg.length) + + // if (!keyInfo) return '' + // let msg = '' + // try { + // msg = aesDecrypt(enMsg, keyInfo.key, keyInfo.iv) + // } catch (err) { + // console.log(err) + // } + // return msg +} + +// export const getSnapshotFilePath = (keyInfo: LX.Sync.KeyInfo): string => { +// return join(global.lx.snapshotPath, `snapshot_${keyInfo.snapshotKey}.json`) +// } + +// export const sendStatus = (status: LX.Sync.ServerStatus) => { +// syncLog.info('status', status.devices.map(d => `${getUserName(d.clientId) ?? ''} ${d.deviceName}`)) +// } + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/utils.ts new file mode 100644 index 000000000..4ac392d25 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/sync/utils.ts @@ -0,0 +1,81 @@ +import { createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'node:crypto' +import os from 'node:os' +import fs from 'node:fs' +import zlib from 'node:zlib' +import cp from 'node:child_process' + + +// https://stackoverflow.com/a/75309339 +export const getComputerName = () => { + let name: string | undefined + switch (process.platform) { + case 'win32': + name = process.env.COMPUTERNAME + break + case 'darwin': + try { + name = cp.execSync('scutil --get ComputerName').toString().trim() + } catch {} + break + case 'linux': + // Don't fail even if hostnamectl is unavailable + try { + name = cp.execSync('hostnamectl --pretty').toString().trim() + } catch {} + break + } + if (!name) name = os.hostname() + return name +} + +const gzip = async(data: string) => new Promise((resolve, reject) => { + zlib.gzip(data, (err, buf) => { + if (err) { + reject(err) + return + } + resolve(buf.toString('base64')) + }) +}) +const unGzip = async(data: string) => new Promise((resolve, reject) => { + zlib.gunzip(Buffer.from(data, 'base64'), (err, buf) => { + if (err) { + reject(err) + return + } + resolve(buf.toString()) + }) +}) + +export const encodeData = async(data: string) => { + return data.length > 1024 + ? 'cg_' + await gzip(data) + : data +} + +export const decodeData = async(enData: string) => { + return enData.substring(0, 3) == 'cg_' + ? await unGzip(enData.replace('cg_', '')) + : enData +} + + +export const aesEncrypt = (text: string, key: string) => { + const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '') + return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64') +} + +export const aesDecrypt = (text: string, key: string) => { + const decipher = createDecipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '') + return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString() +} + +export const rsaEncrypt = (buffer: Buffer, key: string): string => { + return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64') +} +export const rsaDecrypt = (buffer: Buffer, key: string): Buffer => { + return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer) +} + + +export const exists = async(path: string) => fs.promises.stat(path).then(() => true).catch(() => false) diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/tray.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/tray.ts new file mode 100644 index 000000000..c80b3b377 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/tray.ts @@ -0,0 +1,444 @@ +import { Tray, Menu, nativeImage } from 'electron' +import { isMac, isWin } from '@common/utils' +import path from 'node:path' +import { + hideWindow as hideMainWindow, + isExistWindow as isExistMainWindow, + isShowWindow as isShowMainWindow, + sendTaskbarButtonClick, + showWindow as showMainWindow, +} from './winMain' +import { quitApp } from '@main/app' +import { TRAY_AUTO_ID } from '@common/constants' + +let tray: Electron.Tray | null +let isEnableTray: boolean = false +let themeId: number +let isShowStatusBarLyric: boolean = false + +const playerState = { + empty: false, + collect: false, + play: false, + next: true, + prev: true, +} + +const watchConfigKeys = [ + 'desktopLyric.enable', + 'desktopLyric.isLock', + 'desktopLyric.isAlwaysOnTop', + 'tray.themeId', + 'tray.enable', + 'player.isShowStatusBarLyric', + 'common.langId', +] satisfies Array + +const themeList = [ + { + id: 0, + fileName: 'trayTemplate', + isNative: true, + }, + { + id: 1, + fileName: 'tray_origin', + isNative: false, + }, + { + id: 2, + fileName: 'tray_black', + isNative: false, + }, +] + +const messages = { + 'en-us': { + collect: 'Love', + uncollect: 'Unlove', + play: 'Play', + pause: 'Pause', + next: 'Next Song', + prev: 'Prev Song', + hide_win_main: 'Hide Main Window', + show_win_main: 'Show Main Window', + hide_win_lyric: 'Hide Lyric Window', + show_win_lyric: 'Show Lyric Window', + lock_win_lyric: 'Lock Lyric Window', + unlock_win_lyric: 'Unlock Lyric Window', + top_win_lyric: 'On-top Lyric Window', + untop_win_lyric: 'Un-top Lyric Window', + show_statusbar_lyric: 'Show Lyrics on Statusbar', + hide_statusbar_lyric: 'Hide Lyrics on Statusbar', + exit: 'Exit', + music_name: 'Title: ', + music_singer: 'Artist: ', + }, + 'zh-cn': { + collect: '收藏', + uncollect: '取消收藏', + play: '播放', + pause: '暂停', + next: '下一曲', + prev: '上一曲', + hide_win_main: '隐藏主界面', + show_win_main: '显示主界面', + hide_win_lyric: '关闭桌面歌词', + show_win_lyric: '开启桌面歌词', + lock_win_lyric: '锁定桌面歌词', + unlock_win_lyric: '解锁桌面歌词', + top_win_lyric: '置顶歌词', + untop_win_lyric: '取消置顶', + show_statusbar_lyric: '显示状态栏歌词', + hide_statusbar_lyric: '隐藏状态栏歌词', + exit: '退出', + music_name: '歌曲名: ', + music_singer: '艺术家: ', + }, + 'zh-tw': { + collect: '收藏', + uncollect: '取消收藏', + play: '播放', + pause: '暫停', + next: '下一曲', + prev: '上一曲', + hide_win_main: '隱藏軟體視窗', + show_win_main: '顯示軟體視窗', + hide_win_lyric: '關閉歌詞視窗', + show_win_lyric: '開啟歌詞視窗', + lock_win_lyric: '鎖定歌詞視窗', + unlock_win_lyric: '解鎖歌詞視窗', + top_win_lyric: '置頂歌詞視窗', + untop_win_lyric: '取消置頂歌詞視窗', + show_statusbar_lyric: '顯示狀態列歌詞', + hide_statusbar_lyric: '隱藏狀態列歌詞', + exit: '退出', + music_name: '標題: ', + music_singer: '演出者: ', + }, + 'ko-kr': { + collect: '즐겨찾기 추가', + uncollect: '즐겨찾기 해제', + play: '재생', + pause: '일시정지', + next: '다음 곡', + prev: '이전 곡', + hide_win_main: '메인 창 숨기기', + show_win_main: '메인 창 표시', + hide_win_lyric: '가사 창 닫기', + show_win_lyric: '가사 창 열기', + lock_win_lyric: '가사 창 잠금', + unlock_win_lyric: '가사 창 잠금 해제', + top_win_lyric: '가사 항상 위에 표시', + untop_win_lyric: '항상 위에 표시 해제', + show_statusbar_lyric: '상태 표시줄에 가사 표시', + hide_statusbar_lyric: '상태 표시줄 가사 숨기기', + exit: '종료', + music_name: '제목: ', + music_singer: '아티스트: ', + }, +} as const +type Messages = typeof messages +type Langs = keyof Messages +const i18n = { + message: messages['zh-cn'] as Messages[Langs], + fallbackLocale: 'en-us' as 'en-us', + getMessage(key: keyof Messages[Langs]) { + return this.message[key] + }, + setLang(lang?: Langs | null) { + this.message = lang + ? messages[lang] ?? messages[this.fallbackLocale] + : messages[this.fallbackLocale] + }, +} + +const getIconPath = (id: number) => { + let theme = id == TRAY_AUTO_ID + ? global.lx.theme.shouldUseDarkColors + ? themeList[0] : themeList[2] + : themeList.find(item => item.id === id) ?? themeList[0] + return path.join(global.staticPath, 'images/tray', theme.fileName + (isWin ? '.ico' : '.png')) +} + +export const createTray = () => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if ((tray && !tray.isDestroyed()) || !global.lx.appSetting['tray.enable']) return + + // 托盘 + tray = new Tray(nativeImage.createFromPath(getIconPath(global.lx.appSetting['tray.themeId']))) + + // tray.setToolTip('LX Music') + // createMenu() + tray.setIgnoreDoubleClickEvents(true) + if (!isMac) { + tray.on('click', () => { + showMainWindow() + }) + } +} + +export const destroyTray = () => { + if (!tray) return + tray.destroy() + isEnableTray = false + isShowStatusBarLyric = false + tray = null +} + +const handleUpdateConfig = (setting: Partial) => { + global.lx.event_app.update_config(setting) +} + +const createPlayerMenu = () => { + let menu: Electron.MenuItemConstructorOptions[] = [] + menu.push(playerState.play ? { + label: i18n.getMessage('pause'), + click() { + sendTaskbarButtonClick('pause') + }, + } : { + label: i18n.getMessage('play'), + click() { + sendTaskbarButtonClick('play') + }, + }) + menu.push({ + label: i18n.getMessage('prev'), + click() { + sendTaskbarButtonClick('prev') + }, + }) + menu.push({ + label: i18n.getMessage('next'), + click() { + sendTaskbarButtonClick('next') + }, + }) + menu.push(playerState.collect ? { + label: i18n.getMessage('uncollect'), + click() { + sendTaskbarButtonClick('unCollect') + }, + } : { + label: i18n.getMessage('collect'), + click() { + sendTaskbarButtonClick('collect') + }, + }) + return menu +} + +export const createMenu = () => { + if (!tray) return + let menu: Electron.MenuItemConstructorOptions[] = createPlayerMenu() + if (playerState.empty) for (const m of menu) m.enabled = false + menu.push({ type: 'separator' }) + menu.push(global.lx.appSetting['desktopLyric.enable'] + ? { + label: i18n.getMessage('hide_win_lyric'), + click() { + handleUpdateConfig({ 'desktopLyric.enable': false }) + }, + } + : { + label: i18n.getMessage('show_win_lyric'), + click() { + handleUpdateConfig({ 'desktopLyric.enable': true }) + }, + }) + menu.push(global.lx.appSetting['desktopLyric.isLock'] + ? { + label: i18n.getMessage('unlock_win_lyric'), + click() { + handleUpdateConfig({ 'desktopLyric.isLock': false }) + }, + } + : { + label: i18n.getMessage('lock_win_lyric'), + click() { + handleUpdateConfig({ 'desktopLyric.isLock': true }) + }, + }) + menu.push(global.lx.appSetting['desktopLyric.isAlwaysOnTop'] + ? { + label: i18n.getMessage('untop_win_lyric'), + click() { + handleUpdateConfig({ 'desktopLyric.isAlwaysOnTop': false }) + }, + } + : { + label: i18n.getMessage('top_win_lyric'), + click() { + handleUpdateConfig({ 'desktopLyric.isAlwaysOnTop': true }) + }, + }) + if (isMac) { + menu.push({ type: 'separator' }) + menu.push(isShowStatusBarLyric + ? { + label: i18n.getMessage('hide_statusbar_lyric'), + click() { + handleUpdateConfig({ 'player.isShowStatusBarLyric': false }) + }, + } + : { + label: i18n.getMessage('show_statusbar_lyric'), + click() { + handleUpdateConfig({ 'player.isShowStatusBarLyric': true }) + }, + }) + } + menu.push({ type: 'separator' }) + if (isExistMainWindow()) { + const isShow = isShowMainWindow() + menu.push(isShow + ? { + label: i18n.getMessage('hide_win_main'), + click() { + hideMainWindow() + }, + } + : { + label: i18n.getMessage('show_win_main'), + click() { + showMainWindow() + }, + }) + } + menu.push({ + label: i18n.getMessage('exit'), + click() { + quitApp() + }, + }) + const contextMenu = Menu.buildFromTemplate(menu) + tray.setContextMenu(contextMenu) +} + +export const setTrayImage = (themeId: number) => { + if (!tray) return + tray.setImage(nativeImage.createFromPath(getIconPath(themeId))) +} + +const setLyric = (lyricLineText?: string) => { + if (isShowStatusBarLyric && tray && lyricLineText != null) { + tray.setTitle(lyricLineText) + } +} + +const defaultTip = 'LX Music' +const setTip = () => { + if (!tray) return + + let name = global.lx.player_status.name + let tip: string + if (name) { + if (name.length > 20) name = name.substring(0, 20) + '...' + let singer = global.lx.player_status.singer + if (singer?.length > 20) singer = singer.substring(0, 20) + '...' + + tip = `${defaultTip}\n${i18n.getMessage('music_name')}${name}${singer ? `\n${i18n.getMessage('music_singer')}${singer}` : ''}` + } else tip = defaultTip + tray.setToolTip(tip) +} + +const init = () => { + if (themeId != global.lx.appSetting['tray.themeId']) { + themeId = global.lx.appSetting['tray.themeId'] + setTrayImage(themeId) + } + if (isEnableTray !== global.lx.appSetting['tray.enable']) { + isEnableTray = global.lx.appSetting['tray.enable'] + global.lx.appSetting['tray.enable'] ? createTray() : destroyTray() + } + if (isShowStatusBarLyric !== global.lx.appSetting['player.isShowStatusBarLyric']) { + isShowStatusBarLyric = global.lx.appSetting['player.isShowStatusBarLyric'] + if (isShowStatusBarLyric) { + setLyric(global.lx.player_status.lyricLineText) + } else { + tray?.setTitle('') + } + } + setTip() + createMenu() +} + +export default () => { + global.lx.event_app.on('updated_config', (keys, setting) => { + if (!watchConfigKeys.some(key => keys.includes(key))) return + + if (keys.includes('common.langId')) i18n.setLang(setting['common.langId']) + + init() + }) + + global.lx.event_app.on('main_window_ready_to_show', () => { + createMenu() + }) + global.lx.event_app.on('main_window_show', () => { + createMenu() + }) + if (!isWin) { + global.lx.event_app.on('main_window_focus', () => { + createMenu() + }) + global.lx.event_app.on('main_window_blur', () => { + createMenu() + }) + } + global.lx.event_app.on('main_window_hide', () => { + createMenu() + }) + global.lx.event_app.on('main_window_close', () => { + destroyTray() + }) + + global.lx.event_app.on('app_inited', () => { + i18n.setLang(global.lx.appSetting['common.langId']) + init() + }) + + global.lx.event_app.on('system_theme_change', () => { + if (global.lx.appSetting['tray.themeId'] != TRAY_AUTO_ID) return + setTrayImage(global.lx.appSetting['tray.themeId']) + }) + + global.lx.event_app.on('player_status', (status) => { + let updated = false + if (status.status) { + switch (status.status) { + case 'paused': + playerState.play = false + playerState.empty &&= false + setLyric('') + break + case 'error': + playerState.play = false + playerState.empty &&= false + setLyric('') + break + case 'playing': + playerState.play = true + playerState.empty &&= false + setLyric(global.lx.player_status.lyricLineText) + break + case 'stoped': + playerState.play &&= false + playerState.empty = true + setLyric('') + break + } + updated = true + } else { + setLyric(status.lyricLineText) + } + if (status.name != null) setTip() + if (status.singer != null) setTip() + if (status.collect != null) { + playerState.collect = status.collect + updated = true + } + if (updated) init() + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/config/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/config/index.ts new file mode 100644 index 000000000..9433e68e8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/config/index.ts @@ -0,0 +1,2 @@ + +export const userApis: LX.UserApi.UserApiInfoFull[] = [] diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/index.ts new file mode 100644 index 000000000..fe26d3961 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/index.ts @@ -0,0 +1,49 @@ +import { closeWindow } from './main' +import { getUserApis, importApi as handleImportApi, removeApi as handleRemoveApi, setAllowShowUpdateAlert as saveAllowShowUpdateAlert } from './utils' +import { loadApi, setAllowShowUpdateAlert as setRendererEventAllowShowUpdateAlert, init } from './rendererEvent/rendererEvent' + +let userApiId: string | null + +export const getApiList = getUserApis + +export const importApi = async(script: string): Promise => { + return { + apiInfo: await handleImportApi(script), + apiList: getUserApis(), + } +} +export const removeApi = async(ids: string[]): Promise => { + if (userApiId && ids.includes(userApiId)) { + userApiId = null + await closeWindow() + } + handleRemoveApi(ids) + return getUserApis() +} + +export const setApi = async(id: string) => { + if (userApiId) { + userApiId = null + await closeWindow() + } + const apiList = getUserApis() + if (!apiList.some(a => a.id === id)) return + userApiId ||= id + await loadApi(id) +} + +export const setAllowShowUpdateAlert = (id: string, enable: boolean) => { + saveAllowShowUpdateAlert(id, enable) + setRendererEventAllowShowUpdateAlert(id, enable) +} + + +export * from './rendererEvent/rendererEvent' + +export default () => { + init() + + global.lx.event_app.on('main_window_close', () => { + void closeWindow() + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/main.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/main.ts new file mode 100644 index 000000000..96aad60bc --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/main.ts @@ -0,0 +1,154 @@ +import { mainSend } from '@common/mainIpc' +import { BrowserWindow } from 'electron' +import fs from 'fs' +import path from 'node:path' +import { openDevTools as handleOpenDevTools } from '@main/utils' +import USER_API_RENDERER_EVENT_NAME from './rendererEvent/name' +import { getScript } from './utils' + +let browserWindow: Electron.BrowserWindow | null = null + +let html: string | null = null +let dir: string | null = null + +const denyEvents = [ + 'will-navigate', + 'will-redirect', + 'will-attach-webview', + 'will-prevent-unload', + 'media-started-playing', +] as const + + +export const getProxy = () => { + if (global.lx.appSetting['network.proxy.enable'] && global.lx.appSetting['network.proxy.host']) { + return { + host: global.lx.appSetting['network.proxy.host'], + port: global.lx.appSetting['network.proxy.port'], + } + } + const envProxy = envParams.cmdParams['proxy-server'] + if (envProxy) { + if (envProxy && typeof envProxy == 'string') { + const [host, port = ''] = envProxy.split(':') + return { + host, + port, + } + } + } + return { + host: '', + port: '', + } +} +const handleUpdateProxy = (keys: Array) => { + if (keys.includes('network.proxy.enable') || (global.lx.appSetting['network.proxy.enable'] && keys.some(k => k.startsWith('network.proxy.')))) { + sendEvent(USER_API_RENDERER_EVENT_NAME.proxyUpdate, getProxy()) + } +} + +const winEvent = () => { + if (!browserWindow) return + browserWindow.on('closed', () => { + browserWindow = null + }) +} + +export const createWindow = async(userApi: LX.UserApi.UserApiInfo) => { + await closeWindow() + dir ??= process.env.NODE_ENV !== 'production' ? webpackUserApiPath : path.join(__dirname, 'userApi') + + if (!html) { + // eslint-disable-next-line require-atomic-updates + html = await fs.promises.readFile(path.join(dir, 'renderer/user-api.html'), 'utf8') + } + const preloadUrl = process.env.NODE_ENV !== 'production' + ? `${path.join(__dirname, '../dist/user-api-preload.js')}` + : `${path.join(__dirname, 'user-api-preload.js')}` + // console.log(preloadUrl) + + /** + * Initial window options + */ + browserWindow = new BrowserWindow({ + // enableRemoteModule: false, + resizable: false, + minimizable: false, + maximizable: false, + fullscreenable: false, + roundedCorners: false, + hasShadow: false, + show: false, + webPreferences: { + contextIsolation: true, + // worldSafeExecuteJavaScript: true, + nodeIntegration: false, + nodeIntegrationInWorker: false, + sandbox: false, + + spellcheck: false, + autoplayPolicy: 'document-user-activation-required', + enableWebSQL: false, + disableDialogs: true, + // nativeWindowOpen: false, + webgl: false, + images: false, + + preload: preloadUrl, + }, + }) + + for (const eventName of denyEvents) { + // @ts-expect-error + browserWindow.webContents.on(eventName, (event: Electron.Event) => { + event.preventDefault() + }) + } + browserWindow.webContents.session.setPermissionRequestHandler((webContents, permission, resolve) => { + if (webContents === browserWindow?.webContents) { + resolve(false) + return + } + resolve(true) + }) + browserWindow.webContents.setWindowOpenHandler(() => { + return { action: 'deny' } + }) + + winEvent() + + // console.log(html.replace('', ``)) + // const randomNum = Math.random().toString().substring(2, 10) + await browserWindow.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(html)) + + browserWindow.on('ready-to-show', async() => { + global.lx.event_app.on('updated_config', handleUpdateProxy) + sendEvent(USER_API_RENDERER_EVENT_NAME.initEnv, { ...userApi, script: await getScript(userApi.id), proxy: getProxy() }) + }) + + // global.modules.userApiWindow.loadFile(join(dir, 'renderer/user-api.html')) + // global.modules.userApiWindow.webContents.openDevTools() +} + +export const closeWindow = async() => { + global.lx.event_app.off('updated_config', handleUpdateProxy) + if (!browserWindow) return + await Promise.all([ + browserWindow.webContents.session.clearAuthCache(), + browserWindow.webContents.session.clearStorageData(), + browserWindow.webContents.session.clearCache(), + ]) + browserWindow?.destroy() + browserWindow = null +} + +export const sendEvent = (name: string, params?: T) => { + if (!browserWindow) return + mainSend(browserWindow, name, params) +} + +export const openDevTools = () => { + if (!browserWindow) return + handleOpenDevTools(browserWindow.webContents) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/renderer/preload.js b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/renderer/preload.js new file mode 100644 index 000000000..b53ae92ef --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/renderer/preload.js @@ -0,0 +1,377 @@ +import { contextBridge, ipcRenderer, webFrame } from 'electron' +import needle from 'needle' +import zlib from 'zlib' +import { createCipheriv, publicEncrypt, constants, randomBytes, createHash } from 'crypto' +import USER_API_RENDERER_EVENT_NAME from '../rendererEvent/name' +import { httpOverHttp, httpsOverHttp } from 'tunnel' + + +const sendMessage = (action, data, status, message) => { + ipcRenderer.send(action, { data, status, message }) +} + +let isInitedApi = false +const proxy = { + host: '', + port: '', +} +let isShowedUpdateAlert = false +const EVENT_NAMES = { + request: 'request', + inited: 'inited', + updateAlert: 'updateAlert', +} +const eventNames = Object.values(EVENT_NAMES) +const events = { + request: null, +} +const allSources = ['kw', 'kg', 'tx', 'wy', 'mg', 'local'] +const supportQualitys = { + kw: ['128k', '320k', 'flac', 'flac24bit'], + kg: ['128k', '320k', 'flac', 'flac24bit'], + tx: ['128k', '320k', 'flac', 'flac24bit'], + wy: ['128k', '320k', 'flac', 'flac24bit'], + mg: ['128k', '320k', 'flac', 'flac24bit'], + local: [], +} +const supportActions = { + kw: ['musicUrl'], + kg: ['musicUrl'], + tx: ['musicUrl'], + wy: ['musicUrl'], + mg: ['musicUrl'], + xm: ['musicUrl'], + local: ['musicUrl', 'lyric', 'pic'], +} + +const httpsRxp = /^https:/ +const getRequestAgent = url => { + return proxy.host ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)({ + proxy: { + host: proxy.host, + port: proxy.port, + }, + }) : undefined +} + +const verifyLyricInfo = (info) => { + if (typeof info != 'object' || typeof info.lyric != 'string') throw new Error('failed') + if (info.lyric.length > 51200) throw new Error('failed') + return { + lyric: info.lyric, + tlyric: (typeof info.tlyric == 'string' && info.tlyric.length < 5120) ? info.tlyric : null, + rlyric: (typeof info.rlyric == 'string' && info.rlyric.length < 5120) ? info.rlyric : null, + lxlyric: (typeof info.lxlyric == 'string' && info.lxlyric.length < 8192) ? info.lxlyric : null, + } +} + +const handleRequest = (context, { requestKey, data }) => { + // console.log(data) + if (!events.request) return sendMessage(USER_API_RENDERER_EVENT_NAME.response, { requestKey }, false, 'Request event is not defined') + try { + events.request.call(context, { source: data.source, action: data.action, info: data.info }).then(response => { + let sendData = { + requestKey, + } + switch (data.action) { + case 'musicUrl': + if (typeof response != 'string' || response.length > 2048 || !/^https?:/.test(response)) throw new Error('failed') + sendData.result = { + source: data.source, + action: data.action, + data: { + type: data.info.type, + url: response, + }, + } + break + case 'lyric': + sendData.result = { + source: data.source, + action: data.action, + data: verifyLyricInfo(response), + } + break + case 'pic': + if (typeof response != 'string' || response.length > 2048 || !/^https?:/.test(response)) throw new Error('failed') + sendData.result = { + source: data.source, + action: data.action, + data: response, + } + break + } + sendMessage(USER_API_RENDERER_EVENT_NAME.response, sendData, true) + }).catch(err => { + sendMessage(USER_API_RENDERER_EVENT_NAME.response, { requestKey }, false, err.message) + }) + } catch (err) { + sendMessage(USER_API_RENDERER_EVENT_NAME.response, { requestKey }, false, err.message) + } +} + +/** + * + * @param {*} context + * @param {*} info { + * openDevTools: false, + * message: 'xxx', + * sources: { + * kw: ['128k', '320k', 'flac', 'flac24bit'], + * kg: ['128k', '320k', 'flac', 'flac24bit'], + * tx: ['128k', '320k', 'flac', 'flac24bit'], + * wy: ['128k', '320k', 'flac', 'flac24bit'], + * mg: ['128k', '320k', 'flac', 'flac24bit'], + * } + * } + */ +const handleInit = (context, info) => { + if (!info) { + sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, 'Missing required parameter init info') + // sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '') + return + } + if (info.openDevTools === true) { + sendMessage(USER_API_RENDERER_EVENT_NAME.openDevTools) + } + // if (!info.status) { + // sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, 'Missing required parameter init info') + // // sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '') + // return + // } + const sourceInfo = { + sources: {}, + } + try { + for (const source of allSources) { + const userSource = info.sources[source] + if (!userSource || userSource.type !== 'music') continue + const qualitys = supportQualitys[source] + const actions = supportActions[source] + sourceInfo.sources[source] = { + type: 'music', + actions: actions.filter(a => userSource.actions.includes(a)), + qualitys: qualitys.filter(q => userSource.qualitys.includes(q)), + } + } + } catch (error) { + console.log(error) + sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, error.message) + return + } + sendMessage(USER_API_RENDERER_EVENT_NAME.init, sourceInfo, true) + + ipcRenderer.on(USER_API_RENDERER_EVENT_NAME.request, (event, data) => { + handleRequest(context, data) + }) +} + +const handleShowUpdateAlert = (data, resolve, reject) => { + if (!data || typeof data != 'object') return reject(new Error('parameter format error.')) + if (!data.log || typeof data.log != 'string') return reject(new Error('log is required.')) + if (data.updateUrl && !/^https?:\/\/[^\s$.?#].[^\s]*$/.test(data.updateUrl) && data.updateUrl.length > 1024) delete data.updateUrl + if (data.log.length > 1024) data.log = data.log.substring(0, 1024) + '...' + sendMessage(USER_API_RENDERER_EVENT_NAME.showUpdateAlert, { + log: data.log, + updateUrl: data.updateUrl, + }) + resolve() +} + +const onError = (errorMessage) => { + if (isInitedApi) return + isInitedApi = true + if (errorMessage.length > 1024) errorMessage = errorMessage.substring(0, 1024) + '...' + sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, errorMessage) +} + +const initEnv = (userApi) => { + proxy.host = userApi.proxy.host + proxy.port = userApi.proxy.port + + contextBridge.exposeInMainWorld('lx', { + EVENT_NAMES, + request(url, { method = 'get', timeout, headers, body, form, formData }, callback) { + let options = { + headers, + agent: getRequestAgent(url), + } + let data + if (body) { + data = body + } else if (form) { + data = form + // data.content_type = 'application/x-www-form-urlencoded' + options.json = false + } else if (formData) { + data = formData + // data.content_type = 'multipart/form-data' + options.json = false + } + options.response_timeout = typeof timeout == 'number' && timeout > 0 ? Math.min(timeout, 60_000) : 60_000 + + let request = needle.request(method, url, data, options, (err, resp, body) => { + // console.log(err, resp, body) + try { + if (err) { + callback.call(this, err, null, null) + } else { + body = resp.body = resp.raw.toString() + try { + resp.body = JSON.parse(resp.body) + } catch (_) {} + body = resp.body + callback.call(this, err, { + statusCode: resp.statusCode, + statusMessage: resp.statusMessage, + headers: resp.headers, + bytes: resp.bytes, + raw: resp.raw, + body, + }, body) + } + } catch (err) { + onError(err.message) + } + }).request + + return () => { + if (!request.aborted) request.abort() + request = null + } + }, + send(eventName, data) { + return new Promise((resolve, reject) => { + if (!eventNames.includes(eventName)) return reject(new Error('The event is not supported: ' + eventName)) + switch (eventName) { + case EVENT_NAMES.inited: + if (isInitedApi) return reject(new Error('Script is inited')) + isInitedApi = true + handleInit(this, data) + resolve() + break + case EVENT_NAMES.updateAlert: + if (isShowedUpdateAlert) return reject(new Error('The update alert can only be called once.')) + isShowedUpdateAlert = true + handleShowUpdateAlert(data, resolve, reject) + break + default: + reject(new Error('Unknown event name: ' + eventName)) + } + }) + }, + on(eventName, handler) { + if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName)) + switch (eventName) { + case EVENT_NAMES.request: + events.request = handler + break + default: return Promise.reject(new Error('The event is not supported: ' + eventName)) + } + return Promise.resolve() + }, + utils: { + crypto: { + aesEncrypt(buffer, mode, key, iv) { + const cipher = createCipheriv(mode, key, iv) + return Buffer.concat([cipher.update(buffer), cipher.final()]) + }, + rsaEncrypt(buffer, key) { + buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]) + return publicEncrypt({ key, padding: constants.RSA_NO_PADDING }, buffer) + }, + randomBytes(size) { + return randomBytes(size) + }, + md5(str) { + return createHash('md5').update(str).digest('hex') + }, + }, + buffer: { + from(...args) { + return Buffer.from(...args) + }, + bufToString(buf, format) { + return Buffer.from(buf, 'binary').toString(format) + }, + }, + zlib: { + inflate(buf) { + return new Promise((resolve, reject) => { + zlib.inflate(buf, (err, data) => { + if (err) reject(new Error(err.message)) + else resolve(data) + }) + }) + }, + deflate(data) { + return new Promise((resolve, reject) => { + zlib.deflate(data, (err, buf) => { + if (err) reject(new Error(err.message)) + else resolve(buf) + }) + }) + }, + }, + }, + currentScriptInfo: { + name: userApi.name, + description: userApi.description, + version: userApi.version, + author: userApi.author, + homepage: userApi.homepage, + rawScript: userApi.script, + }, + version: '2.0.0', + env: 'desktop', + // removeEvent(eventName, handler) { + // if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName)) + // let handlers + // switch (eventName) { + // case EVENT_NAMES.request: + // handlers = events.request + // break + // } + // for (let index = 0; index < handlers.length; index++) { + // if (handlers[index] === handler) { + // handlers.splice(index, 1) + // break + // } + // } + // }, + // removeAllEvents() { + // for (const handlers of Object.values(events)) { + // handlers.splice(0, handlers.length) + // } + // }, + }) + + contextBridge.exposeInMainWorld('__lx_init_error_handler__', { + sendError(errorMessage) { + onError(errorMessage) + }, + }) + + webFrame.executeJavaScript(`(() => { +window.addEventListener('error', (event) => { + if (event.isTrusted) globalThis.__lx_init_error_handler__.sendError(event.message.replace(/^Uncaught\\sError:\\s/, '')) +}) +window.addEventListener('unhandledrejection', (event) => { + if (!event.isTrusted) return + const message = typeof event.reason === 'string' ? event.reason : event.reason?.message ?? String(event.reason) + globalThis.__lx_init_error_handler__.sendError(message.replace(/^Error:\\s/, '')) +}) +})()`) + + webFrame.executeJavaScript(userApi.script).catch(_ => _) +} + + +ipcRenderer.on(USER_API_RENDERER_EVENT_NAME.initEnv, (event, data) => { + initEnv(data) +}) + +ipcRenderer.on(USER_API_RENDERER_EVENT_NAME.proxyUpdate, (event, data) => { + proxy.host = data.host + proxy.port = data.port +}) diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/renderer/user-api.html b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/renderer/user-api.html new file mode 100644 index 000000000..aada3349d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/renderer/user-api.html @@ -0,0 +1,12 @@ + + + + + + + User api + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/rendererEvent/name.js b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/rendererEvent/name.js new file mode 100644 index 000000000..3e5ca8b12 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/rendererEvent/name.js @@ -0,0 +1,17 @@ +const names = { + initEnv: '', + init: '', + request: '', + response: '', + openDevTools: '', + showUpdateAlert: '', + getProxy: '', + proxyUpdate: '', +} + + +for (const key of Object.keys(names)) { + names[key] = `userApi_${key}` +} + +export default names diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/rendererEvent/rendererEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/rendererEvent/rendererEvent.ts new file mode 100644 index 000000000..098cc77ae --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/rendererEvent/rendererEvent.ts @@ -0,0 +1,154 @@ +import { mainOn } from '@common/mainIpc' + +import USER_API_RENDERER_EVENT_NAME from './name' +import { createWindow, getProxy, openDevTools, sendEvent } from '../main' +import { getUserApis } from '../utils' +import { sendShowUpdateAlert, sendStatusChange } from '@main/modules/winMain' + +let userApi: LX.UserApi.UserApiInfo +let apiStatus: LX.UserApi.UserApiStatus = { status: true } +const requestQueue = new Map() +const timeouts = new Map() +interface InitParams { + params: { + status: boolean + message: string + data: LX.UserApi.UserApiInfo + } +} +interface ResponseParams { + params: { + status: boolean + message: string + data: { + requestKey: string + result: any + } + } +} +interface UpdateInfoParams { + params: { + data: { + log: string + updateUrl: string + } + } +} + +export const init = () => { + const handleInit = ({ params: { status, message, data: apiInfo } }: InitParams) => { + // console.log('inited') + // if (!status) { + // console.log('init failed:', message) + // global.lx_event.userApi.status(status = { status: true, apiInfo: { ...userApi, sources: apiInfo.sources } }) + // return + // } + apiStatus = status + ? { status: true, apiInfo: { ...userApi, sources: apiInfo.sources } } + : { status: false, apiInfo: userApi, message } + sendStatusChange(apiStatus) + } + const handleResponse = ({ params: { status, data: { requestKey, result }, message } }: ResponseParams) => { + const request = requestQueue.get(requestKey) + if (!request) return + requestQueue.delete(requestKey) + clearRequestTimeout(requestKey) + if (status) { + request[0](result) + } else { + request[1](new Error(message)) + } + } + const handleOpenDevTools = () => { + openDevTools() + } + const handleShowUpdateAlert = ({ params: { data } }: UpdateInfoParams) => { + if (!userApi.allowShowUpdateAlert) return + sendShowUpdateAlert({ + name: userApi.name, + description: userApi.description, + log: data.log, + updateUrl: data.updateUrl, + }) + } + const handleGetProxy = () => { + sendEvent(USER_API_RENDERER_EVENT_NAME.proxyUpdate, getProxy()) + } + mainOn(USER_API_RENDERER_EVENT_NAME.init, handleInit) + mainOn(USER_API_RENDERER_EVENT_NAME.response, handleResponse) + mainOn(USER_API_RENDERER_EVENT_NAME.openDevTools, handleOpenDevTools) + mainOn(USER_API_RENDERER_EVENT_NAME.showUpdateAlert, handleShowUpdateAlert) + mainOn(USER_API_RENDERER_EVENT_NAME.getProxy, handleGetProxy) +} + +export const clearRequestTimeout = (requestKey: string) => { + const timeout = timeouts.get(requestKey) + if (timeout) { + clearTimeout(timeout) + timeouts.delete(requestKey) + } +} + +export const loadApi = async(apiId: string) => { + if (!apiId) { + apiStatus = { status: false, message: 'api id is null' } + sendStatusChange(apiStatus) + return + } + const targetApi = getUserApis().find(api => api.id == apiId) + if (!targetApi) throw new Error('api not found') + userApi = targetApi + console.log('load api', userApi.name) + await createWindow(userApi) + // if (!userApi) return global.lx_event.userApi.status(status = { status: false, message: 'api script is not found' }) + // if (!global.modules.userApiWindow) { + // global.lx_event.userApi.status(status = { status: false, message: 'user api runtime is not defined' }) + // throw new Error('user api window is not defined') + // } + + // // const path = require('path') + // // // eslint-disable-next-line no-undef + // // userApi.script = require('fs').readFileSync(join(process.env.NODE_ENV !== 'production' ? __userApi : __dirname, 'renderer/test-api.js')).toString() + // console.log('load api', userApi.name) + // mainSend(global.modules.userApiWindow, USER_API_RENDERER_EVENT_NAME.init, { userApi }) +} + +export const cancelRequest = (requestKey: string) => { + if (!requestQueue.has(requestKey)) return + const request = requestQueue.get(requestKey) + request[1](new Error('Cancel request')) + requestQueue.delete(requestKey) + clearRequestTimeout(requestKey) +} + +export const request = async({ requestKey, data }: LX.UserApi.UserApiRequestParams): Promise => await new Promise((resolve, reject) => { + if (!userApi) { + reject(new Error('user api is not load')) + } + + // const requestKey = `request__${Math.random().toString().substring(2)}` + const timeout = timeouts.get(requestKey) + if (timeout) { + clearTimeout(timeout) + timeouts.delete(requestKey) + cancelRequest(requestKey) + } + + timeouts.set(requestKey, setTimeout(() => { + cancelRequest(requestKey) + }, 20000)) + + requestQueue.set(requestKey, [resolve, reject, data]) + sendRequest({ requestKey, data }) +}) + +export const getStatus = (): LX.UserApi.UserApiStatus => apiStatus + +export const setAllowShowUpdateAlert = (id: string, enable: boolean) => { + if (!userApi || userApi.id != id) return + userApi.allowShowUpdateAlert = enable +} + +export const sendRequest = (reqData: { requestKey: string, data: any }) => { + sendEvent(USER_API_RENDERER_EVENT_NAME.request, reqData) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/utils.ts new file mode 100644 index 000000000..6a56e122c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/userApi/utils.ts @@ -0,0 +1,152 @@ +import { userApis as defaultUserApis } from './config' +import { STORE_NAMES } from '@common/constants' +import getStore from '@main/utils/store' +import zlib from 'node:zlib' + +let userApis: LX.UserApi.UserApiInfo[] | null +let scripts = new Map() + +const saveData = () => { + getStore(STORE_NAMES.USER_API).set('userApis', userApis!.map(api => { + return { + ...api, + script: scripts.get(api.id), + } + })) +} + +export const getUserApis = (): LX.UserApi.UserApiInfo[] => { + if (userApis) return userApis + + const electronStore_userApi = getStore(STORE_NAMES.USER_API) + let infoFull = electronStore_userApi.get('userApis') as LX.UserApi.UserApiInfoFull[] + let requiredUpdate = false + if (infoFull) { + for (let i = 0; i < infoFull.length; i++) { + const api = infoFull[i] + if (api.version != null) continue + requiredUpdate ||= true + try { + infoFull.splice(i, 1, { + ...parseScriptInfo(api.script), + ...api, + }) + } catch (e) { + infoFull.splice(i, 1) + i-- + } + } + } else { + infoFull = defaultUserApis + electronStore_userApi.set('userApis', userApis) + } + userApis = infoFull.map(api => { + if (api.allowShowUpdateAlert == null) api.allowShowUpdateAlert = false + const { script, ...info } = api + scripts.set(api.id, script) + return info + }) + if (requiredUpdate) saveData() + return userApis +} + +const INFO_NAMES = { + name: 24, + description: 36, + author: 56, + homepage: 1024, + version: 36, +} as const +type INFO_NAMES_Type = typeof INFO_NAMES +const matchInfo = (scriptInfo: string) => { + const infoArr = scriptInfo.split(/\r?\n/) + const rxp = /^\s?\*\s?@(\w+)\s(.+)$/ + const infos: Partial> = {} + for (const info of infoArr) { + const result = rxp.exec(info) + if (!result) continue + const key = result[1] as keyof typeof INFO_NAMES + if (INFO_NAMES[key] == null) continue + infos[key] = result[2].trim() + } + + for (const [key, len] of Object.entries(INFO_NAMES) as Array<{ [K in keyof INFO_NAMES_Type]: [K, INFO_NAMES_Type[K]] }[keyof INFO_NAMES_Type]>) { + infos[key] ||= '' + if (infos[key] == null) infos[key] = '' + else if (infos[key].length > len) infos[key] = infos[key].substring(0, len) + '...' + } + + return infos as Record +} +const parseScriptInfo = (script: string) => { + const result = /^\/\*[\S|\s]+?\*\//.exec(script) + if (!result) throw new Error('无效的自定义源文件') + + let scriptInfo = matchInfo(result[0]) + + scriptInfo.name ||= `user_api_${new Date().toLocaleString()}` + return scriptInfo +} +const deflateScript = async(script: string) => new Promise((resolve, reject) => { + zlib.deflate(Buffer.from(script, 'utf8'), (err, buf) => { + if (err) { + reject(err) + return + } + resolve('gz_' + buf.toString('base64')) + }) +}) +const inflateScript = async(script: string) => new Promise((resolve, reject) => { + if (script.startsWith('gz_')) { + zlib.inflate(Buffer.from(script.substring(3), 'base64'), (err, buf) => { + if (err) { + reject(err) + return + } + resolve(buf.toString('utf8')) + }) + } else resolve(script) +}) +export const importApi = async(scriptRaw: string): Promise => { + let scriptInfo = parseScriptInfo(scriptRaw) + const script = await deflateScript(scriptRaw) + userApis ??= [] + for (const api of userApis) { + const existingScript = scripts.get(api.id) + if (existingScript === script) { + throw new Error(`导入失败,脚本内容与已有的源「${api.name}」相同`) + } + } + const apiInfo = { + id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`, + ...scriptInfo, + allowShowUpdateAlert: true, + } + userApis.push(apiInfo) + scripts.set(apiInfo.id, script) + saveData() + return apiInfo +} + +export const removeApi = (ids: string[]) => { + if (!userApis) return + for (let index = userApis.length - 1; index > -1; index--) { + if (ids.includes(userApis[index].id)) { + scripts.delete(userApis[index].id) + userApis.splice(index, 1) + ids.splice(index, 1) + } + } + saveData() +} + +export const setAllowShowUpdateAlert = (id: string, enable: boolean) => { + const targetApi = userApis?.find(api => api.id == id) + if (!targetApi) return + targetApi.allowShowUpdateAlert = enable + saveData() +} + +export const getScript = async(id: string) => { + return inflateScript(scripts.get(id) ?? '') +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/config.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/config.ts new file mode 100644 index 000000000..8a4c69916 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/config.ts @@ -0,0 +1,92 @@ +import { isLinux } from '@common/utils' +import { closeWindow, createWindow, getBounds, isExistWindow, alwaysOnTopTools, setBounds, setIgnoreMouseEvents, setSkipTaskbar } from './main' +import { sendConfigChange, sendMouseLeave } from './rendererEvent' +import { buildLyricConfig, getLyricWindowBounds, initWindowSize, watchConfigKeys } from './utils' +import { mouseCheckTools } from './mouseCheckTools' + +let isLock: boolean +let isEnable: boolean +let isAlwaysOnTop: boolean +let isAlwaysOnTopLoop: boolean +let isShowTaskbar: boolean +let isLockScreen: boolean +let isHoverHide: boolean + + +export const setLrcConfig = (keys: Array, setting: Partial) => { + if (!watchConfigKeys.some(key => keys.includes(key))) return + + if (isExistWindow()) { + sendConfigChange(buildLyricConfig(setting)) + if (keys.includes('desktopLyric.isLock') && isLock != global.lx.appSetting['desktopLyric.isLock']) { + isLock = global.lx.appSetting['desktopLyric.isLock'] + if (global.lx.appSetting['desktopLyric.isLock']) { + setIgnoreMouseEvents(true, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] }) + mouseCheckTools.runCheck(sendMouseLeave) + } else { + setIgnoreMouseEvents(false, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] }) + mouseCheckTools.cacnelCheck() + } + } + if (keys.includes('desktopLyric.isHoverHide') && isHoverHide != global.lx.appSetting['desktopLyric.isHoverHide']) { + isHoverHide = global.lx.appSetting['desktopLyric.isHoverHide'] + if (!isLinux) { + setIgnoreMouseEvents(global.lx.appSetting['desktopLyric.isLock'], { forward: isHoverHide }) + if (isHoverHide) { + mouseCheckTools.runCheck(sendMouseLeave) + } else { + mouseCheckTools.cacnelCheck() + } + } + } + if (keys.includes('desktopLyric.isAlwaysOnTop') && isAlwaysOnTop != global.lx.appSetting['desktopLyric.isAlwaysOnTop']) { + isAlwaysOnTop = global.lx.appSetting['desktopLyric.isAlwaysOnTop'] + alwaysOnTopTools.setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) + if (isAlwaysOnTop && global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) { + alwaysOnTopTools.startLoop() + } else alwaysOnTopTools.clearLoop() + } + if (keys.includes('desktopLyric.isShowTaskbar') && isShowTaskbar != global.lx.appSetting['desktopLyric.isShowTaskbar']) { + isShowTaskbar = global.lx.appSetting['desktopLyric.isShowTaskbar'] + setSkipTaskbar(!global.lx.appSetting['desktopLyric.isShowTaskbar']) + } + if (keys.includes('desktopLyric.isAlwaysOnTopLoop') && isAlwaysOnTopLoop != global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) { + isAlwaysOnTopLoop = global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop'] + if (!global.lx.appSetting['desktopLyric.isAlwaysOnTop']) return + if (isAlwaysOnTopLoop) { + alwaysOnTopTools.startLoop() + } else { + alwaysOnTopTools.clearLoop() + } + } + if (keys.includes('desktopLyric.isLockScreen') && isLockScreen != global.lx.appSetting['desktopLyric.isLockScreen']) { + isLockScreen = global.lx.appSetting['desktopLyric.isLockScreen'] + if (global.lx.appSetting['desktopLyric.isLockScreen']) { + setBounds(getLyricWindowBounds(getBounds()!, { + x: 0, + y: 0, + w: global.lx.appSetting['desktopLyric.width'], + h: global.lx.appSetting['desktopLyric.height'], + })) + } + } + if (keys.includes('desktopLyric.x') && setting['desktopLyric.x'] == null) { + setBounds(initWindowSize( + global.lx.appSetting['desktopLyric.x'], + global.lx.appSetting['desktopLyric.y'], + global.lx.appSetting['desktopLyric.width'], + global.lx.appSetting['desktopLyric.height'], + )) + } + } + if (keys.includes('desktopLyric.enable') && isEnable != global.lx.appSetting['desktopLyric.enable']) { + isEnable = global.lx.appSetting['desktopLyric.enable'] + if (global.lx.appSetting['desktopLyric.enable']) { + createWindow() + } else { + alwaysOnTopTools.clearLoop() + mouseCheckTools.cacnelCheck() + closeWindow() + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/index.ts new file mode 100644 index 000000000..f08aeba57 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/index.ts @@ -0,0 +1,79 @@ +import { APP_EVENT_NAMES } from '@common/constants' +import initRendererEvent, { sendMainWindowInitedEvent } from './rendererEvent' +import { setLrcConfig } from './config' +import { HOTKEY_DESKTOP_LYRIC } from '@common/hotKey' +import { closeWindow, createWindow, isExistWindow } from './main' +// import main from './main' +// import { Event, EVENT_NAMES } from './event' + +let isMainWidnowFullscreen = false + +export default () => { + initRendererEvent() + // global.lx.event_app.winLyric = new Event() + // global.app_event.winMain. + + global.lx.event_app.on('main_window_inited', () => { + isMainWidnowFullscreen = global.lx.appSetting['common.startInFullscreen'] + + if (global.lx.appSetting['desktopLyric.enable']) { + if (global.lx.appSetting['desktopLyric.fullscreenHide'] && isMainWidnowFullscreen) { + closeWindow() + } else { + if (isExistWindow()) sendMainWindowInitedEvent() + else createWindow() + } + } + }) + global.lx.event_app.on('updated_config', (keys, setting) => { + setLrcConfig(keys, setting) + if (keys.includes('desktopLyric.fullscreenHide') && global.lx.appSetting['desktopLyric.enable'] && isMainWidnowFullscreen) { + if (global.lx.appSetting['desktopLyric.fullscreenHide']) closeWindow() + else if (!isExistWindow()) createWindow() + } + }) + global.lx.event_app.on('main_window_close', () => { + closeWindow() + }) + global.lx.event_app.on('main_window_fullscreen', (isFullscreen) => { + isMainWidnowFullscreen = isFullscreen + if (global.lx.appSetting['desktopLyric.enable'] && global.lx.appSetting['desktopLyric.fullscreenHide']) { + if (isFullscreen) closeWindow() + else if (!isExistWindow()) createWindow() + } + }) + + + // global.lx_event.mainWindow.on(MAIN_WINDOW_EVENT_NAME.setLyricInfo, info => { + // if (!global.modules.lyricWindow) return + // mainSend(global.modules.lyricWindow, ipcWinLyricNames.set_lyric_info, info) + // }) + + global.lx.event_app.on('hot_key_down', ({ type, key }) => { + let info = global.lx.hotKey.config.global.keys[key] + if (!info || info.type != APP_EVENT_NAMES.winLyricName) return + let newSetting: Partial = {} + let settingKey: keyof LX.AppSetting + switch (info.action) { + case HOTKEY_DESKTOP_LYRIC.toggle_visible.action: + settingKey = 'desktopLyric.enable' + break + case HOTKEY_DESKTOP_LYRIC.toggle_lock.action: + settingKey = 'desktopLyric.isLock' + break + case HOTKEY_DESKTOP_LYRIC.toggle_always_top.action: + settingKey = 'desktopLyric.isAlwaysOnTop' + break + default: return + } + newSetting[settingKey] = !global.lx.appSetting[settingKey] + + global.lx.event_app.update_config(newSetting) + }) +} +export * from './main' +export * from './rendererEvent' + +// export { +// EVENT_NAMES, +// } diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/main.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/main.ts new file mode 100644 index 000000000..9a1b88dc7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/main.ts @@ -0,0 +1,236 @@ +import path from 'node:path' +import { BrowserWindow } from 'electron' +import { debounce, getPlatform, isLinux, isWin } from '@common/utils' +import { initWindowSize, minHeight, minWidth } from './utils' +import { mainSend } from '@common/mainIpc' +import { encodePath } from '@common/utils/electron' + +// require('./event') +// require('./rendererEvent') + +let browserWindow: Electron.BrowserWindow | null = null +let isWinBoundsUpdateing = false + +const saveBoundsConfig = debounce((config: Partial) => { + global.lx.event_app.update_config(config) + if (isWinBoundsUpdateing) isWinBoundsUpdateing = false +}, 500) + +const winEvent = () => { + if (!browserWindow) return + + // browserWindow.on('close', () => { + // if (global.lx.appSetting['desktopLyric.enable'] && !global.lx.mainWindowClosed) { + // browserWindow = null + // global.lx.event_app.update_config({ 'desktopLyric.enable': false }) + // } + // }) + + browserWindow.on('closed', () => { + browserWindow = null + }) + + browserWindow.on('move', () => { + // bounds = browserWindow.getBounds() + // console.log('move', isWinBoundsUpdateing) + if (isWinBoundsUpdateing) { + const bounds = browserWindow!.getBounds() + saveBoundsConfig({ + 'desktopLyric.x': bounds.x, + 'desktopLyric.y': bounds.y, + 'desktopLyric.width': bounds.width, + 'desktopLyric.height': bounds.height, + }) + } else if (isWin) { // Linux 不允许将窗口设置出屏幕之外,MacOS未知,故只在Windows下执行强制设置 + // 非主动调整窗口触发的窗口位置变化将重置回设置值 + browserWindow!.setBounds({ + x: global.lx.appSetting['desktopLyric.x'] ?? 0, + y: global.lx.appSetting['desktopLyric.y'] ?? 0, + width: global.lx.appSetting['desktopLyric.width'], + height: global.lx.appSetting['desktopLyric.height'], + }) + } + }) + + browserWindow.on('resize', () => { + // bounds = browserWindow.getBounds() + // console.log(bounds) + isWinBoundsUpdateing = true + const bounds = browserWindow!.getBounds() + saveBoundsConfig({ + 'desktopLyric.x': bounds.x, + 'desktopLyric.y': bounds.y, + 'desktopLyric.width': bounds.width, + 'desktopLyric.height': bounds.height, + }) + }) + + // browserWindow.on('restore', () => { + // browserWindow.webContents.send('restore') + // }) + // browserWindow.on('focus', () => { + // browserWindow.webContents.send('focus') + // }) + + browserWindow.once('ready-to-show', () => { + showWindow() + if (global.lx.appSetting['desktopLyric.isLock']) { + browserWindow!.setIgnoreMouseEvents(true, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] }) + } + // linux下每次重开时貌似要重新设置置顶 + // if (isLinux && global.lx.appSetting['desktopLyric.isAlwaysOnTop']) { + // browserWindow!.setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTop'], 'screen-saver') + // } + if (global.lx.appSetting['desktopLyric.isAlwaysOnTop'] && global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) alwaysOnTopTools.startLoop() + browserWindow!.blur() + }) +} + +export const createWindow = () => { + closeWindow() + if (!global.envParams.workAreaSize) return + let x = global.lx.appSetting['desktopLyric.x'] + let y = global.lx.appSetting['desktopLyric.y'] + let width = global.lx.appSetting['desktopLyric.width'] + let height = global.lx.appSetting['desktopLyric.height'] + let isAlwaysOnTop = global.lx.appSetting['desktopLyric.isAlwaysOnTop'] + // let isLockScreen = global.lx.appSetting['desktopLyric.isLockScreen'] + let isShowTaskbar = global.lx.appSetting['desktopLyric.isShowTaskbar'] + // let { width: screenWidth, height: screenHeight } = global.envParams.workAreaSize + const winSize = initWindowSize(x, y, width, height) + global.lx.event_app.update_config({ + 'desktopLyric.x': winSize.x, + 'desktopLyric.y': winSize.y, + 'desktopLyric.width': winSize.width, + 'desktopLyric.height': winSize.height, + }) + + const { shouldUseDarkColors, theme } = global.lx.theme + + /** + * Initial window options + */ + browserWindow = new BrowserWindow({ + height: winSize.height, + width: winSize.width, + x: winSize.x, + y: winSize.y, + minWidth, + minHeight, + useContentSize: true, + frame: false, + transparent: true, + hasShadow: false, + // enableRemoteModule: false, + // icon: join(global.__static, isWin ? 'icons/256x256.ico' : 'icons/512x512.png'), + resizable: isWin, + minimizable: false, + maximizable: false, + fullscreenable: false, + roundedCorners: false, + show: false, + alwaysOnTop: isAlwaysOnTop, + skipTaskbar: !isShowTaskbar, + webPreferences: { + contextIsolation: false, + webSecurity: false, + sandbox: false, + nodeIntegration: true, + enableWebSQL: false, + webgl: false, + spellcheck: false, // 禁用拼写检查器 + backgroundThrottling: false, + }, + }) + + const winURL = process.env.NODE_ENV !== 'production' ? 'http://localhost:9081/lyric.html' : `file://${path.join(encodePath(__dirname), 'lyric.html')}` + void browserWindow.loadURL(winURL + `?os=${getPlatform()}&dark=${shouldUseDarkColors}&theme=${encodeURIComponent(JSON.stringify(theme))}`) + + winEvent() + // browserWindow.webContents.openDevTools() + global.lx.event_app.desktop_lyric_window_created(browserWindow) +} +export const isExistWindow = (): boolean => !!browserWindow + +export const closeWindow = () => { + if (!browserWindow) return + browserWindow.close() +} + +export const showWindow = () => { + if (!browserWindow) return + browserWindow.show() +} + +export const setResizeable = (isResizeable: boolean) => { + if (!browserWindow) return + browserWindow.setResizable(isResizeable) +} + +export const sendEvent = (name: string, params?: T) => { + if (!browserWindow) return + mainSend(browserWindow, name, params) +} + +export const getBounds = (): Electron.Rectangle | null => { + if (!browserWindow) return null + return browserWindow.getBounds() +} + +export const setBounds = (bounds: Electron.Rectangle) => { + if (!browserWindow) return + isWinBoundsUpdateing = true + browserWindow.setBounds(bounds) +} + + +export const setIgnoreMouseEvents = (ignore: boolean, options?: Electron.IgnoreMouseEventsOptions) => { + if (!browserWindow) return + browserWindow.setIgnoreMouseEvents(ignore, options) +} + +export const setSkipTaskbar = (skip: boolean) => { + if (!browserWindow) return + browserWindow.setSkipTaskbar(skip) +} + +export const setAlwaysOnTop = (flag: boolean, level?: 'normal' | 'floating' | 'torn-off-menu' | 'modal-panel' | 'main-menu' | 'status' | 'pop-up-menu' | 'screen-saver' | undefined, relativeLevel?: number | undefined) => { + if (!browserWindow) return + browserWindow.setAlwaysOnTop(flag, level, relativeLevel) +} + +export const getMainFrame = (): Electron.WebFrameMain | null => { + if (!browserWindow) return null + return browserWindow.webContents.mainFrame +} + +interface AlwaysOnTopTools { + timeout: NodeJS.Timeout | null + setAlwaysOnTop: (isLoop: boolean) => void + startLoop: () => void + clearLoop: () => void +} +export const alwaysOnTopTools: AlwaysOnTopTools = { + timeout: null, + setAlwaysOnTop(isLoop) { + this.clearLoop() + setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTop'], 'screen-saver') + // console.log(isLoop) + if (isLoop) this.startLoop() + }, + startLoop() { + this.clearLoop() + this.timeout = setInterval(() => { + if (!isExistWindow()) { + this.clearLoop() + return + } + setAlwaysOnTop(true, 'screen-saver') + }, 500) + }, + clearLoop() { + if (!this.timeout) return + clearInterval(this.timeout) + this.timeout = null + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/mouseCheckTools.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/mouseCheckTools.ts new file mode 100644 index 000000000..b27f29fa5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/mouseCheckTools.ts @@ -0,0 +1,57 @@ +import { screen } from 'electron' +import { getBounds } from './main' +import { isLinux } from '@common/utils' + + +export const mouseCheckTools = { + timer: null as NodeJS.Timeout | null, + isMouseInWindow: false, + sendMouseLeave: null as (() => void) | null, + + onInBoundsChange(isInBounds: boolean) { + if (isInBounds) return + this.stop() + this.sendMouseLeave?.() + }, + + checkMousePosition() { + const bounds = getBounds() + if (!bounds) return + const point = screen.getCursorScreenPoint() + const isInBounds = point.x >= bounds.x && point.x <= bounds.x + bounds.width && + point.y >= bounds.y && point.y <= bounds.y + bounds.height + if (isInBounds !== this.isMouseInWindow) { + this.isMouseInWindow = isInBounds + this.onInBoundsChange(isInBounds) + } + }, + + start() { + this.timer = setTimeout(() => { + this.timer = null + this.checkMousePosition() + this.start() + }, 500) + }, + + stop() { + if (!this.timer) return + clearTimeout(this.timer) + this.timer = null + // console.log('stop check') + }, + + setMouseInWindow(isInWindow: boolean) { + this.isMouseInWindow = isInWindow + }, + runCheck(sendMouseLeave: () => void) { + if (isLinux || !global.lx.appSetting['desktopLyric.isLock'] || !global.lx.appSetting['desktopLyric.isHoverHide'] || !this.isMouseInWindow) return + // console.log('run check') + this.sendMouseLeave = sendMouseLeave + this.stop() + this.start() + }, + cacnelCheck() { + this.stop() + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/rendererEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/rendererEvent.ts new file mode 100644 index 000000000..a537e70c6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/rendererEvent.ts @@ -0,0 +1,71 @@ +import { registerRendererEvents as common } from '@main/modules/commonRenderers/common' +import { mainOn, mainHandle } from '@common/mainIpc' +import { WIN_LYRIC_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { buildLyricConfig, getLyricWindowBounds } from './utils' +import { sendNewDesktopLyricClient } from '@main/modules/winMain' +import { getBounds, getMainFrame, sendEvent, setBounds, setResizeable } from './main' +import { MessageChannelMain } from 'electron' +import { mouseCheckTools } from './mouseCheckTools' + + +export default () => { + // mainOn(WIN_LYRIC_RENDERER_EVENT_NAME.get_lyric_info, ({ params: action }) => { + // sendMainEvent(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_info, { + // name: WIN_LYRIC_RENDERER_EVENT_NAME.set_lyric_info, + // modal: 'lyricWindow', + // action, + // }) + // }) + common(sendEvent) + + mainHandle>(WIN_LYRIC_RENDERER_EVENT_NAME.set_config, async({ params: config }) => { + global.lx.event_app.update_config(config) + }) + + mainHandle(WIN_LYRIC_RENDERER_EVENT_NAME.get_config, async() => { + return buildLyricConfig(global.lx.appSetting) as LX.DesktopLyric.Config + }) + + mainOn(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_bounds, ({ params: options }) => { + setBounds(getLyricWindowBounds(getBounds()!, options)) + }) + + mainOn(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_resizeable, ({ params: resizable }) => { + setResizeable(resizable) + }) + + mainOn(WIN_LYRIC_RENDERER_EVENT_NAME.request_main_window_channel, ({ event }) => { + if (event.senderFrame !== getMainFrame()) return + // Create a new channel ... + const { port1, port2 } = new MessageChannelMain() + // ... send one end to the worker ... + sendNewDesktopLyricClient(port1) + // ... and the other end to the main window. + event.senderFrame?.postMessage(WIN_LYRIC_RENDERER_EVENT_NAME.provide_main_window_channel, null, [port2]) + // Now the main window and the worker can communicate with each other + // without going through the main process! + console.log('request_main_window_channel') + }) + + mainOn(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, ({ params: isEnter }) => { + if (isEnter) { + mouseCheckTools.setMouseInWindow(true) + mouseCheckTools.runCheck(sendMouseLeave) + } else { + mouseCheckTools.setMouseInWindow(false) + mouseCheckTools.cacnelCheck() + } + }) +} + +export const sendConfigChange = (setting: Partial) => { + sendEvent(WIN_LYRIC_RENDERER_EVENT_NAME.on_config_change, setting) +} + +export const sendMainWindowInitedEvent = () => { + sendEvent(WIN_LYRIC_RENDERER_EVENT_NAME.main_window_inited) +} + +export const sendMouseLeave = () => { + sendEvent(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, false) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/utils.ts new file mode 100644 index 000000000..2175ccd34 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winLyric/utils.ts @@ -0,0 +1,121 @@ +// 设置窗口位置、大小 +export let minWidth = 38 +export let minHeight = 38 + + +// const updateBounds = (bounds: Bounds) => { +// bounds.x = bounds.x +// return bounds +// } + +/** + * + * @param bounds 当前设置 + * @param param 新设置(相对于当前设置) + * @returns + */ +export const getLyricWindowBounds = (bounds: Electron.Rectangle, { x, y, w, h }: LX.DesktopLyric.NewBounds): Electron.Rectangle => { + if (w < minWidth) w = minWidth + if (h < minHeight) h = minHeight + + if (global.lx.appSetting['desktopLyric.isLockScreen']) { + if (!global.envParams.workAreaSize) return bounds + const maxWinW = global.envParams.workAreaSize.width + const maxWinH = global.envParams.workAreaSize.height + + if (w > maxWinW) w = maxWinW + if (h > maxWinH) h = maxWinH + + const maxX = global.envParams.workAreaSize.width - w + const maxY = global.envParams.workAreaSize.height - h + + x += bounds.x + y += bounds.y + + if (x > maxX) x = maxX + else if (x < 0) x = 0 + + if (y > maxY) y = maxY + else if (y < 0) y = 0 + } else { + y += bounds.y + x += bounds.x + } + + // console.log('util bounds', bounds) + return { width: w, height: h, x, y } +} + + +export const watchConfigKeys = [ + 'desktopLyric.enable', + 'desktopLyric.isLock', + 'desktopLyric.isAlwaysOnTop', + 'desktopLyric.isAlwaysOnTopLoop', + 'desktopLyric.isShowTaskbar', + 'desktopLyric.pauseHide', + 'desktopLyric.audioVisualization', + 'desktopLyric.width', + 'desktopLyric.height', + 'desktopLyric.x', + 'desktopLyric.y', + 'desktopLyric.isLockScreen', + 'desktopLyric.isDelayScroll', + 'desktopLyric.scrollAlign', + 'desktopLyric.isHoverHide', + 'desktopLyric.direction', + 'desktopLyric.style.align', + 'desktopLyric.style.lyricUnplayColor', + 'desktopLyric.style.lyricPlayedColor', + 'desktopLyric.style.lyricShadowColor', + 'desktopLyric.style.font', + 'desktopLyric.style.fontSize', + 'desktopLyric.style.lineGap', + // 'desktopLyric.style.fontWeight', + 'desktopLyric.style.opacity', + 'desktopLyric.style.ellipsis', + 'desktopLyric.style.isFontWeightFont', + 'desktopLyric.style.isFontWeightLine', + 'desktopLyric.style.isFontWeightExtended', + 'desktopLyric.style.isZoomActiveLrc', + 'common.langId', + 'player.isShowLyricTranslation', + 'player.isShowLyricRoma', + 'player.isSwapLyricTranslationAndRoma', + 'player.isPlayLxlrc', + 'player.playbackRate', +] satisfies Array + +export const buildLyricConfig = (appSetting: Partial): Partial => { + const setting: Partial = {} + for (const key of watchConfigKeys) { + // @ts-expect-error + if (key in appSetting) setting[key] = appSetting[key] + } + return setting +} + +export const initWindowSize = (x: LX.AppSetting['desktopLyric.x'], y: LX.AppSetting['desktopLyric.y'], width: LX.AppSetting['desktopLyric.width'], height: LX.AppSetting['desktopLyric.height']) => { + if (x == null || y == null) { + if (width < minWidth) width = minWidth + if (height < minHeight) height = minHeight + if (global.envParams.workAreaSize) { + x = global.envParams.workAreaSize.width - width + y = global.envParams.workAreaSize.height - height + } else { + x = y = 0 + } + } else { + let bounds = getLyricWindowBounds({ x, y, width, height }, { x: 0, y: 0, w: width, h: height }) + x = bounds.x + y = bounds.y + width = bounds.width + height = bounds.height + } + return { + x, + y, + width, + height, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/autoUpdate.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/autoUpdate.ts new file mode 100644 index 000000000..5f45ac166 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/autoUpdate.ts @@ -0,0 +1,147 @@ +import { autoUpdater } from 'electron-updater' +import { log, isWin } from '@common/utils' +import { mainOn } from '@common/mainIpc' +import { isExistWindow, sendEvent } from './index' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' + +autoUpdater.logger = log +autoUpdater.autoDownload = false +// autoUpdater.forceDevUpdateConfig = true +// autoUpdater.autoDownload = false + +// let isFirstCheckedUpdate = true + +log.info('App starting...') + + +// ------------------------------------------------------------------- +// Open a window that displays the version +// +// THIS SECTION IS NOT REQUIRED +// +// This isn't required for auto-updates to work, but it's easier +// for the app to show a window than to have to click "About" to see +// that updates are working. +// ------------------------------------------------------------------- +// let win + +function sendStatusToWindow(text: string) { + log.info(text) + // ipcMain.send('message', text) +} + + +// ------------------------------------------------------------------- +// Auto updates +// +// For details about these events, see the Wiki: +// https://github.com/electron-userland/electron-builder/wiki/Auto-Update#events +// +// The app doesn't need to listen to any events except `update-downloaded` +// +// Uncomment any of the below events to listen for them. Also, +// look in the previous section to see them being used. +// ------------------------------------------------------------------- +// autoUpdater.on('checking-for-update', () => { +// }) +// autoUpdater.on('update-available', (ev, info) => { +// }) +// autoUpdater.on('update-not-available', (ev, info) => { +// }) +// autoUpdater.on('error', (ev, err) => { +// }) +// autoUpdater.on('download-progress', (ev, progressObj) => { +// }) +// autoUpdater.on('update-downloaded', (ev, info) => { +// // Wait 5 seconds, then quit and install +// // In your application, you don't need to wait 5 seconds. +// // You could call autoUpdater.quitAndInstall(); immediately +// // setTimeout(function() { +// // autoUpdater.quitAndInstall() +// // }, 5000) + +// }) + +interface WaitEvent { + type: string + info: any +} + +// let waitEvent: WaitEvent[] = [] +const handleSendEvent = (action: WaitEvent) => { + if (isExistWindow()) { + setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成 + sendEvent(action.type, action.info) + }, 1000) + } +} + +export default () => { + autoUpdater.on('checking-for-update', () => { + sendStatusToWindow('Checking for update...') + }) + autoUpdater.on('update-available', info => { + sendStatusToWindow('Update available.') + handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_available, info }) + }) + autoUpdater.on('update-not-available', info => { + sendStatusToWindow('Update not available.') + handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_not_available, info }) + }) + autoUpdater.on('error', err => { + sendStatusToWindow('Error in auto-updater.') + handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_error, info: err.message }) + }) + autoUpdater.on('download-progress', progressObj => { + let log_message = `Download speed: ${progressObj.bytesPerSecond}` + log_message = `${log_message} - Downloaded ${progressObj.percent}%` + log_message = `${log_message} (progressObj.transferred/${progressObj.total})` + sendStatusToWindow(log_message) + handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_progress, info: progressObj }) + }) + autoUpdater.on('update-downloaded', info => { + sendStatusToWindow('Update downloaded.') + handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_downloaded, info }) + }) + + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.update_check, () => { + console.log('check') + checkUpdate() + }) + + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.update_download_update, () => { + if (!autoUpdater.isUpdaterActive()) return + void autoUpdater.downloadUpdate() + }) + + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.quit_update, () => { + global.lx.isSkipTrayQuit = true + + setTimeout(() => { + autoUpdater.quitAndInstall(true, true) + }, 1000) + }) +} + +const checkUpdate = () => { + // if (!isFirstCheckedUpdate) { + // if (waitEvent.length) { + // waitEvent.forEach((event, index) => { + // setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成 + // sendEvent(event.type, event.info) + // }, 2000 * (index + 1)) + // }) + // waitEvent = [] + // } + // return + // } + // isFirstCheckedUpdate = false + + // 由于集合安装包中不包含win arm版,这将会导致arm版更新失败 + if (isWin && process.arch.includes('arm')) { + handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_error, info: 'failed' }) + } else { + autoUpdater.autoDownload = global.lx.appSetting['common.tryAutoUpdate'] + void autoUpdater.checkForUpdates() + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/index.ts new file mode 100644 index 000000000..c05e4466d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/index.ts @@ -0,0 +1,119 @@ +import initRendererEvent, { handleKeyDown, hotKeyConfigUpdate } from './rendererEvent' + +import { APP_EVENT_NAMES } from '@common/constants' +import { createWindow, minimize, setProgressBar, setProxy, setThumbarButtons, toggleHide, toggleMinimize } from './main' +import initUpdate from './autoUpdate' +import { HOTKEY_COMMON } from '@common/hotKey' +import { quitApp } from '@main/app' + +export default () => { + initRendererEvent() + initUpdate() + + global.lx.event_app.on('hot_key_down', ({ type, key }) => { + let info = global.lx.hotKey.config.global.keys[key] + if (info?.type != APP_EVENT_NAMES.winMainName) return + switch (info.action) { + case HOTKEY_COMMON.close.action: + quitApp() + break + case HOTKEY_COMMON.hide_toggle.action: + toggleHide() + break + case HOTKEY_COMMON.min.action: + minimize() + break + case HOTKEY_COMMON.min_toggle.action: + toggleMinimize() + break + default: + handleKeyDown(type, key) + break + } + }) + global.lx.event_app.on('hot_key_config_update', (config) => { + hotKeyConfigUpdate(config) + }) + + global.lx.event_app.on('app_inited', () => { + createWindow() + }) + + const keys = (['status', 'collect'] as const) satisfies Array + const taskBarButtonFlags: LX.TaskBarButtonFlags = { + empty: true, + collect: false, + play: false, + next: true, + prev: true, + } + const progressStatus = { + progress: -1, + status: 'none' as Electron.ProgressBarOptions['mode'], + } + let showProgress = global.lx.appSetting['player.isShowTaskProgess'] + global.lx.event_app.on('player_status', (status) => { + if (status.status) { + switch (status.status) { + case 'paused': + taskBarButtonFlags.play = false + taskBarButtonFlags.empty &&= false + progressStatus.status = 'paused' + break + case 'error': + taskBarButtonFlags.play = false + taskBarButtonFlags.empty &&= false + progressStatus.status = 'error' + break + case 'playing': + taskBarButtonFlags.play = true + taskBarButtonFlags.empty &&= false + progressStatus.status = 'normal' + break + case 'stoped': + taskBarButtonFlags.play &&= false + taskBarButtonFlags.empty = true + progressStatus.status = 'none' + progressStatus.progress = 0 + break + } + if (showProgress) { + setProgressBar(progressStatus.progress, { + mode: progressStatus.status, + }) + } + } + if (keys.some(k => status[k] != null)) { + if (status.collect != null) taskBarButtonFlags.collect = status.collect + setThumbarButtons(taskBarButtonFlags) + } + if (showProgress && status.progress != null) { + const progress = global.lx.player_status.duration ? status.progress / global.lx.player_status.duration : 0 + if (progress.toFixed(2) != progressStatus.progress.toFixed(2)) { + progressStatus.progress = progress < 0.01 ? 0.01 : progress + setProgressBar(progressStatus.progress, { + mode: progressStatus.status, + }) + } + } + }) + global.lx.event_app.on('updated_config', (keys, setting) => { + if (keys.includes('player.isShowTaskProgess')) { + showProgress = setting['player.isShowTaskProgess']! + if (showProgress) { + setProgressBar(progressStatus.progress, { + mode: progressStatus.status, + }) + } else { + setProgressBar(-1, { mode: 'none' }) + } + } + if (keys.includes('network.proxy.enable') || (global.lx.appSetting['network.proxy.enable'] && keys.some(k => k.includes('network.proxy.')))) { + setProxy() + } + }) +} + +export * from './main' +export * from './rendererEvent' + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/main.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/main.ts new file mode 100644 index 000000000..cedb60961 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/main.ts @@ -0,0 +1,300 @@ +import { BrowserWindow, dialog, session } from 'electron' +import path from 'node:path' +import { createTaskBarButtons, getWindowSizeInfo } from './utils' +import { getOSVersion, getPlatform, isLinux, isMac, isWin } from '@common/utils' +import { getProxy, openDevTools as handleOpenDevTools } from '@main/utils' +import { mainSend } from '@common/mainIpc' +import { sendFocus, sendTaskbarButtonClick } from './rendererEvent' +import { encodePath } from '@common/utils/electron' + +let browserWindow: Electron.BrowserWindow | null = null + +const winEvent = () => { + if (!browserWindow) return + + browserWindow.on('close', event => { + if (global.lx.isSkipTrayQuit || !global.lx.appSetting['tray.enable']) { + browserWindow!.setProgressBar(-1) + // global.lx.mainWindowClosed = true + global.lx.event_app.main_window_close() + return + } + + event.preventDefault() + browserWindow!.hide() + }) + + browserWindow.on('closed', () => { + // global.lx.mainWindowClosed = true + browserWindow = null + }) + + // browserWindow.on('restore', () => { + // browserWindow.webContents.send('restore') + // }) + browserWindow.on('focus', () => { + sendFocus() + global.lx.event_app.main_window_focus() + }) + + browserWindow.on('blur', () => { + global.lx.event_app.main_window_blur() + }) + browserWindow.on('enter-full-screen', () => { + global.lx.event_app.main_window_fullscreen(true) + }) + browserWindow.on('leave-full-screen', () => { + global.lx.event_app.main_window_fullscreen(false) + + // macOS needs here to set resizable to false after exiting full screen + if (isMac) { + if (browserWindow?.resizable) { + browserWindow.setResizable(false) + } + } + }) + + browserWindow.once('ready-to-show', () => { + if (!global.envParams.cmdParams.hidden) { + showWindow() + setThumbarButtons() + } + global.lx.event_app.main_window_ready_to_show() + }) + + browserWindow.on('show', () => { + global.lx.event_app.main_window_show() + + // 修复隐藏窗口后再显示时任务栏按钮丢失的问题 + setThumbarButtons() + }) + browserWindow.on('hide', () => { + global.lx.event_app.main_window_hide() + }) +} + + +export const createWindow = () => { + closeWindow() + const windowSizeInfo = getWindowSizeInfo(global.lx.appSetting['common.windowSizeId']) + + const { shouldUseDarkColors, theme } = global.lx.theme + const ses = session.fromPartition('persist:win-main') + const proxy = getProxy() + setSesProxy(ses, proxy?.host, proxy?.port) + + /** + * Initial window options + */ + const options: Electron.BrowserWindowConstructorOptions = { + height: windowSizeInfo.height, + useContentSize: true, + width: windowSizeInfo.width, + frame: false, + transparent: !global.envParams.cmdParams.dt, + hasShadow: global.envParams.cmdParams.dt, + // enableRemoteModule: false, + // icon: join(global.__static, isWin ? 'icons/256x256.ico' : 'icons/512x512.png'), + resizable: false, + maximizable: false, + fullscreenable: true, + roundedCorners: global.envParams.cmdParams.dt, + show: false, + webPreferences: { + session: ses, + nodeIntegrationInWorker: true, + contextIsolation: false, + webSecurity: false, + nodeIntegration: true, + sandbox: false, + enableWebSQL: false, + webgl: false, + spellcheck: false, // 禁用拼写检查器 + }, + } + if (global.envParams.cmdParams.dt) options.backgroundColor = theme.colors['--color-primary-light-1000'] + if (global.lx.appSetting['common.startInFullscreen']) { + options.fullscreen = true + if (isLinux) options.resizable = true + } + browserWindow = new BrowserWindow(options) + + const winURL = process.env.NODE_ENV !== 'production' ? 'http://localhost:9080' : `file://${path.join(encodePath(__dirname), 'index.html')}` + void browserWindow.loadURL(winURL + `?os=${getPlatform()}&osver=${encodeURIComponent(getOSVersion())}&dt=${global.envParams.cmdParams.dt}&dark=${shouldUseDarkColors}&theme=${encodeURIComponent(JSON.stringify(theme))}`) + + winEvent() + + if (global.envParams.cmdParams.odt) handleOpenDevTools(browserWindow.webContents) + + // global.lx.mainWindowClosed = false + // browserWindow.webContents.openDevTools() + global.lx.event_app.main_window_created(browserWindow) +} + +export const isExistWindow = (): boolean => !!browserWindow +export const isShowWindow = (): boolean => { + if (!browserWindow) return false + return browserWindow.isVisible() && (isWin ? true : browserWindow.isFocused()) +} + +export const closeWindow = () => { + if (!browserWindow) return + browserWindow.close() +} + +const setSesProxy = (ses: Electron.Session, host?: string, port?: string | number) => { + if (host) { + void ses.setProxy({ + mode: 'fixed_servers', + proxyRules: `http://${host}:${port}`, + }) + } else { + void ses.setProxy({ + mode: 'direct', + }) + } +} +export const setProxy = () => { + if (!browserWindow) return + const proxy = getProxy() + setSesProxy(browserWindow.webContents.session, proxy?.host, proxy?.port) +} + + +export const sendEvent = (name: string, params?: T) => { + if (!browserWindow) return + mainSend(browserWindow, name, params) +} + +export const showSelectDialog = async(options: Electron.OpenDialogOptions) => { + if (!browserWindow) throw new Error('main window is undefined') + return dialog.showOpenDialog(browserWindow, options) +} +export const showDialog = ({ type, message, detail }: Electron.MessageBoxSyncOptions) => { + if (!browserWindow) return + dialog.showMessageBoxSync(browserWindow, { + type, + message, + detail, + }) +} +export const showSaveDialog = async(options: Electron.SaveDialogOptions) => { + if (!browserWindow) throw new Error('main window is undefined') + return dialog.showSaveDialog(browserWindow, options) +} +export const minimize = () => { + if (!browserWindow) return + browserWindow.minimize() +} +export const maximize = () => { + if (!browserWindow) return + browserWindow.maximize() +} +export const unmaximize = () => { + if (!browserWindow) return + browserWindow.unmaximize() +} +export const toggleHide = () => { + if (!browserWindow) return + browserWindow.isVisible() + ? browserWindow.hide() + : browserWindow.show() +} +export const toggleMinimize = () => { + if (!browserWindow) return + if (browserWindow.isVisible()) { + if (browserWindow.isMinimized()) browserWindow.restore() + else browserWindow.minimize() + } else browserWindow.show() +} +export const showWindow = () => { + if (!browserWindow) return + if (browserWindow.isVisible()) { + if (browserWindow.isMinimized()) browserWindow.restore() + else browserWindow.focus() + } else browserWindow.show() +} +export const hideWindow = () => { + if (!browserWindow) return + browserWindow.hide() +} +export const setWindowBounds = (options: Partial) => { + if (!browserWindow) return + browserWindow.setBounds(options) +} +export const setProgressBar = (progress: number, options?: Electron.ProgressBarOptions) => { + if (!browserWindow) return + browserWindow.setProgressBar(progress, options) +} +export const setIgnoreMouseEvents = (ignore: boolean, options?: Electron.IgnoreMouseEventsOptions) => { + if (!browserWindow) return + browserWindow.setIgnoreMouseEvents(ignore, options) +} +export const toggleDevTools = () => { + if (!browserWindow) return + if (browserWindow.webContents.isDevToolsOpened()) { + browserWindow.webContents.closeDevTools() + } else { + handleOpenDevTools(browserWindow.webContents) + } +} + +export const setFullScreen = (isFullscreen: boolean): boolean => { + if (!browserWindow) return false + // https://github.com/any-listen/any-listen/issues/190 + // in electron ^41.2.0, windows -dt mode need to set resizable to true before setting full screen + if (!!global.envParams.cmdParams.dt || isLinux) { + // linux 需要先设置为可调整窗口大小才能全屏 + if (isFullscreen) { + browserWindow.setResizable(isFullscreen) + browserWindow.setFullScreen(isFullscreen) + } else { + browserWindow.setFullScreen(isFullscreen) + // windows/linux need to set resizable to true after exiting full screen + if (!isMac) browserWindow.setResizable(isFullscreen) + } + } else { + browserWindow.setFullScreen(isFullscreen) + } + return isFullscreen +} + +const taskBarButtonFlags: LX.TaskBarButtonFlags = { + empty: true, + collect: false, + play: false, + next: true, + prev: true, +} +export const setThumbarButtons = ({ empty, collect, play, next, prev }: LX.TaskBarButtonFlags = taskBarButtonFlags) => { + if (!isWin || !browserWindow) return + taskBarButtonFlags.empty = empty + taskBarButtonFlags.collect = collect + taskBarButtonFlags.play = play + taskBarButtonFlags.next = next + taskBarButtonFlags.prev = prev + browserWindow.setThumbarButtons(createTaskBarButtons(taskBarButtonFlags, action => { + sendTaskbarButtonClick(action) + })) +} + +export const setThumbnailClip = (region: Electron.Rectangle) => { + if (!browserWindow) return + browserWindow.setThumbnailClip(region) +} + + +export const clearCache = async() => { + if (!browserWindow) throw new Error('main window is undefined') + await browserWindow.webContents.session.clearCache() +} + +export const getCacheSize = async() => { + if (!browserWindow) throw new Error('main window is undefined') + return browserWindow.webContents.session.getCacheSize() +} + +export const getWebContents = (): Electron.WebContents => { + if (!browserWindow) throw new Error('main window is undefined') + return browserWindow.webContents +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/app.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/app.ts new file mode 100644 index 000000000..a4d09102b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/app.ts @@ -0,0 +1,145 @@ +// const path = require('path') +import { app } from 'electron' +import { mainHandle, mainOn } from '@common/mainIpc' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +// import { name as defaultName } from '../../../../../package.json' +import { + minimize, + maximize, + closeWindow, + showWindow, + setFullScreen, + sendEvent, + clearCache, + getCacheSize, + toggleDevTools, + setWindowBounds, + setIgnoreMouseEvents, + // setThumbnailClip, + toggleMinimize, + toggleHide, + showSelectDialog, + showDialog, + showSaveDialog, +} from '@main/modules/winMain' +import { quitApp } from '@main/app' +import { getAllThemes, removeTheme, saveTheme, setPowerSaveBlocker } from '@main/utils' +import { openDirInExplorer } from '@common/utils/electron' + +export default () => { + // 设置应用名称 + // mainOn(WIN_MAIN_RENDERER_EVENT_NAME.set_app_name, ({ params: name }) => { + // if (name == null) { + // app.setName(defaultName) + // } else { + // app.setName(name) + // } + // }) + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.quit, () => { + quitApp() + }) + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.min_toggle, () => { + toggleMinimize() + }) + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.hide_toggle, () => { + toggleHide() + }) + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.min, () => { + minimize() + }) + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.max, () => { + maximize() + }) + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.focus, () => { + showWindow() + }) + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.set_power_save_blocker, ({ params: enabled }) => { + setPowerSaveBlocker(enabled) + }) + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.close, ({ params: isForce }) => { + if (isForce) { + app.exit(0) + return + } + closeWindow() + }) + // 全屏 + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.fullscreen, async({ params: isFullscreen }) => { + global.lx.event_app.main_window_fullscreen(isFullscreen) + return setFullScreen(isFullscreen) + }) + + // 选择目录 + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.show_select_dialog, async({ params: options }) => { + return showSelectDialog(options) + }) + // 显示弹窗信息 + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.show_dialog, ({ params }) => { + showDialog(params) + }) + // 显示保存弹窗 + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.show_save_dialog, async({ params }) => { + return showSaveDialog(params) + }) + // 在资源管理器中定位文件 + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.open_dir_in_explorer, async({ params }) => { + return openDirInExplorer(params) + }) + + + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_cache, async() => { + await clearCache() + }) + + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_cache_size, async() => { + return getCacheSize() + }) + + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.open_dev_tools, () => { + toggleDevTools() + }) + + mainOn>(WIN_MAIN_RENDERER_EVENT_NAME.set_window_size, ({ params }) => { + setWindowBounds(params) + }) + + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.set_ignore_mouse_events, ({ params: isIgnored }) => { + isIgnored + ? setIgnoreMouseEvents(isIgnored, { forward: true }) + : setIgnoreMouseEvents(false) + }) + + // mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_set_thumbnail_clip, async({ params }) => { + // return setThumbnailClip(params) + // }) + + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.player_status, ({ params }) => { + // setThumbarButtons(params) + global.lx.event_app.player_status(params) + }) + + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.inited, () => { + global.lx.event_app.main_window_inited() + }) + + mainHandle<{ themes: LX.Theme[], userThemes: LX.Theme[] }>(WIN_MAIN_RENDERER_EVENT_NAME.get_themes, async() => { + return getAllThemes() + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.save_theme, async({ params: theme }) => { + saveTheme(theme) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.remove_theme, async({ params: id }) => { + removeTheme(id) + }) +} + +export const sendFocus = () => { + sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.focus) +} + +export const sendTaskbarButtonClick = (action: LX.Player.StatusButtonActions, data?: unknown) => { + sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.player_action_on_button_click, { action, data }) +} +export const sendConfigChange = (setting: Partial) => { + sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.on_config_change, setting) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/data.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/data.ts new file mode 100644 index 000000000..fcbdd5d33 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/data.ts @@ -0,0 +1,17 @@ +import { STORE_NAMES } from '@common/constants' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { mainOn, mainHandle } from '@common/mainIpc' +import getStore from '@main/utils/store' + +export default () => { + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_data, ({ params: path }) => { + return getStore(STORE_NAMES.DATA).get(path) as any + }) + + mainOn<{ + path: string + data: any + }>(WIN_MAIN_RENDERER_EVENT_NAME.save_data, ({ params: { path, data } }) => { + getStore(STORE_NAMES.DATA).set(path, data) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/download.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/download.ts new file mode 100644 index 000000000..46ef026c5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/download.ts @@ -0,0 +1,21 @@ +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { mainHandle } from '@common/mainIpc' + + +export default () => { + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.download_list_get, async() => { + return global.lx.worker.dbService.getDownloadList() + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.download_list_add, async({ params: { list, addMusicLocationType } }) => { + await global.lx.worker.dbService.downloadInfoSave(list, addMusicLocationType) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.download_list_update, async({ params: list }) => { + await global.lx.worker.dbService.downloadInfoUpdate(list) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.download_list_remove, async({ params: ids }) => { + await global.lx.worker.dbService.downloadInfoRemove(ids) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.download_list_clear, async() => { + await global.lx.worker.dbService.downloadInfoClear() + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/hotKey.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/hotKey.ts new file mode 100644 index 000000000..84ffbe016 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/hotKey.ts @@ -0,0 +1,42 @@ +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { mainHandle } from '@common/mainIpc' +import { sendEvent } from '../main' +// import getStore from '@common/store' + + +// const { registerHotkey, unRegisterHotkey } = require('../modules/hotKey/utils') + +// mainHandle(ipcMainWindowNames.set_hot_key_config, async(event, { action, data }) => { +// switch (action) { +// case 'config': +// global.lx_event.hotKey.saveConfig(data.data, MAIN_WINDOW_EVENT_NAME.source) +// return +// case 'register': +// return registerHotkey(data) +// case 'unregister': +// return unRegisterHotkey(data) +// } +// }) + +export default () => { + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_hot_key, async() => { + // const electronStore_hotKey = getStore('hotKey') + return { + local: global.lx.hotKey?.config.local, + global: global.lx.hotKey?.config.global, + } + }) + + // global.lx.event_app.on(APP_EVENT_NAMES.hotKeyConfig, (config, source) => { + // if (!global.modules.mainWindow || source === MAIN_WINDOW_EVENT_NAME.name) return + // mainSend(global.modules.mainWindow, WIN_MAIN_RENDERER_EVENT_NAME.set_hot_key_config, { config, source }) + // }) +} + +export const handleKeyDown = (type: string, key: string) => { + sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.key_down, { type, key }) +} + +export const hotKeyConfigUpdate = (config: LX.HotKeyConfigAll) => { + sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.set_hot_key_config, config) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/index.ts new file mode 100644 index 000000000..65ea458a7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/index.ts @@ -0,0 +1,43 @@ +import { registerRendererEvents as common } from '@main/modules/commonRenderers/common' +import { registerRendererEvents as list } from '@main/modules/commonRenderers/list' +import { registerRendererEvents as dislike } from '@main/modules/commonRenderers/dislike' +import app, { sendConfigChange } from './app' +import hotKey from './hotKey' +import userApi from './userApi' +import sync from './sync' +import data from './data' +import music from './music' +import download from './download' +import soundEffect from './soundEffect' +import openAPI from './openAPI' +import { sendEvent } from '../main' + +export * from './app' +export * from './hotKey' +export * from './userApi' +export * from './sync' +export * from './process' + +let isInitialized = false +export default () => { + if (isInitialized) return + isInitialized = true + + common(sendEvent) + list(sendEvent) + dislike(sendEvent) + app() + hotKey() + userApi() + sync() + data() + music() + download() + soundEffect() + openAPI() + + global.lx.event_app.on('updated_config', (keys, setting) => { + sendConfigChange(setting) + }) +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/music.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/music.ts new file mode 100644 index 000000000..df1cc8569 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/music.ts @@ -0,0 +1,113 @@ +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { mainHandle } from '@common/mainIpc' + + +export default () => { + // =========================歌词========================= + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_palyer_lyric, async({ params: id }) => { + // return (getStore(LRC_EDITED, true, false).get(id) as LX.Music.LyricInfo | undefined) ?? + // getStore(LRC_RAW, true, false).get(id, {}) as LX.Music.LyricInfo + return global.lx.worker.dbService.getPlayerLyric(id) + }) + + // 原始歌词 + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_raw, async({ params: id }) => { + return global.lx.worker.dbService.getRawLyric(id) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_raw, async({ params: { id, lyrics } }) => { + await global.lx.worker.dbService.rawLyricAdd(id, lyrics) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_raw, async() => { + await global.lx.worker.dbService.rawLyricClear() + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_raw_count, async() => { + return global.lx.worker.dbService.rawLyricCount() + }) + + // 已编辑的歌词 + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_edited, async({ params: id }) => { + return global.lx.worker.dbService.getEditedLyric(id) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_edited, async({ params: { id, lyrics } }) => { + await global.lx.worker.dbService.editedLyricUpdateAddAndUpdate(id, lyrics) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.remove_lyric_edited, async({ params: id }) => { + await global.lx.worker.dbService.editedLyricRemove([id]) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_edited, async() => { + await global.lx.worker.dbService.editedLyricClear() + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_edited_count, async() => { + return global.lx.worker.dbService.editedLyricCount() + }) + + + // =========================歌曲URL========================= + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_music_url, async({ params: id }) => { + return (await global.lx.worker.dbService.getMusicUrl(id)) ?? '' + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.save_music_url, async({ params: { id, url } }) => { + await global.lx.worker.dbService.musicUrlSave([{ id, url }]) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_music_url, async() => { + await global.lx.worker.dbService.musicUrlClear() + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_music_url_count, async() => { + return global.lx.worker.dbService.musicUrlCount() + }) + + // =========================换源歌曲========================= + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_other_source, async({ params: id }) => { + return global.lx.worker.dbService.getMusicInfoOtherSource(id) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.save_other_source, async({ params: { id, list } }) => { + await global.lx.worker.dbService.musicInfoOtherSourceAdd(id, list) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_other_source, async() => { + await global.lx.worker.dbService.musicInfoOtherSourceClear() + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_other_source_count, async() => { + return global.lx.worker.dbService.musicInfoOtherSourceCount() + }) + + // mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.remove_dislike_music_infos, async({ params: ids }) => { + // await global.lx.worker.dbService.dislikeInfoRemove(ids) + // }) + // mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_dislike_music_infos, async() => { + // await global.lx.worker.dbService.dislikeInfoClear() + // }) + + + // =========================我的列表========================= + // mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_playlist, async({ params: isIgnoredError = false }) => { + // const electronStore_list = getStore('playList', isIgnoredError, false) + + // return { + // defaultList: electronStore_list.get('defaultList'), + // loveList: electronStore_list.get('loveList'), + // tempList: electronStore_list.get('tempList'), + // userList: electronStore_list.get('userList'), + // downloadList: getStore('downloadList').get('list'), + // } + // }) + + // const handleSaveList = ({ defaultList, loveList, userList, tempList }: Partial) => { + // let data: Partial = {} + // if (defaultList != null) data.defaultList = defaultList + // if (loveList != null) data.loveList = loveList + // if (userList != null) data.userList = userList + // if (tempList != null) data.tempList = tempList + // getStore('playList').set(data) + // } + // mainOn(WIN_MAIN_RENDERER_EVENT_NAME.save_playlist, ({ params }) => { + // switch (params.type) { + // case 'myList': + // handleSaveList(params.data) + // global.lx.event_app.save_my_list(params.data) + // break + // case 'downloadList': + // getStore('downloadList').set('list', params.data) + // break + // } + // }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/openAPI.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/openAPI.ts new file mode 100644 index 000000000..1a1ad4fa1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/openAPI.ts @@ -0,0 +1,18 @@ +import { mainHandle } from '@common/mainIpc' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { + startServer, + stopServer, + getStatus, +} from '@main/modules/openApi' + + +export default () => { + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.open_api_action, async({ params: data }) => { + switch (data.action) { + case 'enable': + return data.data.enable ? await startServer(parseInt(data.data.port), data.data.bindLan) : await stopServer() + case 'status': return getStatus() + } + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/process.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/process.ts new file mode 100644 index 000000000..0c28efa74 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/process.ts @@ -0,0 +1,18 @@ +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { getWebContents } from '../main' + + +// export default () => { + + +// } + +/** + * 发送桌面歌词进程创建事件 + * @param port 端口 + */ +export const sendNewDesktopLyricClient = (port: Electron.MessagePortMain) => { + getWebContents().postMessage(WIN_MAIN_RENDERER_EVENT_NAME.process_new_desktop_lyric_client, null, [port]) +} + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/soundEffect.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/soundEffect.ts new file mode 100644 index 000000000..42e58226f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/soundEffect.ts @@ -0,0 +1,27 @@ +import { STORE_NAMES } from '@common/constants' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { mainOn, mainHandle } from '@common/mainIpc' +import getStore from '@main/utils/store' + +export default () => { + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_eq_preset, async() => { + return getStore(STORE_NAMES.SOUND_EFFECT).get('eqPreset') as LX.SoundEffect.EQPreset[] | null ?? [] + }) + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_eq_preset, ({ params }) => { + getStore(STORE_NAMES.SOUND_EFFECT).set('eqPreset', params) + }) + + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_convolution_preset, async() => { + return getStore(STORE_NAMES.SOUND_EFFECT).get('convolutionPreset') as LX.SoundEffect.ConvolutionPreset[] | null ?? [] + }) + mainOn(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, ({ params }) => { + getStore(STORE_NAMES.SOUND_EFFECT).set('convolutionPreset', params) + }) + + // mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_pitch_shifter_preset, async() => { + // return getStore(STORE_NAMES.SOUND_EFFECT).get('pitchShifterPreset') as LX.SoundEffect.PitchShifterPreset[] | null ?? [] + // }) + // mainOn(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_pitch_shifter_preset, ({ params }) => { + // getStore(STORE_NAMES.SOUND_EFFECT).set('pitchShifterPreset', params) + // }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/sync.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/sync.ts new file mode 100644 index 000000000..58e7f723f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/sync.ts @@ -0,0 +1,76 @@ +import { mainHandle } from '@common/mainIpc' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { + startServer, + stopServer, + getServerStatus, + generateCode, + connectServer, + disconnectServer, + getClientStatus, + getServerDevices, + removeServerDevice, +} from '@main/modules/sync' +import { sendEvent } from '../main' + + +let selectModeListenr: ((mode: LX.Sync.ModeTypes[keyof LX.Sync.ModeTypes] | null) => void) | null = null + +export default () => { + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, async({ params: data }) => { + switch (data.action) { + case 'enable_server': + data.data.enable ? await startServer(parseInt(data.data.port)) : await stopServer() + return + case 'enable_client': + data.data.enable ? await connectServer(data.data.host, data.data.authCode) : await disconnectServer() + return + case 'get_server_status': return getServerStatus() + case 'get_client_status': return getClientStatus() + case 'generate_code': return generateCode() + case 'select_mode': + if (selectModeListenr) { + selectModeListenr(data.data.mode) + selectModeListenr = null + } + break + default: + break + } + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.sync_get_server_devices, async() => { + return getServerDevices() + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.sync_remove_server_device, async({ params: clientId }) => { + await removeServerDevice(clientId) + }) +} + + +export const sendSyncAction = (data: LX.Sync.SyncMainWindowActions) => { + sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, data) +} + +export const sendClientStatus = (status: LX.Sync.ClientStatus) => { + sendSyncAction({ + action: 'client_status', + data: status, + }) +} +export const sendServerStatus = (status: LX.Sync.ServerStatus) => { + sendSyncAction({ + action: 'server_status', + data: status, + }) +} +export const sendSelectMode = (deviceName: string, type: T, listener: (mode: LX.Sync.ModeTypes[T] | null) => void) => { + selectModeListenr = listener as typeof selectModeListenr + sendSyncAction({ action: 'select_mode', data: { deviceName, type } }) +} +export const removeSelectModeListener = () => { + if (selectModeListenr) selectModeListenr(null) + selectModeListenr = null +} +export const sendCloseSelectMode = () => { + sendSyncAction({ action: 'close_select_mode' }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/userApi.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/userApi.ts new file mode 100644 index 000000000..06f9e6cf2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/rendererEvent/userApi.ts @@ -0,0 +1,54 @@ +import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' +import { mainHandle } from '@common/mainIpc' +import { + getApiList, + importApi, + removeApi, + setApi, + getStatus, + request, + cancelRequest, + setAllowShowUpdateAlert, +} from '@main/modules/userApi' +import { sendEvent } from '@main/modules/winMain/main' + +export default () => { + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.import_user_api, async({ params: script }) => { + return importApi(script) + }) + + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.remove_user_api, async({ params: apiIds }) => { + return removeApi(apiIds) + }) + + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.set_user_api, async({ params: apiId }) => { + await setApi(apiId) + }) + + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_user_api_list, async() => { + return getApiList() + }) + + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_user_api_status, async() => { + return getStatus() + }) + + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.user_api_set_allow_update_alert, async({ params: { id, enable } }) => { + setAllowShowUpdateAlert(id, enable) + }) + + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api, async({ params }) => { + return request(params) + }) + mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api_cancel, async({ params: requestKey }) => { + cancelRequest(requestKey) + }) +} + +export const sendStatusChange = (status: LX.UserApi.UserApiStatus) => { + sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.user_api_status, status) +} +export const sendShowUpdateAlert = (info: LX.UserApi.UserApiUpdateInfo) => { + sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.user_api_show_update_alert, info) +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/utils.ts new file mode 100644 index 000000000..48927224b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/modules/winMain/utils.ts @@ -0,0 +1,79 @@ +// import fs from 'fs' +import path from 'node:path' +import { type WindowSize, windowSizeList } from '@common/config' +import { nativeImage } from 'electron' + +export const getWindowSizeInfo = (windowSizeId: number | string): WindowSize => { + return windowSizeList.find(i => i.id == windowSizeId) ?? windowSizeList[0] +} + +const getIconPath = (name: string): Electron.NativeImage => { + return nativeImage.createFromPath(path.join(global.staticPath, 'images/taskbar', name + '.png')) +} + +export const createTaskBarButtons = ({ + empty = false, + collect = false, + play = false, + next = true, + prev = true, +}: LX.TaskBarButtonFlags, onClick: (action: LX.Player.StatusButtonActions) => void): Electron.ThumbarButton[] => { + const buttons: Electron.ThumbarButton[] = [ + collect + ? { + icon: getIconPath('collected'), + click() { + onClick('unCollect') + }, + tooltip: '取消收藏', + flags: ['nobackground'], + } + : { + icon: getIconPath('collect'), + click() { + onClick('collect') + }, + tooltip: '收藏', + flags: ['nobackground'], + }, + { + icon: getIconPath('prev'), + click() { + onClick('prev') + }, + tooltip: '上一曲', + flags: prev ? ['nobackground'] : ['nobackground', 'disabled'], + }, + play + ? { + icon: getIconPath('pause'), + click() { + onClick('pause') + }, + tooltip: '暂停', + flags: ['nobackground'], + } + : { + icon: getIconPath('play'), + click() { + onClick('play') + }, + tooltip: '播放', + flags: ['nobackground'], + }, + { + icon: getIconPath('next'), + click() { + onClick('next') + }, + tooltip: '下一曲', + flags: next ? ['nobackground'] : ['nobackground', 'disabled'], + }, + ] + if (empty) { + for (const button of buttons) { + button.flags = ['nobackground', 'disabled'] + } + } + return buttons +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/tsconfig.json b/plugins/lx-music-ztools/vendor/lx/src/main/tsconfig.json new file mode 100644 index 000000000..7652af4c6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "typeRoots": [ + "./types" + ], + "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ + "@common/*": ["../common/*"], + "@main/*": ["../main/*"], + "@static/*": ["../static/*"], + "@/*": ["../*"], + }, + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/types/app.d.ts b/plugins/lx-music-ztools/vendor/lx/src/main/types/app.d.ts new file mode 100644 index 000000000..2604a57e0 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/types/app.d.ts @@ -0,0 +1,54 @@ +/* eslint-disable no-var */ +// import { Event as WinMainEvent } from '@main/modules/winMain/event' +// import { Event as WinLyricEvent } from '@main/modules/winLyric/event' +import { type DislikeType, type AppType, type ListType } from '@main/event' +import { type DBSeriveTypes } from '@main/worker/utils' + +interface Lx { + inited: boolean + appSetting: LX.AppSetting + hotKey: { + enable: boolean + config: LX.HotKeyConfigAll + state: LX.HotKeyState + } + /** + * 是否跳过托盘退出 + */ + isSkipTrayQuit: boolean + /** + * main window 是否关闭 + */ + // mainWindowClosed: boolean + event_app: AppType + event_list: ListType + event_dislike: DislikeType + worker: { + dbService: DBSeriveTypes + } + theme: LX.ThemeSetting + player_status: LX.Player.Status +} + +declare global { + // declare module NodeJS { + // export interface Global { + // lx: { + // app_event: { + // winMain: WinMainEvent + // winLyric: WinLyricEvent + // } + // } + // } + // } + + // var isDev: boolean + var envParams: LX.EnvParams + var staticPath: string + var lxDataPath: string + var lxOldDataPath: string + var lx: Lx + var appWorder: AppWorder +} + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/types/common.d.ts b/plugins/lx-music-ztools/vendor/lx/src/main/types/common.d.ts new file mode 100644 index 000000000..378650ee3 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/types/common.d.ts @@ -0,0 +1,17 @@ +import '@common/types/utils' +import '@common/types/app_setting' +import '@common/types/common' +import '@common/types/user_api' +import '@common/types/sync' +import '@common/types/list' +import '@common/types/list_sync' +import '@common/types/download_list' +import '@common/types/music' +import '@common/types/player' +import '@common/types/desktop_lyric' +import '@common/types/theme' +import '@common/types/ipc_main' +import '@common/types/sound_effect' +import '@common/types/dislike_list' +import '@common/types/dislike_list_sync' +import '@common/types/open_api' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/types/db_service.d.ts b/plugins/lx-music-ztools/vendor/lx/src/main/types/db_service.d.ts new file mode 100644 index 000000000..261f8d7cf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/types/db_service.d.ts @@ -0,0 +1,89 @@ +declare namespace LX { + namespace DBService { + + interface MusicInfo { + id: string + listId: string + name: string + singer: string + interval: string | null + source: LX.Music.MusicInfo['source'] + meta: string + order: number + } + + interface MusicInfoOrder { + listId: string + musicInfoId: string + order: number + } + + interface MusicInfoQuery { + listId: string + } + + interface MusicInfoRemove { + listId: string + id: string + } + + interface ListMusicInfoQuery { + listId: string + musicInfoId: string + } + + interface UserListInfo { + id: string + name: string + source?: LX.OnlineSource + sourceListId?: string + position: number + locationUpdateTime: number | null + } + + type Lyricnfo = { + id: string + type: 'lyric' + text: string + source: 'raw' | 'edited' + } | { + id: string + type: keyof Omit + text: string | null + source: 'raw' | 'edited' + } + + interface MusicUrlInfo { + id: string + url: string + } + + interface DownloadMusicInfo { + id: string + isComplate: 0 | 1 + status: LX.Download.DownloadTaskStatus + statusText: string + progress_downloaded: number + progress_total: number + url: string | null + quality: LX.Quality + ext: LX.Download.FileExt + fileName: string + filePath: string + musicInfo: string + position: number + } + + interface DislikeInfo { + // type: 'music' + content: string + // meta: string | null + } + + interface MusicInfoOtherSource extends Omit { + source_id: string + order: number + } + + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/types/global.d.ts b/plugins/lx-music-ztools/vendor/lx/src/main/types/global.d.ts new file mode 100644 index 000000000..ad5420672 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/types/global.d.ts @@ -0,0 +1,14 @@ + + +// // declare module NodeJS { +// // interface Global { +// // isDev: boolean +// // envParams: LX.EnvParams +// // staticPath: string +// // lx: Lx +// // } +// // } + +declare const webpackStaticPath: string +declare const webpackUserApiPath: string + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/types/sync.d.ts b/plugins/lx-music-ztools/vendor/lx/src/main/types/sync.d.ts new file mode 100644 index 000000000..25879c525 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/types/sync.d.ts @@ -0,0 +1,68 @@ +import type WS from 'ws' + +type DefaultEventsMap = Record void> + + +declare global { + namespace LX { + namespace Sync { + namespace Client { + interface Socket extends WS.WebSocket { + isReady: boolean + data: { + keyInfo: ClientKeyInfo + urlInfo: UrlInfo + } + moduleReadys: { + list: boolean + dislike: boolean + } + + onClose: (handler: (err: Error) => (void | Promise)) => () => void + remote: LX.Sync.ServerSyncActions + remoteQueueList: LX.Sync.ServerSyncListActions + remoteQueueDislike: LX.Sync.ServerSyncDislikeActions + } + + interface UrlInfo { + wsProtocol: string + httpProtocol: string + hostPath: string + href: string + } + } + namespace Server { + interface Socket extends WS.WebSocket { + isAlive?: boolean + isReady: boolean + userInfo: { name: 'default' } + keyInfo: ServerKeyInfo + feature: LX.Sync.EnabledFeatures + moduleReadys: { + list: boolean + dislike: boolean + } + + onClose: (handler: (err: Error) => (void | Promise)) => () => void + broadcast: (handler: (client: Socket) => void) => void + + remote: LX.Sync.ClientSyncActions + remoteQueueList: LX.Sync.ClientSyncListActions + remoteQueueDislike: LX.Sync.ClientSyncDislikeActions + } + type SocketServer = WS.Server + } + } + } + + // interface SyncListActionData_none { + // action: 'finished' + // } + // interface SyncListActionData_getData { + // action: 'getData' + // data: 'all' + // } + + // type SyncListActionData = SyncListActionData_none | SyncListActionData_getData +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/types/sync_common.d.ts b/plugins/lx-music-ztools/vendor/lx/src/main/types/sync_common.d.ts new file mode 100644 index 000000000..d6916e246 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/types/sync_common.d.ts @@ -0,0 +1,50 @@ +type WarpSyncHandlerActions = { + [K in keyof Actions]: (...args: [Socket, ...Parameters]) => ReturnType +} + +declare namespace LX { + namespace Sync { + type ServerSyncActions = WarpPromiseRecord<{ + onFeatureChanged: (feature: EnabledFeatures) => void + }> + type ServerSyncHandlerActions = WarpSyncHandlerActions + + type ServerSyncListActions = WarpPromiseRecord<{ + onListSyncAction: (action: LX.Sync.List.ActionList) => void + }> + type ServerSyncHandlerListActions = WarpSyncHandlerActions + + type ServerSyncDislikeActions = WarpPromiseRecord<{ + onDislikeSyncAction: (action: LX.Sync.Dislike.ActionList) => void + }> + type ServerSyncHandlerDislikeActions = WarpSyncHandlerActions + + type ClientSyncActions = WarpPromiseRecord<{ + getEnabledFeatures: (serverType: ServerType, supportedFeatures: SupportedFeatures) => EnabledFeatures + finished: () => void + }> + type ClientSyncHandlerActions = WarpSyncHandlerActions + + type ClientSyncListActions = WarpPromiseRecord<{ + onListSyncAction: (action: LX.Sync.List.ActionList) => void + list_sync_get_md5: () => string + list_sync_get_sync_mode: () => LX.Sync.List.SyncMode + list_sync_get_list_data: () => LX.Sync.List.ListData + list_sync_set_list_data: (data: LX.Sync.List.ListData) => void + list_sync_finished: () => void + }> + type ClientSyncHandlerListActions = WarpSyncHandlerActions + + type ClientSyncDislikeActions = WarpPromiseRecord<{ + onDislikeSyncAction: (action: LX.Sync.Dislike.ActionList) => void + dislike_sync_get_md5: () => string + dislike_sync_get_sync_mode: () => LX.Sync.Dislike.SyncMode + dislike_sync_get_list_data: () => LX.Dislike.DislikeRules + dislike_sync_set_list_data: (data: LX.Dislike.DislikeRules) => void + dislike_sync_finished: () => void + }> + type ClientSyncHandlerDislikeActions = WarpSyncHandlerActions + } +} + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/types/worker.d.ts b/plugins/lx-music-ztools/vendor/lx/src/main/types/worker.d.ts new file mode 100644 index 000000000..f3f19f288 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/types/worker.d.ts @@ -0,0 +1,10 @@ +import { type workerDBSeriveTypes } from '@main/worker/dbService' + +declare global { + // interface WorkerDBSeriveTypes { + // list: typeof list + // } + namespace LX { + type WorkerDBSeriveListTypes = workerDBSeriveTypes + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/utils/fontManage.ts b/plugins/lx-music-ztools/vendor/lx/src/main/utils/fontManage.ts new file mode 100644 index 000000000..e8e84551a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/utils/fontManage.ts @@ -0,0 +1,21 @@ +// const { getAvailableFontFamilies } = require('electron-font-manager') + + +// exports.getAvailableFontFamilies = getAvailableFontFamilies + +import { getFonts } from 'font-list' +// import { getAvailableFontFamilies } from 'electron-font-manager' + + +// const getFonts = async() => { +// switch (process.platform) { +// case 'win32': +// case 'darwin': +// return getAvailableFontFamilies() +// default: return getFontsByCommand() +// } +// } + +export { + getFonts, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/utils/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/utils/index.ts new file mode 100644 index 000000000..71670b9c3 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/utils/index.ts @@ -0,0 +1,330 @@ +import { encodePath, isUrl, throttle, isMac } from '@common/utils' +import migrateSetting from '@common/utils/migrateSetting' +import getStore from '@main/utils/store' +import { STORE_NAMES, URL_SCHEME_RXP } from '@common/constants' +import defaultSetting from '@common/defaultSetting' +import defaultHotKey from '@common/defaultHotKey' +import { migrateDataJson, migrateHotKey, migrateUserApi, parseDataFile } from './migrate' +import { nativeTheme, powerSaveBlocker } from 'electron' +import { joinPath } from '@common/utils/nodejs' +import themes from '@common/theme/index.json' + +export const parseEnvParams = (argv = process.argv): { cmdParams: LX.CmdParams, deeplink: string | null } => { + const cmdParams: LX.CmdParams = {} + let deeplink = null + const rx = /^-\w+/ + for (let param of argv) { + if (URL_SCHEME_RXP.test(param)) { + deeplink = param + } + + if (!rx.test(param)) continue + param = param.substring(1) + let index = param.indexOf('=') + if (index < 0) { + cmdParams[param] = true + } else { + cmdParams[param.substring(0, index)] = param.substring(index + 1) + } + } + return { + cmdParams, + deeplink, + } +} + +const primitiveType = ['string', 'boolean', 'number'] +const checkPrimitiveType = (val: any): boolean => val === null || primitiveType.includes(typeof val) +// const handleMergeSetting = (defaultSetting: LX.AppSetting, currentSetting: Partial) => { +// const updatedSettingKeys: Array = [] +// for (const key of Object.keys(defaultSetting) as Array) { +// const currentValue: any = currentSetting[key] +// const isPrimitive = checkPrimitiveType(currentValue) +// // if (checkPrimitiveType(value)) { +// if (!isPrimitive) continue +// updatedSettingKeys.push(key) +// // @ts-expect-error +// defaultSetting[key] = currentValue +// // } else { +// // if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue) +// // } +// } +// return { +// setting: defaultSetting, +// updatedSettingKeys, +// } +// } + +export const mergeSetting = (originSetting: LX.AppSetting, targetSetting?: Partial | null): { + setting: LX.AppSetting + updatedSettingKeys: Array + updatedSetting: Partial +} => { + let originSettingCopy: LX.AppSetting = { ...originSetting } + // const defaultVersion = targetSettingCopy.version + const updatedSettingKeys: Array = [] + const updatedSetting: Partial = {} + + if (targetSetting) { + const originSettingKeys = Object.keys(originSettingCopy) + const targetSettingKeys = Object.keys(targetSetting) + + if (originSettingKeys.length > targetSettingKeys.length) { + for (const key of targetSettingKeys as Array) { + const targetValue: any = targetSetting[key] + const isPrimitive = checkPrimitiveType(targetValue) + // if (checkPrimitiveType(value)) { + if (!isPrimitive || targetValue == originSettingCopy[key] || originSettingCopy[key] === undefined) continue + updatedSettingKeys.push(key) + updatedSetting[key] = targetValue + // @ts-expect-error + originSettingCopy[key] = targetValue + // } else { + // if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue) + // } + } + } else { + for (const key of originSettingKeys as Array) { + const targetValue: any = targetSetting[key] + const isPrimitive = checkPrimitiveType(targetValue) + // if (checkPrimitiveType(value)) { + if (!isPrimitive || targetValue == originSettingCopy[key]) continue + updatedSettingKeys.push(key) + updatedSetting[key] = targetValue + // @ts-expect-error + originSettingCopy[key] = targetValue + // } else { + // if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue) + // } + } + } + } + + return { + setting: originSettingCopy, + updatedSettingKeys, + updatedSetting, + } +} + +const applyInitSetting = (setting: LX.AppSetting) => { + if (global.envParams.cmdParams.hidden && !setting['tray.enable']) { + setting['tray.enable'] = true + } +} + +export const updateSetting = (setting?: Partial, isInit: boolean = false) => { + const electronStore_config = getStore(STORE_NAMES.APP_SETTINGS) + + let originSetting: LX.AppSetting + if (isInit) { + setting &&= migrateSetting(setting) + applyInitSetting(setting as LX.AppSetting) + originSetting = { ...defaultSetting } + } else originSetting = global.lx.appSetting + + const result = mergeSetting(originSetting, setting) + + result.setting.version = defaultSetting.version + + electronStore_config.override({ version: result.setting.version, setting: result.setting }) + return result +} + +/** + * 初始化设置 + */ +export const initSetting = async() => { + const electronStore_config = getStore(STORE_NAMES.APP_SETTINGS) + + let setting = electronStore_config.get('setting') as LX.AppSetting | undefined + + // migrate setting + if (!setting) { + const config = await parseDataFile<{ setting?: any }>('config.json') + if (config?.setting) setting = config.setting as LX.AppSetting + await migrateUserApi() + await migrateDataJson() + } + + // console.log(setting) + return updateSetting(setting, true) +} + +/** + * 初始化快捷键设置 + */ +export const initHotKey = async() => { + const electronStore_hotKey = getStore(STORE_NAMES.HOTKEY) + + let localConfig = electronStore_hotKey.get('local') as LX.HotKeyConfig | null + let globalConfig = electronStore_hotKey.get('global') as LX.HotKeyConfig | null + + if (globalConfig) { + // 移除v2.2.0及之前设置的全局媒体快捷键注册 + if (globalConfig.keys.MediaPlayPause) { + delete globalConfig.keys.MediaPlayPause + delete globalConfig.keys.MediaNextTrack + delete globalConfig.keys.MediaPreviousTrack + electronStore_hotKey.set('global', globalConfig) + } + } else { + // migrate hotKey + const config = await migrateHotKey() + if (config) { + localConfig = config.local + globalConfig = config.global + } else { + localConfig = JSON.parse(JSON.stringify(defaultHotKey.local)) + globalConfig = JSON.parse(JSON.stringify(defaultHotKey.global)) + } + + electronStore_hotKey.set('local', localConfig) + electronStore_hotKey.set('global', globalConfig) + } + + return { + local: localConfig!, + global: globalConfig!, + } +} + +type HotKeyType = 'local' | 'global' + +const saveHotKeyConfig = throttle<[LX.HotKeyConfigAll]>((config: LX.HotKeyConfigAll) => { + for (const key of Object.keys(config) as HotKeyType[]) { + global.lx.hotKey.config[key] = config[key] + getStore(STORE_NAMES.HOTKEY).set(key, config[key]) + } +}) +export const saveAppHotKeyConfig = (config: LX.HotKeyConfigAll) => { + saveHotKeyConfig(config) +} + +export const openDevTools = (webContents: Electron.WebContents) => { + webContents.openDevTools({ + mode: 'undocked', + }) +} + + +let userThemes: LX.Theme[] +export const getAllThemes = () => { + userThemes ??= getStore(STORE_NAMES.THEME).get('themes') as (LX.Theme[] | null) ?? [] + return { + themes, + userThemes, + dataPath: joinPath(global.lxDataPath, 'theme_images'), + } +} + +export const saveTheme = (theme: LX.Theme) => { + const targetTheme = userThemes.find(t => t.id === theme.id) + if (targetTheme) Object.assign(targetTheme, theme) + else userThemes.push(theme) + getStore(STORE_NAMES.THEME).set('themes', userThemes) +} + +export const removeTheme = (id: string) => { + const index = userThemes.findIndex(t => t.id === id) + if (index < 0) return + userThemes.splice(index, 1) + getStore(STORE_NAMES.THEME).set('themes', userThemes) +} + +const copyTheme = (theme: LX.Theme): LX.Theme => { + return { + ...theme, + config: { + ...theme.config, + extInfo: { ...theme.config.extInfo }, + themeColors: { ...theme.config.themeColors }, + }, + } +} +export const getTheme = () => { + // fs.promises.readdir() + const shouldUseDarkColors = nativeTheme.shouldUseDarkColors + let themeId = global.lx.appSetting['theme.id'] == 'auto' + ? shouldUseDarkColors + ? global.lx.appSetting['theme.darkId'] + : global.lx.appSetting['theme.lightId'] + : global.lx.appSetting['theme.id'] + // themeId = 'naruto' + // themeId = 'pink' + // themeId = 'black' + let theme = themes.find(theme => theme.id == themeId) + if (!theme) { + userThemes = getStore(STORE_NAMES.THEME).get('themes') as LX.Theme[] | null ?? [] + theme = userThemes.find(theme => theme.id == themeId) + if (theme) { + if (theme.config.extInfo['--background-image'] != 'none') { + theme = copyTheme(theme) + theme.config.extInfo['--background-image'] = + isUrl(theme.config.extInfo['--background-image']) + ? `url(${theme.config.extInfo['--background-image']})` + : `url(${encodePath(joinPath(global.lxDataPath, 'theme_images', theme.config.extInfo['--background-image']))})` + } + } else { + themeId = global.lx.appSetting['theme.id'] == 'auto' && shouldUseDarkColors ? 'black' : 'green' + theme = themes.find(theme => theme.id == themeId) as LX.Theme + } + } + + const colors: Record = { + ...theme.config.themeColors, + ...theme.config.extInfo, + } + + return { + shouldUseDarkColors, + theme: { + id: global.lx.appSetting['theme.id'], + name: theme.name, + isDark: theme.isDark, + isDarkFont: theme.isDarkFont, + colors, + }, + } +} + +let powerSaveBlockerId: number | null = null +export const setPowerSaveBlocker = (enabled: boolean) => { + let isEnabled = powerSaveBlockerId != null && powerSaveBlocker.isStarted(powerSaveBlockerId) + if (enabled) { + if (isEnabled) return + powerSaveBlockerId = powerSaveBlocker.start(isMac ? 'prevent-display-sleep' : 'prevent-app-suspension') + } else { + if (!isEnabled) return + powerSaveBlocker.stop(powerSaveBlockerId!) + powerSaveBlockerId = null + } +} + + +let envProxy: null | { host: string, port: number } = null +export const getProxy = () => { + if (global.lx.appSetting['network.proxy.enable'] && global.lx.appSetting['network.proxy.host']) { + return { + host: global.lx.appSetting['network.proxy.host'], + port: parseInt(global.lx.appSetting['network.proxy.port'] || '80'), + } + } + if (envProxy) { + return { + host: envProxy.host, + port: envProxy.port, + } + } else { + const envProxyStr = envParams.cmdParams['proxy-server'] + if (envProxyStr && typeof envProxyStr == 'string') { + const [host, port = ''] = envProxyStr.split(':') + return envProxy = { + host, + port: parseInt(port || '80'), + } + } + } + + return null +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/utils/logInit.ts b/plugins/lx-music-ztools/vendor/lx/src/main/utils/logInit.ts new file mode 100644 index 000000000..c4d8138b7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/utils/logInit.ts @@ -0,0 +1,4 @@ +import log from 'electron-log/node' + +log.transports.file.level = 'info' +// log.initialize() diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/utils/migrate.ts b/plugins/lx-music-ztools/vendor/lx/src/main/utils/migrate.ts new file mode 100644 index 000000000..d804f31fa --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/utils/migrate.ts @@ -0,0 +1,161 @@ +import fs from 'node:fs' +import { checkPath, joinPath } from '@common/utils/nodejs' +import { log } from '@common/utils' +import { filterMusicList, toNewMusicInfo } from '@common/utils/tools' +import { APP_EVENT_NAMES, STORE_NAMES } from '@common/constants' + +/** + * 读取配置文件 + * @returns + */ +export const parseDataFile = async(name: string): Promise => { + const path = joinPath(global.lxOldDataPath, name) + if (await checkPath(path)) { + try { + return JSON.parse((await fs.promises.readFile(path)).toString()) + } catch (err) { + log.error(err) + } + } + return null +} + +interface OldUserListInfo { + name: string + id: string + source?: LX.OnlineSource + sourceListId?: string + locationUpdateTime?: number + list: any[] +} + +/** + * 迁移 v2.0.0 之前的 list data + * @returns + */ +export const migrateDBData = async() => { + let playList = await parseDataFile<{ defaultList?: { list: any[] }, loveList?: { list: any[] }, tempList?: { list: any[] }, userList?: OldUserListInfo[] }>('playList.json') + let listDataAll: LX.List.ListDataFull = { + defaultList: [], + loveList: [], + userList: [], + tempList: [], + } + let isRequiredSave = false + if (playList) { + if (playList.defaultList) listDataAll.defaultList = filterMusicList(playList.defaultList.list.map(m => toNewMusicInfo(m))) + if (playList.loveList) listDataAll.loveList = filterMusicList(playList.loveList.list.map(m => toNewMusicInfo(m))) + if (playList.tempList) listDataAll.tempList = filterMusicList(playList.tempList.list.map(m => toNewMusicInfo(m))) + if (playList.userList) { + listDataAll.userList = playList.userList.map(l => { + return { + ...l, + locationUpdateTime: l.locationUpdateTime ?? null, + list: filterMusicList(l.list.map(m => toNewMusicInfo(m))), + } + }) + } + isRequiredSave = true + } else { + const config = await parseDataFile<{ list?: { defaultList?: any[], loveList?: any[] } }>('config.json') + if (config?.list) { + const list = config.list + if (list.defaultList) listDataAll.defaultList = filterMusicList(list.defaultList.map(m => toNewMusicInfo(m))) + if (list.loveList) listDataAll.loveList = filterMusicList(list.loveList.map(m => toNewMusicInfo(m))) + isRequiredSave = true + } + } + if (isRequiredSave) await global.lx.worker.dbService.listDataOverwrite(listDataAll) + + const lyricData = await parseDataFile>('lyrics_edited.json') + if (lyricData) { + for await (const [id, info] of Object.entries(lyricData)) { + await global.lx.worker.dbService.editedLyricAdd(id, info) + } + } +} + +// 迁移文件 +const migrateFile = async(name: string, targetName: string) => { + let path = joinPath(global.lxDataPath, targetName) + let oldPath = joinPath(global.lxOldDataPath, name) + if (!await checkPath(path) && await checkPath(oldPath)) { + await fs.promises.copyFile(oldPath, path).catch(err => { + log.error(err) + }).catch(err => { + log.error(err) + }) + } +} + +/** + * 迁移 v2.0.0 之前的 data.json + * @returns + */ +export const migrateDataJson = async() => { + const path = joinPath(global.lxDataPath, 'data.json') + if (await checkPath(path)) return + const oldDataFile = await parseDataFile<{ + searchHistoryList?: string[] + playInfo?: any + listPrevSelectId?: any + listPosition?: any + listUpdateInfo?: any + }>('data.json') + if (!oldDataFile) return + const newData: any = {} + if (oldDataFile.searchHistoryList) newData.searchHistoryList = oldDataFile.searchHistoryList + if (oldDataFile.playInfo) newData.playInfo = oldDataFile.playInfo + if (oldDataFile.listPrevSelectId) newData.listPrevSelectId = oldDataFile.listPrevSelectId + if (oldDataFile.listPosition) newData.listScrollPosition = oldDataFile.listPosition + if (oldDataFile.listUpdateInfo) newData.listUpdateInfo = oldDataFile.listUpdateInfo + + await fs.promises.writeFile(path, JSON.stringify(newData)).catch(err => { + log.error(err) + }) +} + + +const hotKeyNameMap = { + mainWindow: APP_EVENT_NAMES.winMainName, + winLyric: APP_EVENT_NAMES.winLyricName, +} as const +const updateHotKeyTypeName = (config: LX.HotKeyConfig) => { + for (const keyConfig of Object.values(config.keys)) { + if (hotKeyNameMap[keyConfig.type as keyof typeof hotKeyNameMap]) keyConfig.type = hotKeyNameMap[keyConfig.type as keyof typeof hotKeyNameMap] + } +} +/** + * 迁移 v2.0.0 之前的 hotkey + * @returns + */ +export const migrateHotKey = async() => { + const oldConfig = await parseDataFile('hotKey.json') + if (oldConfig) { + let localConfig: LX.HotKeyConfig + let globalConfig: LX.HotKeyConfig + updateHotKeyTypeName(oldConfig.local) + updateHotKeyTypeName(oldConfig.global) + + localConfig = oldConfig.local + globalConfig = oldConfig.global + + // 移除v1.0.1及之前设置的全局声音媒体快捷键接管 + if (globalConfig.keys.VolumeUp) { + delete globalConfig.keys.VolumeUp + delete globalConfig.keys.VolumeDown + delete globalConfig.keys.VolumeMute + } + return { + local: localConfig, + global: globalConfig, + } + } + return null +} + +/** + * 迁移 v2.0.0 之前的user api + * @returns + */ +export const migrateUserApi = async() => migrateFile('userApi.json', STORE_NAMES.USER_API + '.json') diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/utils/request.ts b/plugins/lx-music-ztools/vendor/lx/src/main/utils/request.ts new file mode 100644 index 000000000..956feb4a1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/utils/request.ts @@ -0,0 +1,62 @@ +// import progress from 'request-progress' +import { request, type Options } from '@common/utils/request' +// import fs from 'fs' + +export const requestMsg = { + fail: '请求异常😮,可以多试几次,若还是不行就换一首吧。。。', + unachievable: '哦No😱...接口无法访问了!', + timeout: '请求超时', + // unachievable: '哦No😱...接口无法访问了!已帮你切换到临时接口,重试下看能不能播放吧~', + notConnectNetwork: '无法连接到服务器', + cancelRequest: '取消http请求', +} as const + +// var proxyUrl = "http://" + user + ":" + password + "@" + host + ":" + port; +// var proxiedRequest = request.defaults({'proxy': proxyUrl}); + +// interface RequestPromise extends Promise { +// abort: () => void +// } + +/** + * 请求超时自动重试 + * @param {*} url + * @param {*} options + */ +export const httpFetch = async (url: string, options: Options) => { + return request(url, options).catch(async(err: any) => { + // console.log('出错', err) + if (err.message === 'socket hang up') { + // window.globalObj.apiSource = 'temp' + throw new Error(requestMsg.unachievable) + } + switch (err.code) { + case 'ETIMEDOUT': + case 'ESOCKETTIMEDOUT': + throw new Error(requestMsg.timeout) + case 'ENOTFOUND': + throw new Error(requestMsg.notConnectNetwork) + default: + throw err + } + }) + // requestObj.promise = requestObj.promise.catch(async err => { + // // console.log('出错', err) + // if (err.message === 'socket hang up') { + // // window.globalObj.apiSource = 'temp' + // return Promise.reject(new Error(requestMsg.unachievable)) + // } + // switch (err.code) { + // case 'ETIMEDOUT': + // case 'ESOCKETTIMEDOUT': + // return Promise.reject(new Error(requestMsg.timeout)) + // case 'ENOTFOUND': + // return Promise.reject(new Error(requestMsg.notConnectNetwork)) + // default: + // return Promise.reject(err) + // } + // }) + // return requestPromise +} + +export type RequestOptions = Options diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/utils/store.ts b/plugins/lx-music-ztools/vendor/lx/src/main/utils/store.ts new file mode 100644 index 000000000..6941b6d1e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/utils/store.ts @@ -0,0 +1,110 @@ +// import { writeFileSync } from 'atomically' +import { dialog, shell } from 'electron' +import path from 'node:path' +import fs from 'node:fs' +import { log } from '@common/utils' + +type Stores = Record + +const stores: Stores = {} + + +class Store { + private readonly filePath: string + private readonly dirPath: string + private store: Record + + private writeFile() { + const tempPath = this.filePath + '.' + Math.random().toString().substring(2, 10) + '.temp' + try { + fs.writeFileSync(tempPath, JSON.stringify(this.store, null, '\t'), 'utf8') + } catch (err: any) { + if (err.code === 'ENOENT') { + fs.mkdirSync(this.dirPath, { recursive: true }) + fs.writeFileSync(tempPath, JSON.stringify(this.store, null, '\t'), 'utf8') + } else throw err + } + fs.renameSync(tempPath, this.filePath) + } + + constructor(filePath: string, clearInvalidConfig: boolean = false) { + this.filePath = filePath + this.dirPath = path.dirname(this.filePath) + + let store: Record + if (fs.existsSync(this.filePath)) { + if (clearInvalidConfig) { + try { + store = JSON.parse(fs.readFileSync(this.filePath, 'utf8')) + } catch { + store = {} + } + } else store = JSON.parse(fs.readFileSync(this.filePath, 'utf8')) + } else store = {} + + if (typeof store != 'object') { + if (clearInvalidConfig) store = {} + else throw new Error('parse data error: ' + String(store)) + } + this.store = store + } + + get(key: string): Value { + return this.store[key] + } + + has(key: string): boolean { + return key in this.store + } + + set(key: string, value: any) { + this.store[key] = value + this.writeFile() + } + + override(value: Record) { + this.store = value + this.writeFile() + } +} + +/** + * 获取 Store 对象 + * @param name store 名 + * @param isIgnoredError 是否忽略错误 + * @param isShowErrorAlert=true 是否显示错误弹窗 + * @returns Store + */ +export default (name: string, isIgnoredError = true, isShowErrorAlert = true): Store => { + if (stores[name]) return stores[name] + let store: Store + const storePath = path.join(global.lxDataPath, name + '.json') + try { + store = stores[name] = new Store(storePath, false) + } catch (err: any) { + const error = err as Error + log.error(error) + + if (!isIgnoredError) throw error + + + const backPath = storePath + '.bak' + fs.renameSync(storePath, 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(storePath, true) + } + return store +} + +export { + Store, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/utils/winLegacy.ts b/plugins/lx-music-ztools/vendor/lx/src/main/utils/winLegacy.ts new file mode 100644 index 000000000..52b35d4a8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/utils/winLegacy.ts @@ -0,0 +1,44 @@ +import os from 'node:os' + +import { dialog } from 'electron' +import { STORE_NAMES } from '@common/constants' +import getStore from '@main/utils/store' +import { openUrl } from '@common/utils/electron' + + +const showWinLegacyMessage = () => { + if (process.platform !== 'win32') return + const storeKey = 'winLegacyMessageShown' + const dataStore = getStore(STORE_NAMES.DATA) + const count = dataStore.get(storeKey) || 0 + if (count > 2) return + const osVersion = os.release() + const majorVersion = parseInt(osVersion.split('.')[0], 10) + const isOldWindows = majorVersion < 10 + + if (isOldWindows) { + const result = dialog.showMessageBoxSync({ + type: 'warning', + title: 'Windows 旧版提示', + message: '您正在使用专为旧版 Windows 准备的 LX Music,它仅用于实在无法升级到新版系统的用户。\n该版本缺乏安全更新,可能存在安全风险,并且功能未经测试,可能存在兼容性问题。\n\n如果您的设备支持 Windows 10 或更高版本,强烈建议升级系统,切换到标准版本的 LX Music,以获得更好的安全性和性能。', + buttons: ['知道了'], + }) + if (result === 0) { + dataStore.set(storeKey, count + 1) + } + } else { + const result = dialog.showMessageBoxSync({ + type: 'warning', + title: '您正在使用旧版', + message: '您正在使用专为旧版 Windows 准备的 LX Music,它仅用于实在无法升级到新版系统的用户,但您的系统似乎在标准版的支持范围内,为了获得最佳体验,请切换到 LX Music 的标准版本。\n\n注意:当前版本缺乏安全更新,可能存在安全风险,并且功能未经测试,可能存在兼容性问题。', + buttons: ['跳转新版发布页', '知道了'], + }) + if (result === 0) { + void openUrl('https://github.com/lyswhut/lx-music-desktop/releases') + } else if (result === 1) { + dataStore.set('winLegacyMessageShown', count + 1) + } + } +} + +showWinLegacyMessage() diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/db.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/db.ts new file mode 100644 index 000000000..e13fda275 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/db.ts @@ -0,0 +1,60 @@ +import Database from 'better-sqlite3' +import path from 'path' +import tables, { DB_VERSION } from './tables' +import verifyDB from './verifyDB' +import migrateData from './migrate' + +let db: Database.Database + + +const initTables = (db: Database.Database) => { + db.exec(` + ${Array.from(tables.values()).join('\n')} + INSERT INTO "main"."db_info" ("field_name", "field_value") VALUES ('version', '${DB_VERSION}'); + `) +} + + +// 打开、初始化数据库 +export const init = (lxDataPath: string): boolean | null => { + const databasePath = path.join(lxDataPath, 'lx.data.db') + const nativeBinding = path.join(__dirname, '../node_modules/better-sqlite3/build/Release/better_sqlite3.node') + let dbFileExists = true + + try { + db = new Database(databasePath, { + fileMustExist: true, + nativeBinding, + // verbose: process.env.NODE_ENV !== 'production' ? console.log : undefined, + }) + } catch (error) { + console.log(error) + db = new Database(databasePath, { + nativeBinding, + // verbose: process.env.NODE_ENV !== 'production' ? console.log : undefined, + }) + initTables(db) + dbFileExists = false + } + db.pragma('journal_mode = WAL') + + if (dbFileExists) migrateData(db) + + // https://www.sqlite.org/pragma.html#pragma_optimize + if (dbFileExists) db.exec('PRAGMA optimize;') + if (!verifyDB(db)) { + db.close() + return null + } + + // https://www.sqlite.org/lang_vacuum.html + // db.exec('VACUUM "main"') + + process.on('exit', () => db.close()) + console.log('db inited') + // require('./test') + return dbFileExists +} + +// 获取数据库实例 +export const getDB = (): Database.Database => db diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/index.ts new file mode 100644 index 000000000..9d58589a4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/index.ts @@ -0,0 +1,18 @@ +import { init } from './db' +import { exposeWorker } from '../utils/worker' +import { list, lyric, music_url, music_other_source, download, dislike_list } from './modules/index' + + +const common = { + init, +} + +exposeWorker(Object.assign(common, list, lyric, music_url, music_other_source, download, dislike_list)) + +export type workerDBSeriveTypes = typeof common + & typeof list + & typeof lyric + & typeof music_url + & typeof music_other_source + & typeof download + & typeof dislike_list diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/migrate.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/migrate.ts new file mode 100644 index 000000000..ef1aa2267 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/migrate.ts @@ -0,0 +1,49 @@ +import type Database from 'better-sqlite3' +import tables, { DB_VERSION } from './tables' + +// const migrateV1 = (db: Database.Database) => { +// const sql = ` +// DROP TABLE "main"."download_list"; + +// CREATE TABLE "download_list" ( +// "id" TEXT NOT NULL, +// "isComplate" INTEGER NOT NULL, +// "status" TEXT NOT NULL, +// "statusText" TEXT NOT NULL, +// "progress_downloaded" INTEGER NOT NULL, +// "progress_total" INTEGER NOT NULL, +// "url" TEXT, +// "quality" TEXT NOT NULL, +// "ext" TEXT NOT NULL, +// "fileName" TEXT NOT NULL, +// "filePath" TEXT NOT NULL, +// "musicInfo" TEXT NOT NULL, +// "position" INTEGER NOT NULL, +// PRIMARY KEY("id") +// ); +// ` +// db.exec(sql) +// db.prepare('UPDATE "main"."db_info" SET "field_value"=@value WHERE "field_name"=@name').run({ name: 'version', value: '2' }) +// } + +const migrateV1 = (db: Database.Database) => { + // 修复 v2.4.0 的默认数据库版本号不对的问题 + const existsTable = db.prepare('SELECT name FROM "main".sqlite_master WHERE type=\'table\' AND name=\'dislike_list\';').get() + if (!existsTable) { + const sql = tables.get('dislike_list')! + db.exec(sql) + } +} + +export default (db: Database.Database) => { + // PRAGMA user_version = x + // console.log(db.prepare('PRAGMA user_version').get().user_version) + // https://github.com/WiseLibs/better-sqlite3/issues/668#issuecomment-1145285728 + const version = (db.prepare<[string]>('SELECT "field_value" FROM "main"."db_info" WHERE "field_name" = ?').get('version') as { field_value: string }).field_value + switch (version) { + case '1': + migrateV1(db) + db.prepare('UPDATE "main"."db_info" SET "field_value"=@value WHERE "field_name"=@name').run({ name: 'version', value: DB_VERSION }) + break + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/dislike_list/dbHelper.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/dislike_list/dbHelper.ts new file mode 100644 index 000000000..23b7f9512 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/dislike_list/dbHelper.ts @@ -0,0 +1,76 @@ +// import type Database from 'better-sqlite3' +import { getDB } from '../../db' +import { + createQueryStatement, + createInsertStatement, + // createDeleteStatement, + // createUpdateStatement, + createClearStatement, +} from './statements' + +/** + * 查询不喜欢歌曲列表 + */ +export const queryDislikeList = () => { + const queryStatement = createQueryStatement() + return queryStatement.all() as LX.DBService.DislikeInfo[] +} + +/** + * 批量插入不喜欢歌曲并刷新顺序 + * @param infos 列表 + */ +export const insertDislikeList = async(infos: LX.DBService.DislikeInfo[]) => { + const db = getDB() + const insertStatement = createInsertStatement() + db.transaction((infos: LX.DBService.DislikeInfo[]) => { + for (const info of infos) insertStatement.run(info) + })(infos) +} + +/** + * 覆盖并批量插入不喜欢歌曲并刷新顺序 + * @param infos 列表 + */ +export const overwirteDislikeList = async(infos: LX.DBService.DislikeInfo[]) => { + const db = getDB() + const clearStatement = createClearStatement() + const insertStatement = createInsertStatement() + db.transaction((infos: LX.DBService.DislikeInfo[]) => { + clearStatement.run() + for (const info of infos) insertStatement.run(info) + })(infos) +} + +// /** +// * 批量删除不喜欢歌曲 +// * @param ids 列表 +// */ +// export const deleteDislikeList = (ids: string[]) => { +// const db = getDB() +// const deleteStatement = createDeleteStatement() +// db.transaction((ids: string[]) => { +// for (const id of ids) deleteStatement.run(BigInt(id)) +// })(ids) +// } + +// /** +// * 批量更新不喜欢歌曲 +// * @param urlInfo 列表 +// */ +// export const updateDislikeList = async(infos: LX.DBService.DislikeInfo[]) => { +// const db = getDB() +// const updateStatement = createUpdateStatement() +// db.transaction((infos: LX.DBService.DislikeInfo[]) => { +// for (const info of infos) updateStatement.run(info) +// })(infos) +// } + +// /** +// * 清空不喜欢歌曲列表 +// */ +// export const clearDislikeList = () => { +// const clearStatement = createClearStatement() +// clearStatement.run() +// } + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/dislike_list/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/dislike_list/index.ts new file mode 100644 index 000000000..8894b6040 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/dislike_list/index.ts @@ -0,0 +1,100 @@ +import { SPLIT_CHAR } from '@common/constants' +import { + queryDislikeList, + insertDislikeList, + overwirteDislikeList, + // updateDislikeList, + // deleteDislikeList, + // clearDislikeList, +} from './dbHelper' + +// let dislikeInfo: LX.Dislike.DislikeInfo + +const toDBDislikeInfo = (musicInfos: string[]): LX.DBService.DislikeInfo[] => { + const list: LX.DBService.DislikeInfo[] = [] + for (const item of musicInfos) { + if (!item.trim()) continue + list.push({ + content: item, + }) + } + return list +} + +const initDislikeList = () => { + const dislikeInfo: LX.Dislike.DislikeInfo = { + // musicIds: new Set(), + names: new Set(), + singerNames: new Set(), + musicNames: new Set(), + rules: '', + } + const list: string[] = [] + for (const item of queryDislikeList()) { + if (!item) continue + let [name, singer] = item.content.split(SPLIT_CHAR.DISLIKE_NAME) + if (name) { + name = name.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + if (singer) { + singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + const rule = `${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}` + dislikeInfo.names.add(rule) + list.push(rule) + } else { + dislikeInfo.musicNames.add(name) + list.push(name) + } + } else if (singer) { + singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + dislikeInfo.singerNames.add(singer) + list.push(`${SPLIT_CHAR.DISLIKE_NAME}${singer}`) + } + } + + dislikeInfo.rules = Array.from(new Set(list)).join('\n') + + return dislikeInfo +} + +/** + * 获取不喜欢列表信息 + * @returns 不喜欢列表信息 + */ +export const getDislikeListInfo = (): LX.Dislike.DislikeInfo => { + // if (!dislikeInfo) initDislikeList() + return initDislikeList() +} + + +/** + * 添加信息 + * @param lists 列表信息 + */ +export const dislikeInfoAdd = async(lists: LX.Dislike.DislikeMusicInfo[]) => { + await insertDislikeList(lists.map(info => ({ content: `${info.name}${SPLIT_CHAR.DISLIKE_NAME}${info.singer}` }))) +} + +/** + * 覆盖列表信息 + * @param rules 规则信息 + */ +export const dislikeInfoOverwrite = async(rules: string) => { + await overwirteDislikeList(toDBDislikeInfo(rules.split('\n'))) +} + + +// /** +// * 删除不喜欢列表 +// * @param ids 歌曲id +// */ +// export const dislikeInfoRemove = (ids: string[]) => { +// deleteDislikeList(ids) +// } + +// /** +// * 清空不喜欢列表 +// */ +// export const dislikeInfoClear = () => { +// clearDislikeList() +// } + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/dislike_list/statements.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/dislike_list/statements.ts new file mode 100644 index 000000000..7c2924cd4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/dislike_list/statements.ts @@ -0,0 +1,62 @@ +import { getDB } from '../../db' + +/** + * 创建不喜欢列表查询语句 + * @returns 查询语句 + */ +export const createQueryStatement = () => { + const db = getDB() + return db.prepare<[]>(` + SELECT "content" + FROM dislike_list + WHERE "type"='music' + `) +} + +/** + * 创建不喜欢记录插入语句 + * @returns 插入语句 + */ +export const createInsertStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.DislikeInfo]>(` + INSERT INTO "main"."dislike_list" ("type", "content") + VALUES ('music', @content)`) +} + +/** + * 创建不喜欢记录清空语句 + * @returns 清空语句 + */ +export const createClearStatement = () => { + const db = getDB() + return db.prepare<[]>(` + DELETE FROM "main"."dislike_list" + `) +} + +// /** +// * 创建不喜欢记录删除语句 +// * @returns 删除语句 +// */ +// export const createDeleteStatement = () => { +// const db = getDB() +// return db.prepare<[bigint]>(` +// DELETE FROM "main"."dislike_list" +// WHERE "id"=? +// `) +// } + +// /** +// * 创建不喜欢记录更新语句 +// * @returns 更新语句 +// */ +// export const createUpdateStatement = () => { +// const db = getDB() +// return db.prepare<[LX.DBService.DislikeInfo]>(` +// UPDATE "main"."dislike_list" +// SET "name"=@name, "singer"=@singer +// WHERE "id"=@id +// `) +// } + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/download/dbHelper.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/download/dbHelper.ts new file mode 100644 index 000000000..6a6e9940f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/download/dbHelper.ts @@ -0,0 +1,64 @@ +import { getDB } from '../../db' +import { + createQueryStatement, + createInsertStatement, + createDeleteStatement, + createUpdateStatement, + createUpdatePositionStatement, + createClearStatement, +} from './statements' + +/** + * 查询下载歌曲列表 + */ +export const queryDownloadList = () => { + const queryStatement = createQueryStatement() + return queryStatement.all() as LX.DBService.DownloadMusicInfo[] +} + +/** + * 批量插入下载歌曲并刷新顺序 + * @param mInfos 列表 + */ +export const insertDownloadList = (mInfos: LX.DBService.DownloadMusicInfo[], listPositions: Array<{ id: string, position: number }>) => { + const db = getDB() + const insertStatement = createInsertStatement() + const updatePositionStatement = createUpdatePositionStatement() + db.transaction((mInfos: LX.DBService.DownloadMusicInfo[]) => { + for (const info of mInfos) insertStatement.run(info) + for (const info of listPositions) updatePositionStatement.run(info) + })(mInfos) +} + +/** + * 批量删除下载歌曲 + * @param ids 列表 + */ +export const deleteDownloadList = (ids: string[]) => { + const db = getDB() + const deleteStatement = createDeleteStatement() + db.transaction((ids: string[]) => { + for (const id of ids) deleteStatement.run(id) + })(ids) +} + +/** + * 批量更新下载歌曲 + * @param urlInfo 列表 + */ +export const updateDownloadList = (urlInfo: LX.DBService.DownloadMusicInfo[]) => { + const db = getDB() + const updateStatement = createUpdateStatement() + db.transaction((urlInfo: LX.DBService.DownloadMusicInfo[]) => { + for (const info of urlInfo) updateStatement.run(info) + })(urlInfo) +} + +/** + * 清空下载歌曲列表 + */ +export const clearDownloadList = () => { + const clearStatement = createClearStatement() + clearStatement.run() +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/download/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/download/index.ts new file mode 100644 index 000000000..06c5bd364 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/download/index.ts @@ -0,0 +1,119 @@ +import { arrPush, arrUnshift } from '@common/utils/common' +import { + queryDownloadList, + insertDownloadList, + updateDownloadList, + deleteDownloadList, + clearDownloadList, +} from './dbHelper' + +let list: LX.Download.ListItem[] + +const toDBDownloadInfo = (musicInfos: LX.Download.ListItem[], offset: number = 0): LX.DBService.DownloadMusicInfo[] => { + return musicInfos.map((info, index) => { + return { + id: info.id, + isComplate: info.isComplate ? 1 : 0, + status: info.status, + statusText: info.statusText, + progress_downloaded: info.downloaded, + progress_total: info.total, + url: info.metadata.url, + quality: info.metadata.quality, + ext: info.metadata.ext, + fileName: info.metadata.fileName, + filePath: info.metadata.filePath, + musicInfo: JSON.stringify(info.metadata.musicInfo), + position: offset + index, + } + }) +} + +const initDownloadList = () => { + list = queryDownloadList().map(item => { + const musicInfo = JSON.parse(item.musicInfo) as LX.Music.MusicInfoOnline + return { + id: item.id, + isComplate: item.isComplate == 1, + status: item.status, + statusText: item.statusText, + downloaded: item.progress_downloaded, + total: item.progress_total, + progress: item.progress_total ? parseInt((item.progress_downloaded / item.progress_total).toFixed(2)) * 100 : 0, + speed: '', + writeQueue: 0, + metadata: { + musicInfo, + url: item.url, + quality: item.quality, + ext: item.ext, + fileName: item.fileName, + filePath: item.filePath, + }, + } + }) +} + +/** + * 获取下载列表 + * @returns 下载列表 + */ +export const getDownloadList = (): LX.Download.ListItem[] => { + if (!list) initDownloadList() + return list +} + +/** + * 添加下载歌曲信息 + * @param downloadInfos url信息 + */ +export const downloadInfoSave = (downloadInfos: LX.Download.ListItem[], addMusicLocationType: LX.AddMusicLocationType) => { + if (!list) initDownloadList() + if (addMusicLocationType == 'top') { + let newList = [...list] + arrUnshift(newList, downloadInfos) + insertDownloadList(toDBDownloadInfo(downloadInfos), newList.slice(downloadInfos.length - 1).map((info, index) => { + return { id: info.id, position: index } + })) + list = newList + } else { + insertDownloadList(toDBDownloadInfo(downloadInfos, list.length), []) + arrPush(list, downloadInfos) + } +} + +/** + * 批量更新列表信息 + * @param lists 列表信息 + */ +export const downloadInfoUpdate = (lists: LX.Download.ListItem[]) => { + updateDownloadList(toDBDownloadInfo(lists)) + if (list) { + for (const item of lists) { + const index = list.findIndex(info => info.id === item.id) + if (index < 0) continue + list.splice(index, 1, item) + } + } +} + + +/** + * 删除下载列表 + * @param ids 歌曲id + */ +export const downloadInfoRemove = (ids: string[]) => { + deleteDownloadList(ids) + if (list) { + const idSet = new Set(ids) + list = list.filter(task => !idSet.has(task.id)) + } +} + +/** + * 清空下载列表 + */ +export const downloadInfoClear = () => { + clearDownloadList() +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/download/statements.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/download/statements.ts new file mode 100644 index 000000000..80b323676 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/download/statements.ts @@ -0,0 +1,72 @@ +import { getDB } from '../../db' + +/** + * 创建下载列表查询语句 + * @returns 查询语句 + */ +export const createQueryStatement = () => { + const db = getDB() + return db.prepare<[]>(` + SELECT "id", "isComplate", "status", "statusText", "progress_downloaded", "progress_total", "url", "quality", "ext", "fileName", "filePath", "musicInfo", "position" + FROM download_list + ORDER BY "position" ASC + `) +} + +/** + * 创建下载记录插入语句 + * @returns 插入语句 + */ +export const createInsertStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.DownloadMusicInfo]>(` + INSERT INTO "main"."download_list" ("id", "isComplate", "status", "statusText", "progress_downloaded", "progress_total", "url", "quality", "ext", "fileName", "filePath", "musicInfo", "position") + VALUES (@id, @isComplate, @status, @statusText, @progress_downloaded, @progress_total, @url, @quality, @ext, @fileName, @filePath, @musicInfo, @position)`) +} + +/** + * 创建下载记录清空语句 + * @returns 清空语句 + */ +export const createClearStatement = () => { + const db = getDB() + return db.prepare<[]>(` + DELETE FROM "main"."download_list" + `) +} + +/** + * 创建下载记录删除语句 + * @returns 删除语句 + */ +export const createDeleteStatement = () => { + const db = getDB() + return db.prepare<[string]>(` + DELETE FROM "main"."download_list" + WHERE "id"=? + `) +} + +/** + * 创建下载记录更新语句 + * @returns 更新语句 + */ +export const createUpdateStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.DownloadMusicInfo]>(` + UPDATE "main"."download_list" + SET "isComplate"=@isComplate, "status"=@status, "statusText"=@statusText, "progress_downloaded"=@progress_downloaded, "progress_total"=@progress_total, "url"=@url, "filePath"=@filePath + WHERE "id"=@id`) +} + +/** + * 创建下载记录顺序更新语句 + * @returns 更新语句 + */ +export const createUpdatePositionStatement = () => { + const db = getDB() + return db.prepare<[{ id: string, position: number }]>(` + UPDATE "main"."download_list" + SET "position"=@position + WHERE "id"=@id`) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/index.ts new file mode 100644 index 000000000..7d87157d4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/index.ts @@ -0,0 +1,7 @@ + +export * as list from './list' +export * as lyric from './lyric' +export * as music_url from './music_url' +export * as music_other_source from './music_other_source' +export * as download from './download' +export * as dislike_list from './dislike_list' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/list/dbHelper.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/list/dbHelper.ts new file mode 100644 index 000000000..dd6387ed0 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/list/dbHelper.ts @@ -0,0 +1,382 @@ +import { getDB } from '../../db' +import { + createListQueryStatement, + createListInsertStatement, + createListDeleteStatement, + createListClearStatement, + createListUpdateStatement, + createMusicInfoQueryStatement, + createMusicInfoInsertStatement, + createMusicInfoUpdateStatement, + createMusicInfoDeleteStatement, + createMusicInfoDeleteByListIdStatement, + createMusicInfoOrderInsertStatement, + createMusicInfoOrderDeleteStatement, + createMusicInfoOrderDeleteByListIdStatement, + createMusicInfoClearStatement, + createMusicInfoOrderClearStatement, + createMusicInfoByListAndMusicInfoIdQueryStatement, + createMusicInfoByMusicInfoIdQueryStatement, + createMusicInfoOrderStatement, +} from './statements' + +const idFixRxp = /\.0$/ +/** + * 获取用户列表 + * @returns + */ +export const queryAllUserList = () => { + const list = createListQueryStatement().all() as LX.DBService.UserListInfo[] + for (const info of list) { + // 兼容v2.3.0之前版本插入数字类型的ID导致其意外在末尾追加 .0 的问题 + if (info.sourceListId?.endsWith?.('.0')) { + info.sourceListId = info.sourceListId.replace(idFixRxp, '') + } + } + return list +} + +/** + * 批量插入用户列表 + * @param lists 列表 + * @param isClear 是否清空列表 + */ +export const insertUserLists = (lists: LX.DBService.UserListInfo[], isClear: boolean = false) => { + const db = getDB() + const listClearStatement = createListClearStatement() + const listInsertStatement = createListInsertStatement() + db.transaction((lists: LX.DBService.UserListInfo[]) => { + if (isClear) listClearStatement.run() + for (const list of lists) { + listInsertStatement.run({ + id: list.id, + name: list.name, + source: list.source, + sourceListId: list.sourceListId, + locationUpdateTime: list.locationUpdateTime, + position: list.position, + }) + } + })(lists) +} + +/** + * 批量删除用户列表及列表内歌曲 + * @param listIds 列表id + */ +export const deleteUserLists = (listIds: string[]) => { + const db = getDB() + const listDeleteStatement = createListDeleteStatement() + const musicInfoDeleteByListIdStatement = createMusicInfoDeleteByListIdStatement() + const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement() + db.transaction((listIds: string[]) => { + for (const id of listIds) { + listDeleteStatement.run(id) + musicInfoDeleteByListIdStatement.run(id) + musicInfoOrderDeleteByListIdStatement.run(id) + } + })(listIds) +} + +/** + * 批量更新用户列表 + * @param lists 列表 + */ +export const updateUserLists = (lists: LX.DBService.UserListInfo[]) => { + const db = getDB() + const listUpdateStatement = createListUpdateStatement() + db.transaction((lists: LX.DBService.UserListInfo[]) => { + for (const list of lists) listUpdateStatement.run(list) + })(lists) +} + + +/** + * 批量添加歌曲 + * @param list + */ +export const insertMusicInfoList = (list: LX.DBService.MusicInfo[]) => { + const musicInfoInsertStatement = createMusicInfoInsertStatement() + const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement() + const db = getDB() + db.transaction((musics: LX.DBService.MusicInfo[]) => { + for (const music of musics) { + musicInfoInsertStatement.run(music) + musicInfoOrderInsertStatement.run({ + listId: music.listId, + musicInfoId: music.id, + order: music.order, + }) + } + })(list) +} + +/** + * 批量添加歌曲并刷新排序 + * @param list 新增歌曲 + * @param listId 列表Id + * @param listAll 原始列表歌曲,列表去重后 + */ +export const insertMusicInfoListAndRefreshOrder = (list: LX.DBService.MusicInfo[], listId: string, listAll: LX.DBService.MusicInfo[]) => { + const musicInfoInsertStatement = createMusicInfoInsertStatement() + const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement() + const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement() + + const db = getDB() + db.transaction((list: LX.DBService.MusicInfo[], listId: string, listAll: LX.DBService.MusicInfo[]) => { + musicInfoOrderDeleteByListIdStatement.run(listId) + for (const music of list) { + musicInfoInsertStatement.run(music) + musicInfoOrderInsertStatement.run({ + listId: music.listId, + musicInfoId: music.id, + order: music.order, + }) + } + for (const music of listAll) { + musicInfoOrderInsertStatement.run({ + listId: music.listId, + musicInfoId: music.id, + order: music.order, + }) + } + })(list, listId, listAll) +} + +/** + * 批量更新歌曲 + * @param list + */ +export const updateMusicInfos = (list: LX.DBService.MusicInfo[]) => { + const musicInfoUpdateStatement = createMusicInfoUpdateStatement() + const db = getDB() + db.transaction((musics: LX.DBService.MusicInfo[]) => { + for (const music of musics) { + musicInfoUpdateStatement.run(music) + } + })(list) +} + +/** + * 获取列表内的歌曲 + * @param listId 列表Id + * @returns 列表歌曲 + */ +export const queryMusicInfoByListId = (listId: string) => { + const musicInfoQueryStatement = createMusicInfoQueryStatement() + return musicInfoQueryStatement.all({ listId }) as LX.DBService.MusicInfo[] +} + +/** + * 批量移动歌曲 + * @param fromId 源列表Id + * @param ids 要移动的歌曲 + * @param musicInfos 音乐信息 + */ +export const moveMusicInfo = (fromId: string, ids: string[], musicInfos: LX.DBService.MusicInfo[]) => { + const musicInfoInsertStatement = createMusicInfoInsertStatement() + const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement() + const musicInfoDeleteStatement = createMusicInfoDeleteStatement() + const musicInfoOrderDeleteStatement = createMusicInfoOrderDeleteStatement() + // const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement() + + const db = getDB() + db.transaction((fromId: string, ids: string[], musicInfos: LX.DBService.MusicInfo[]) => { + // musicInfoOrderDeleteByListIdStatement.run(fromId) + for (const id of ids) { + musicInfoDeleteStatement.run({ listId: fromId, id }) + musicInfoOrderDeleteStatement.run({ listId: fromId, id }) + } + for (const music of musicInfos) { + musicInfoInsertStatement.run(music) + musicInfoOrderInsertStatement.run({ + listId: music.listId, + musicInfoId: music.id, + order: music.order, + }) + } + })(fromId, ids, musicInfos) +} + +/** + * 批量移动歌曲并刷新排序 + * @param fromId 源列表Id + * @param ids 要移动的歌曲id,原始选择的歌曲 + * @param musicInfos 要移动的歌曲,目标列表去重后 + * @param toListAll 目标列表歌曲 + */ +export const moveMusicInfoAndRefreshOrder = (fromId: string, ids: string[], toId: string, musicInfos: LX.DBService.MusicInfo[], toListAll: LX.DBService.MusicInfo[]) => { + const musicInfoInsertStatement = createMusicInfoInsertStatement() + const musicInfoDeleteStatement = createMusicInfoDeleteStatement() + const musicInfoOrderDeleteStatement = createMusicInfoOrderDeleteStatement() + const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement() + const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement() + + const db = getDB() + db.transaction((fromId: string, ids: string[], musicInfos: LX.DBService.MusicInfo[], toListAll: LX.DBService.MusicInfo[]) => { + for (const id of ids) { + musicInfoDeleteStatement.run({ listId: fromId, id }) + musicInfoOrderDeleteStatement.run({ listId: fromId, id }) + } + musicInfoOrderDeleteByListIdStatement.run(toId) + for (const music of musicInfos) { + musicInfoInsertStatement.run(music) + musicInfoOrderInsertStatement.run({ + listId: music.listId, + musicInfoId: music.id, + order: music.order, + }) + } + for (const music of toListAll) { + musicInfoOrderInsertStatement.run({ + listId: music.listId, + musicInfoId: music.id, + order: music.order, + }) + } + })(fromId, ids, musicInfos, toListAll) +} + +/** + * 批量移除列表内音乐 + * @param listId 列表id + * @param ids 音乐id + */ +export const removeMusicInfos = (listId: string, ids: string[]) => { + const musicInfoDeleteStatement = createMusicInfoDeleteStatement() + const musicInfoOrderDeleteStatement = createMusicInfoOrderDeleteStatement() + const db = getDB() + db.transaction((listId: string, ids: string[]) => { + for (const id of ids) { + musicInfoDeleteStatement.run({ listId, id }) + musicInfoOrderDeleteStatement.run({ listId, id }) + } + })(listId, ids) +} + +/** + * 清空列表内歌曲 + * @param listId 列表id + */ +export const removeMusicInfoByListId = (ids: string[]) => { + const db = getDB() + const musicInfoDeleteByListIdStatement = createMusicInfoDeleteByListIdStatement() + const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement() + db.transaction((ids: string[]) => { + for (const id of ids) { + musicInfoDeleteByListIdStatement.run(id) + musicInfoOrderDeleteByListIdStatement.run(id) + } + })(ids) +} + +/** + * 创建根据列表Id与音乐id查询音乐信息 + * @param listId 列表id + * @param musicInfoId 音乐id + * @returns + */ +export const queryMusicInfoByListIdAndMusicInfoId = (listId: string, musicInfoId: string) => { + const musicInfoByListAndMusicInfoIdQueryStatement = createMusicInfoByListAndMusicInfoIdQueryStatement() + return musicInfoByListAndMusicInfoIdQueryStatement.get({ listId, musicInfoId }) as LX.DBService.MusicInfo | null +} + +/** + * 创建根据音乐id查询所有列表的音乐信息 + * @param id 音乐id + * @returns + */ +export const queryMusicInfoByMusicInfoId = (id: string) => { + const musicInfoByMusicInfoIdQueryStatement = createMusicInfoByMusicInfoIdQueryStatement() + return musicInfoByMusicInfoIdQueryStatement.all(id) as LX.DBService.MusicInfo[] +} + +/** + * 批量更新歌曲位置 + * @param listId 列表id + * @param musicInfoOrders 音乐顺序 + */ +export const updateMusicInfoOrder = (listId: string, musicInfoOrders: LX.DBService.MusicInfoOrder[]) => { + const db = getDB() + const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement() + const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement() + db.transaction((listId: string, musicInfoOrders: LX.DBService.MusicInfoOrder[]) => { + musicInfoOrderDeleteByListIdStatement.run(listId) + for (const orderInfo of musicInfoOrders) musicInfoOrderInsertStatement.run(orderInfo) + })(listId, musicInfoOrders) +} + +/** + * 覆盖列表内的歌曲 + * @param listId 列表id + * @param musicInfos 歌曲列表 + */ +export const overwriteMusicInfo = (listId: string, musicInfos: LX.DBService.MusicInfo[]) => { + const db = getDB() + const musicInfoDeleteByListIdStatement = createMusicInfoDeleteByListIdStatement() + const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement() + const musicInfoInsertStatement = createMusicInfoInsertStatement() + const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement() + db.transaction((listId: string, musicInfos: LX.DBService.MusicInfo[]) => { + musicInfoDeleteByListIdStatement.run(listId) + musicInfoOrderDeleteByListIdStatement.run(listId) + for (const musicInfo of musicInfos) { + musicInfoInsertStatement.run(musicInfo) + musicInfoOrderInsertStatement.run({ + listId: musicInfo.listId, + musicInfoId: musicInfo.id, + order: musicInfo.order, + }) + } + })(listId, musicInfos) +} + +/** + * 覆盖整个列表 + * @param lists 列表 + * @param musicInfos 歌曲列表 + */ +export const overwriteListData = (lists: LX.DBService.UserListInfo[], musicInfos: LX.DBService.MusicInfo[]) => { + const db = getDB() + const listClearStatement = createListClearStatement() + const listInsertStatement = createListInsertStatement() + const musicInfoClearStatement = createMusicInfoClearStatement() + const musicInfoInsertStatement = createMusicInfoInsertStatement() + const musicInfoOrderClearStatement = createMusicInfoOrderClearStatement() + const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement() + db.transaction((lists: LX.DBService.UserListInfo[], musicInfos: LX.DBService.MusicInfo[]) => { + listClearStatement.run() + for (const list of lists) { + listInsertStatement.run({ + id: list.id, + name: list.name, + source: list.source, + sourceListId: list.sourceListId, + locationUpdateTime: list.locationUpdateTime, + position: list.position, + }) + } + musicInfoClearStatement.run() + musicInfoOrderClearStatement.run() + for (const musicInfo of musicInfos) { + musicInfoInsertStatement.run(musicInfo) + musicInfoOrderInsertStatement.run({ + listId: musicInfo.listId, + musicInfoId: musicInfo.id, + order: musicInfo.order, + }) + } + })(lists, musicInfos) +} + + +/** + * 获取列表内音乐的排序 + * @param listId 列表id + * @param musicInfoId 音乐id + * @returns 音乐排序信息 + */ +export const getMusicInfoOrder = (listId: string, musicInfoId: string) => { + const musicInfoOrderStatement = createMusicInfoOrderStatement() + return musicInfoOrderStatement.get({ listId, musicInfoId }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/list/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/list/index.ts new file mode 100644 index 000000000..79214d38e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/list/index.ts @@ -0,0 +1,416 @@ +import { LIST_IDS } from '@common/constants' +import { arrPush, arrPushByPosition, arrUnshift } from '@common/utils/common' +import { + deleteUserLists, + insertUserLists, + insertMusicInfoList, + insertMusicInfoListAndRefreshOrder, + moveMusicInfo, + moveMusicInfoAndRefreshOrder, + overwriteListData, + overwriteMusicInfo, + queryAllUserList, + queryMusicInfoByListId, + queryMusicInfoByListIdAndMusicInfoId, + queryMusicInfoByMusicInfoId, + removeMusicInfoByListId, + removeMusicInfos, + updateMusicInfoOrder, + updateMusicInfos, + updateUserLists as updateUserListsFromDB, + getMusicInfoOrder, +} from './dbHelper' + +let userLists: LX.DBService.UserListInfo[] +let musicLists = new Map() +let rawPoss = new Map() + +const toDBMusicInfo = (musicInfos: LX.Music.MusicInfo[], listId: string, offset: number = 0): LX.DBService.MusicInfo[] => { + return musicInfos.map((info, index) => { + return { + ...info, + listId, + meta: JSON.stringify(info.meta), + order: offset + index, + } + }) +} + +/** + * 获取所有用户列表 + * @returns + */ +export const getAllUserList = (): LX.List.UserListInfo[] => { + userLists ??= queryAllUserList() + + rawPoss.clear() + return userLists.map(list => { + const { position, ...newList } = list + rawPoss.set(list.id, position) + return newList + }) +} + +/** + * 批量创建列表 + * @param position 列表位置 + * @param lists 列表信息 + */ +export const createUserLists = (position: number, lists: LX.List.UserListInfo[]) => { + userLists ??= queryAllUserList() + if (position < 0 || position >= userLists.length) { + // 如果是最末尾,那么取最后一个列表的原始位置 + 1 作为新列表的原始位置,否则新列表的原始位置为 position + // 因为原始 pos 可能比 userLists.length 大,所以不能直接用 userLists.length 作为新列表的原始位置 + const order = userLists.length ? (rawPoss.get(userLists.at(-1)!.id) ?? userLists.length) + 1 : 0 + const newLists: LX.DBService.UserListInfo[] = lists.map((list, index) => { + const pos = order + index + rawPoss.set(list.id, pos) + return { + ...list, + position: pos, + } + }) + insertUserLists(newLists) + userLists = [...userLists, ...newLists] + } else { + const newUserLists = [...userLists] + // @ts-expect-error + newUserLists.splice(position, 0, ...lists) + newUserLists.forEach((list, index) => { + list.position = index + }) + insertUserLists(newUserLists, true) + userLists = newUserLists + rawPoss.clear() + for (const list of userLists) rawPoss.set(list.id, list.position) + } +} + +/** + * 覆盖列表 + * @param lists 列表信息 + */ +// const setUserLists = (lists: LX.List.UserListInfo[]) => { +// const newUserLists: LX.DBService.UserListInfo[] = lists.map((list, index) => { +// return { +// ...list, +// position: index, +// } +// }) +// insertUserLists(newUserLists, true) +// userLists = newUserLists +// } + +/** + * 批量删除列表 + * @param ids 列表ids + */ +export const removeUserLists = (ids: string[]) => { + deleteUserLists(ids) + userLists &&= queryAllUserList() + for (const id of ids) rawPoss.delete(id) +} + +/** + * 批量更新列表信息 + * @param lists 列表信息 + */ +export const updateUserLists = (lists: LX.List.UserListInfo[]) => { + const positionMap = new Map() + for (const list of userLists) { + positionMap.set(list.id, list.position) + } + const dbList: LX.DBService.UserListInfo[] = lists.map(list => { + const position = positionMap.get(list.id) + if (position == null) return null + return { + ...list, + position, + } + }).filter(Boolean) as LX.DBService.UserListInfo[] + updateUserListsFromDB(dbList) + userLists &&= queryAllUserList() +} + +/** + * 批量更新列表位置 + * @param position 列表位置 + * @param ids 列表ids + */ +export const updateUserListsPosition = (position: number, ids: string[]) => { + userLists ??= queryAllUserList() + + const newUserLists = [...userLists] + + const updateLists: LX.DBService.UserListInfo[] = [] + + for (let i = newUserLists.length - 1; i >= 0; i--) { + if (ids.includes(newUserLists[i].id)) { + const list = newUserLists.splice(i, 1)[0] + list.locationUpdateTime = Date.now() + updateLists.push(list) + } + } + position = Math.min(newUserLists.length, position) + + newUserLists.splice(position, 0, ...updateLists) + newUserLists.forEach((list, index) => { + list.position = index + }) + insertUserLists(newUserLists, true) + userLists = newUserLists + rawPoss.clear() + for (const list of userLists) rawPoss.set(list.id, list.position) +} + +/** + * 根据列表ID获取列表内歌曲 + * @param listId 列表ID + * @returns 列表内歌曲 + */ +export const getListMusics = (listId: string): LX.Music.MusicInfo[] => { + let targetList: LX.Music.MusicInfo[] | undefined = musicLists.get(listId) + if (targetList == null) { + targetList = queryMusicInfoByListId(listId).map(info => { + return { + id: info.id, + name: info.name, + singer: info.singer, + source: info.source, + interval: info.interval, + meta: JSON.parse(info.meta), + } + }) + musicLists.set(listId, targetList) + } + + return targetList +} + +/** + * 覆盖列表内的歌曲 + * @param listId 列表id + * @param musicInfos 歌曲列表 + */ +export const musicOverwrite = (listId: string, musicInfos: LX.Music.MusicInfo[]) => { + let targetList = getListMusics(listId) + overwriteMusicInfo(listId, toDBMusicInfo(musicInfos, listId)) + if (targetList) { + targetList.splice(0, targetList.length) + arrPush(targetList, musicInfos) + } +} + +/** + * 批量添加歌曲 + * @param listId 列表id + * @param musicInfos 添加的歌曲信息 + * @param addMusicLocationType 添加在到列表的位置 + */ +export const musicsAdd = (listId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType) => { + let targetList = getListMusics(listId) + + const set = new Set() + for (const item of targetList) set.add(item.id) + musicInfos = musicInfos.filter(item => { + if (set.has(item.id)) return false + set.add(item.id) + return true + }) + + switch (addMusicLocationType) { + case 'top': + insertMusicInfoListAndRefreshOrder(toDBMusicInfo(musicInfos, listId), listId, toDBMusicInfo(targetList, listId, musicInfos.length)) + arrUnshift(targetList, musicInfos) + break + case 'bottom': + default: { + // 如果是添加到最后,那么取最后一个歌曲的原始位置 + 1 作为新歌曲的原始位置,否则新歌曲的原始位置为 position + // 因为原始 pos 可能比 targetList.length 大,所以不能直接用 targetList.length 作为新歌曲的原始位置 + const order = targetList.length ? (getMusicInfoOrder(listId, targetList.at(-1)!.id)?.order ?? targetList.length) + 1 : 0 + insertMusicInfoList(toDBMusicInfo(musicInfos, listId, order)) + arrPush(targetList, musicInfos) + break + } + } +} + +/** + * 批量删除歌曲 + * @param listId 列表Id + * @param ids 要删除歌曲的id + */ +export const musicsRemove = (listId: string, ids: string[]) => { + let targetList = getListMusics(listId) + if (!targetList.length) return + removeMusicInfos(listId, ids) + const idsSet = new Set(ids) + musicLists.set(listId, targetList.filter(mInfo => !idsSet.has(mInfo.id))) +} + +/** + * 批量移动歌曲 + * @param fromId 源列表id + * @param toId 目标列表id + * @param musicInfos 添加的歌曲信息 + * @param addMusicLocationType 添加在到列表的位置 + */ +export const musicsMove = (fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType) => { + let fromList = getListMusics(fromId) + let toList = getListMusics(toId) + + const ids = musicInfos.map(musicInfo => musicInfo.id) + + let listSet = new Set() + for (const item of toList) listSet.add(item.id) + musicInfos = musicInfos.filter(item => { + if (listSet.has(item.id)) return false + listSet.add(item.id) + return true + }) + + switch (addMusicLocationType) { + case 'top': + moveMusicInfoAndRefreshOrder(fromId, ids, toId, toDBMusicInfo(musicInfos, toId), toDBMusicInfo(toList, toId, musicInfos.length)) + arrUnshift(toList, musicInfos) + break + case 'bottom': + default:{ + // 如果是添加到最后,那么取最后一个歌曲的原始位置 + 1 作为新歌曲的原始位置,否则新歌曲的原始位置为 position + // 因为原始 pos 可能比 targetList.length 大,所以不能直接用 targetList.length 作为新歌曲的原始位置 + const order = toList.length ? (getMusicInfoOrder(toId, toList.at(-1)!.id)?.order ?? toList.length) + 1 : 0 + moveMusicInfo(fromId, ids, toDBMusicInfo(musicInfos, toId, order)) + arrPush(toList, musicInfos) + break + } + } + + listSet = new Set(ids) + musicLists.set(fromId, fromList.filter(mInfo => !listSet.has(mInfo.id))) +} + +/** + * 批量更新歌曲信息 + * @param musicInfos 歌曲&列表信息 + */ +export const musicsUpdate = (musicInfos: LX.List.ListActionMusicUpdate) => { + updateMusicInfos(musicInfos.map(({ id, musicInfo }) => { + return { + ...musicInfo, + listId: id, + meta: JSON.stringify(musicInfo.meta), + order: 0, + } + })) + for (const { id, musicInfo } of musicInfos) { + const targetList = musicLists.get(id) + if (targetList == null) continue + const targetMusic = targetList.find(item => item.id == musicInfo.id) + if (!targetMusic) continue + targetMusic.name = musicInfo.name + targetMusic.singer = musicInfo.singer + targetMusic.source = musicInfo.source + targetMusic.interval = musicInfo.interval + targetMusic.meta = musicInfo.meta + } +} + +/** + * 清空列表内的歌曲 + * @param listId 列表Id + */ +export const musicsClear = (ids: string[]) => { + removeMusicInfoByListId(ids) + for (const id of ids) { + const targetList = musicLists.get(id) + if (!targetList) continue + targetList.splice(0, targetList.length) + } +} + +/** + * 批量更新歌曲位置 + * @param listId 列表id + * @param position 新位置 + * @param ids 要更新位置的歌曲id + */ +export const musicsPositionUpdate = (listId: string, position: number, ids: string[]) => { + let targetList = getListMusics(listId) + if (!targetList.length) return + + let newTargetList = [...targetList] + + const infos: LX.Music.MusicInfo[] = [] + const map = new Map() + for (const item of newTargetList) map.set(item.id, item) + for (const id of ids) { + infos.push(map.get(id)!) + map.delete(id) + } + newTargetList = newTargetList.filter(mInfo => map.has(mInfo.id)) + arrPushByPosition(newTargetList, infos, Math.min(position, newTargetList.length)) + + updateMusicInfoOrder(listId, newTargetList.map((info, index) => { + return { + listId, + musicInfoId: info.id, + order: index, + } + })) + musicLists.set(listId, newTargetList) +} + +/** + * 覆盖所有列表数据 + * @param myListData 完整列表数据 + */ +export const listDataOverwrite = (myListData: MakeOptional) => { + const dbLists: LX.DBService.UserListInfo[] = [] + const listData: LX.List.ListDataFull = { + ...myListData, + tempList: myListData.tempList ?? getListMusics(LIST_IDS.TEMP), + } + + const dbMusicInfos: LX.DBService.MusicInfo[] = [ + ...toDBMusicInfo(listData.defaultList, LIST_IDS.DEFAULT), + ...toDBMusicInfo(listData.loveList, LIST_IDS.LOVE), + ...toDBMusicInfo(listData.tempList, LIST_IDS.TEMP), + ] + listData.userList.forEach(({ list, ...listInfo }, index) => { + dbLists.push({ ...listInfo, position: index }) + arrPush(dbMusicInfos, toDBMusicInfo(list, listInfo.id)) + }) + overwriteListData(dbLists, dbMusicInfos) + + if (userLists) userLists.splice(0, userLists.length, ...dbLists) + else userLists = dbLists + + rawPoss.clear() + for (const list of userLists) rawPoss.set(list.id, list.position) + musicLists.clear() + musicLists.set(LIST_IDS.DEFAULT, listData.defaultList) + musicLists.set(LIST_IDS.LOVE, listData.loveList) + musicLists.set(LIST_IDS.TEMP, listData.tempList) + for (const list of listData.userList) musicLists.set(list.id, list.list) +} + +/** + * 检查音乐是否存在列表中 + * @param listId 列表id + * @param musicInfoId 音乐id + * @returns + */ +export const checkListExistMusic = (listId: string, musicInfoId: string): boolean => { + const musicInfo = queryMusicInfoByListIdAndMusicInfoId(listId, musicInfoId) + return musicInfo != null +} + +/** + * 获取所有存在该音乐的列表id + * @param musicInfoId 音乐id + * @returns + */ +export const getMusicExistListIds = (musicInfoId: string): string[] => { + const musicInfos = queryMusicInfoByMusicInfoId(musicInfoId) + return musicInfos.map(m => m.listId) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/list/statements.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/list/statements.ts new file mode 100644 index 000000000..a8d19a5a9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/list/statements.ts @@ -0,0 +1,197 @@ +import { getDB } from '../../db' + + +/** + * 创建列表查询语句 + * @returns 查询语句 + */ +export const createListQueryStatement = () => { + const db = getDB() + return db.prepare<[]>(` + SELECT "id", "name", "source", "sourceListId", "position", "locationUpdateTime" + FROM "main"."my_list" + `) +} + +/** + * 创建列表插入语句 + * @returns 插入语句 + */ +export const createListInsertStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.UserListInfo]>(` + INSERT INTO "main"."my_list" ("id", "name", "source", "sourceListId", "position", "locationUpdateTime") + VALUES (@id, @name, @source, @sourceListId, @position, @locationUpdateTime)`) +} + +/** + * 创建列表清空语句 + * @returns 清空语句 + */ +export const createListClearStatement = () => { + const db = getDB() + return db.prepare<[]>('DELETE FROM "main"."my_list"') +} + +/** + * 创建列表删除语句 + * @returns 删除语句 + */ +export const createListDeleteStatement = () => { + const db = getDB() + return db.prepare<[string]>('DELETE FROM "main"."my_list" WHERE "id"=?') +} + +/** + * 创建列表更新语句 + * @returns 更新语句 + */ +export const createListUpdateStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.UserListInfo]>(` + UPDATE "main"."my_list" + SET "name"=@name, "source"=@source, "sourceListId"=@sourceListId, "locationUpdateTime"=@locationUpdateTime + WHERE "id"=@id`) +} + +/** + * 创建音乐信息查询语句 + * @returns 查询语句 + */ +export const createMusicInfoQueryStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.MusicInfoQuery]>(` + SELECT mInfo."id", mInfo."name", mInfo."singer", mInfo."source", mInfo."interval", mInfo."meta" + FROM my_list_music_info mInfo + LEFT JOIN my_list_music_info_order O + ON mInfo.id=O.musicInfoId AND O.listId=@listId + WHERE mInfo.listId=@listId + ORDER BY O."order" ASC + `) +} + +/** + * 创建音乐信息插入语句 + * @returns 插入语句 + */ +export const createMusicInfoInsertStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.MusicInfo]>(` + INSERT INTO "main"."my_list_music_info" ("id", "listId", "name", "singer", "source", "interval", "meta") + VALUES (@id, @listId, @name, @singer, @source, @interval, @meta)`) +} + +/** + * 创建音乐信息更新语句 + * @returns 更新语句 + */ +export const createMusicInfoUpdateStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.MusicInfo]>(` + UPDATE "main"."my_list_music_info" + SET "name"=@name, "singer"=@singer, "source"=@source, "interval"=@interval, "meta"=@meta + WHERE "id"=@id AND "listId"=@listId`) +} + + +/** + * 创建清空音乐信息语句 + * @returns 删除语句 + */ +export const createMusicInfoClearStatement = () => { + const db = getDB() + return db.prepare<[]>('DELETE FROM "main"."my_list_music_info"') +} + +/** + * 创建根据列表id批量删除音乐信息语句 + * @returns 删除语句 + */ +export const createMusicInfoDeleteByListIdStatement = () => { + const db = getDB() + return db.prepare<[string]>('DELETE FROM "main"."my_list_music_info" WHERE "listId"=?') +} + +/** + * 创建根据列表Id与音乐id批量删除音乐信息语句 + * @returns 删除语句 + */ +export const createMusicInfoDeleteStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.MusicInfoRemove]>('DELETE FROM "main"."my_list_music_info" WHERE "id"=@id AND "listId"=@listId') +} + +/** + * 创建根据列表Id与音乐id查询音乐信息语句 + * @returns 删除语句 + */ +export const createMusicInfoByListAndMusicInfoIdQueryStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.ListMusicInfoQuery]>(`SELECT "id", "name", "singer", "source", "interval", "meta" + FROM "main"."my_list_music_info" + WHERE "id"=@musicInfoId + AND "listId"=@listId`) +} + +/** + * 创建根据音乐id查询音乐信息语句 + * @returns 删除语句 + */ +export const createMusicInfoByMusicInfoIdQueryStatement = () => { + const db = getDB() + return db.prepare<[string]>(`SELECT "id", "name", "singer", "source", "interval", "meta", "listId" + FROM "main"."my_list_music_info" + WHERE "id"=?`) +} + + +/** + * 创建根据列表Id与音乐id查询音乐排序语句 + * @returns 查询语句 + */ +export const createMusicInfoOrderStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.ListMusicInfoQuery], LX.DBService.MusicInfoOrder>(`SELECT "listId", "musicInfoId", "order" + FROM "main"."my_list_music_info_order" + WHERE "listId"=@listId AND "musicInfoId"=@musicInfoId`) +} + +/** + * 创建音乐信息排序插入语句 + * @returns 插入语句 + */ +export const createMusicInfoOrderInsertStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.MusicInfoOrder]>(` + INSERT INTO "main"."my_list_music_info_order" ("listId", "musicInfoId", "order") + VALUES (@listId, @musicInfoId, @order)`) +} + +/** + * 创建清空音乐排序语句 + * @returns 删除语句 + */ +export const createMusicInfoOrderClearStatement = () => { + const db = getDB() + return db.prepare<[]>('DELETE FROM "main"."my_list_music_info_order"') +} + +/** + * 创建根据列表id删除音乐排序语句 + * @returns 删除语句 + */ +export const createMusicInfoOrderDeleteByListIdStatement = () => { + const db = getDB() + return db.prepare<[string]>('DELETE FROM "main"."my_list_music_info_order" WHERE "listId"=?') +} + +/** + * 创建根据列表Id与音乐id删除音乐排序语句 + * @returns 删除语句 + */ +export const createMusicInfoOrderDeleteStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.MusicInfoRemove]>('DELETE FROM "main"."my_list_music_info_order" WHERE "musicInfoId"=@id AND "listId"=@listId') +} + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/lyric/dbHelper.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/lyric/dbHelper.ts new file mode 100644 index 000000000..5e13eb5a4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/lyric/dbHelper.ts @@ -0,0 +1,152 @@ +import { getDB } from '../../db' +import { + createLyricQueryStatement, + createRawLyricQueryStatement, + createRawLyricInsertStatement, + createRawLyricDeleteStatement, + createRawLyricUpdateStatement, + createRawLyricClearStatement, + createEditedLyricQueryStatement, + createEditedLyricInsertStatement, + createEditedLyricDeleteStatement, + createEditedLyricUpdateStatement, + createEditedLyricClearStatement, + createEditedLyricCountStatement, + createRawLyricCountStatement, +} from './statements' + +/** + * 查询原始歌词 + * @param id 歌曲id + * @returns 歌词信息 + */ +export const queryLyric = (id: string) => { + const lyricQueryStatement = createLyricQueryStatement() + return lyricQueryStatement.all(id) as LX.DBService.Lyricnfo[] +} + +/** + * 查询原始歌词 + * @param id 歌曲id + * @returns 歌词信息 + */ +export const queryRawLyric = (id: string) => { + const rawLyricQueryStatement = createRawLyricQueryStatement() + return rawLyricQueryStatement.all(id) as LX.DBService.Lyricnfo[] +} + +/** + * 批量插入原始歌词 + * @param lyrics 列表 + */ +export const insertRawLyric = (lyrics: LX.DBService.Lyricnfo[]) => { + const db = getDB() + const rawLyricInsertStatement = createRawLyricInsertStatement() + db.transaction((lyrics: LX.DBService.Lyricnfo[]) => { + for (const lyric of lyrics) rawLyricInsertStatement.run(lyric) + })(lyrics) +} + +/** + * 批量删除原始歌词 + * @param ids 列表 + */ +export const deleteRawLyric = (ids: string[]) => { + const db = getDB() + const rawLyricDeleteStatement = createRawLyricDeleteStatement() + db.transaction((ids: string[]) => { + for (const id of ids) rawLyricDeleteStatement.run(id) + })(ids) +} + +/** + * 批量更新原始歌词 + * @param lyrics 列表 + */ +export const updateRawLyric = (lyrics: LX.DBService.Lyricnfo[]) => { + const db = getDB() + const rawLyricUpdateStatement = createRawLyricUpdateStatement() + db.transaction((lyrics: LX.DBService.Lyricnfo[]) => { + for (const lyric of lyrics) rawLyricUpdateStatement.run(lyric) + })(lyrics) +} + +/** + * 清空原始歌词 + */ +export const clearRawLyric = () => { + const rawLyricClearStatement = createRawLyricClearStatement() + rawLyricClearStatement.run() +} + +/** + * 统计已编辑歌词数量 + */ +export const countRawLyric = () => { + const countStatement = createRawLyricCountStatement() + return (countStatement.get() as { count: number }).count +} + + +/** + * 查询已编辑歌词 + * @param id 歌曲id + * @returns 歌词信息 + */ +export const queryEditedLyric = (id: string) => { + const rawLyricQueryStatement = createEditedLyricQueryStatement() + return rawLyricQueryStatement.all(id) as LX.DBService.Lyricnfo[] +} + +/** + * 批量插入已编辑歌词 + * @param lyrics 列表 + */ +export const insertEditedLyric = (lyrics: LX.DBService.Lyricnfo[]) => { + const db = getDB() + const rawLyricInsertStatement = createEditedLyricInsertStatement() + db.transaction((lyrics: LX.DBService.Lyricnfo[]) => { + for (const lyric of lyrics) rawLyricInsertStatement.run(lyric) + })(lyrics) +} + +/** + * 批量删除已编辑歌词 + * @param ids 列表 + */ +export const deleteEditedLyric = (ids: string[]) => { + const db = getDB() + const rawLyricDeleteStatement = createEditedLyricDeleteStatement() + db.transaction((ids: string[]) => { + for (const id of ids) rawLyricDeleteStatement.run(id) + })(ids) +} + +/** + * 批量更新已编辑歌词 + * @param lyrics 列表 + */ +export const updateEditedLyric = (lyrics: LX.DBService.Lyricnfo[]) => { + const db = getDB() + const rawLyricUpdateStatement = createEditedLyricUpdateStatement() + db.transaction((lyrics: LX.DBService.Lyricnfo[]) => { + for (const lyric of lyrics) rawLyricUpdateStatement.run(lyric) + })(lyrics) +} + +/** + * 清空已编辑歌词 + */ +export const clearEditedLyric = () => { + const rawLyricClearStatement = createEditedLyricClearStatement() + rawLyricClearStatement.run() +} + + +/** + * 统计已编辑歌词数量 + */ +export const countEditedLyric = () => { + const countStatement = createEditedLyricCountStatement() + return (countStatement.get() as { count: number }).count +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/lyric/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/lyric/index.ts new file mode 100644 index 000000000..0c8b4efc9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/lyric/index.ts @@ -0,0 +1,197 @@ +import { + queryLyric, + queryRawLyric, + insertRawLyric, + deleteRawLyric, + updateRawLyric, + clearRawLyric, + queryEditedLyric, + insertEditedLyric, + deleteEditedLyric, + updateEditedLyric, + clearEditedLyric, + countEditedLyric, + countRawLyric, +} from './dbHelper' + +const keys = ['lyric', 'tlyric', 'rlyric', 'lxlyric'] as const + +const toDBLyric = (id: string, source: LX.DBService.Lyricnfo['source'], lyricInfo: LX.Music.LyricInfo): LX.DBService.Lyricnfo[] => { + return (keys.map(k => [k, lyricInfo[k]]) + .filter(([k, t]) => t != null) as Array<[LX.DBService.Lyricnfo['type'], string]>) + .map(([k, t]) => { + return { + id, + type: k, + text: Buffer.from(t).toString('base64'), + source, + } + }) +} + +/** + * 获取歌词 + * @param id 歌曲id + * @returns 歌词信息 + */ +export const getPlayerLyric = (id: string): LX.Player.LyricInfo => { + const lyrics = queryLyric(id) + + let lyricInfo: LX.Music.LyricInfo = { + lyric: '', + } + let rawLyricInfo: LX.Music.LyricInfo = { + lyric: '', + } + for (const lyric of lyrics) { + switch (lyric.source) { + case 'edited': + if (lyric.type == 'lyric') lyricInfo.lyric = Buffer.from(lyric.text, 'base64').toString() + else if (lyric.text != null) lyricInfo[lyric.type] = Buffer.from(lyric.text, 'base64').toString() + break + default: + if (lyric.type == 'lyric') rawLyricInfo.lyric = Buffer.from(lyric.text, 'base64').toString() + else if (lyric.text != null) rawLyricInfo[lyric.type] = Buffer.from(lyric.text, 'base64').toString() + break + } + } + + return lyricInfo.lyric ? { + ...lyricInfo, + rawlrcInfo: rawLyricInfo, + } : { + ...rawLyricInfo, + rawlrcInfo: rawLyricInfo, + } +} + +/** + * 获取原始歌词 + * @param id 歌曲id + * @returns 歌词信息 + */ +export const getRawLyric = (id: string): LX.Music.LyricInfo => { + const lyrics = queryRawLyric(id) + + let lyricInfo: LX.Music.LyricInfo = { + lyric: '', + } + for (const lyric of lyrics) { + if (lyric.type == 'lyric') lyricInfo.lyric = Buffer.from(lyric.text, 'base64').toString() + else if (lyric.text != null) lyricInfo[lyric.type] = Buffer.from(lyric.text, 'base64').toString() + } + + return lyricInfo +} + +/** + * 保存原始歌词信息 + * @param id 歌曲id + * @param lyricInfo 歌词信息 + */ +export const rawLyricAdd = (id: string, lyricInfo: LX.Music.LyricInfo) => { + insertRawLyric(toDBLyric(id, 'raw', lyricInfo)) +} + +/** + * 删除原始歌词信息 + * @param ids 歌曲id + */ +export const rawLyricRemove = (ids: string[]) => { + deleteRawLyric(ids) +} + +/** + * 更新原始歌词信息 + * @param id 歌曲id + * @param lyricInfo 歌词信息 + */ +export const rawLyricUpdate = (id: string, lyricInfo: LX.Music.LyricInfo) => { + updateRawLyric(toDBLyric(id, 'raw', lyricInfo)) +} + +/** + * 清空原始歌词信息 + */ +export const rawLyricClear = () => { + clearRawLyric() +} + +/** + * 统计原始歌词数量 + */ +export const rawLyricCount = () => { + return countRawLyric() +} + + +/** + * 获取已编辑歌词 + * @param id 歌曲id + * @returns 歌词信息 + */ +export const getEditedLyric = (id: string): LX.Music.LyricInfo => { + const lyrics = queryEditedLyric(id) + + let lyricInfo: LX.Music.LyricInfo = { + lyric: '', + } + for (const lyric of lyrics) { + if (lyric.type == 'lyric') lyricInfo.lyric = Buffer.from(lyric.text, 'base64').toString() + else if (lyric.text != null) lyricInfo[lyric.type] = Buffer.from(lyric.text, 'base64').toString() + } + + return lyricInfo +} + +/** + * 保存已编辑歌词信息 + * @param id 歌曲id + * @param lyricInfo 歌词信息 + */ +export const editedLyricAdd = (id: string, lyricInfo: LX.Music.LyricInfo) => { + insertEditedLyric(toDBLyric(id, 'edited', lyricInfo)) +} + +/** + * 删除已编辑歌词信息 + * @param ids 歌曲id + */ +export const editedLyricRemove = (ids: string[]) => { + deleteEditedLyric(ids) +} + +/** + * 更新已编辑歌词信息 + * @param id 歌曲id + * @param lyricInfo 歌词信息 + */ +export const editedLyricUpdate = (id: string, lyricInfo: LX.Music.LyricInfo) => { + updateEditedLyric(toDBLyric(id, 'edited', lyricInfo)) +} + +/** + * 清空已编辑歌词信息 + */ +export const editedLyricClear = () => { + clearEditedLyric() +} + +/** + * 新增或更新已编辑歌词信息 + * @param id 歌曲id + * @param lyricInfo 歌词信息 + */ +export const editedLyricUpdateAddAndUpdate = (id: string, lyricInfo: LX.Music.LyricInfo) => { + const lyrics = queryEditedLyric(id) + if (lyrics.length) updateEditedLyric(toDBLyric(id, 'edited', lyricInfo)) + else insertEditedLyric(toDBLyric(id, 'edited', lyricInfo)) +} + +/** + * 统计已编辑歌词数量 + */ +export const editedLyricCount = () => { + return countEditedLyric() +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/lyric/statements.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/lyric/statements.ts new file mode 100644 index 000000000..40c5bae99 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/lyric/statements.ts @@ -0,0 +1,157 @@ +import { getDB } from '../../db' + +const RAW_LYRIC = 'raw' +const EDITED_LYRIC = 'edited' + +/** + * 创建歌词查询语句 + * @returns 查询语句 + */ +export const createLyricQueryStatement = () => { + const db = getDB() + return db.prepare<[string]>(` + SELECT "type", "text", "source" + FROM "main"."lyric" + WHERE "id"=? + `) +} + +/** + * 创建原始歌词查询语句 + * @returns 查询语句 + */ +export const createRawLyricQueryStatement = () => { + const db = getDB() + return db.prepare<[string]>(` + SELECT "type", "text" + FROM "main"."lyric" + WHERE "id"=? AND "source"='${RAW_LYRIC}' + `) +} + +/** + * 创建原始歌词插入语句 + * @returns 插入语句 + */ +export const createRawLyricInsertStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.Lyricnfo]>(` + INSERT INTO "main"."lyric" ("id", "type", "text", "source") + VALUES (@id, @type, @text, '${RAW_LYRIC}')`) +} + +/** + * 创建原始歌词清空语句 + * @returns 清空语句 + */ +export const createRawLyricClearStatement = () => { + const db = getDB() + return db.prepare<[]>(` + DELETE FROM "main"."lyric" + WHERE "source"='${RAW_LYRIC}' + `) +} + +/** + * 创建原始歌词删除语句 + * @returns 删除语句 + */ +export const createRawLyricDeleteStatement = () => { + const db = getDB() + return db.prepare<[string]>(` + DELETE FROM "main"."lyric" + WHERE "id"=? AND "source"='${RAW_LYRIC}' + `) +} + +/** + * 创建原始歌词更新语句 + * @returns 更新语句 + */ +export const createRawLyricUpdateStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.Lyricnfo]>(` + UPDATE "main"."lyric" + SET "text"=@text + WHERE "id"=@id AND "source"='${RAW_LYRIC}' AND "type"=@type`) +} + + +/** + * 创建原始歌词数量统计语句 + * @returns 统计语句 + */ +export const createRawLyricCountStatement = () => { + const db = getDB() + return db.prepare<[]>(`SELECT COUNT(*) as count FROM "main"."lyric" WHERE "source"='${RAW_LYRIC}'`) +} + + +/** + * 创建已编辑歌词查询语句 + * @returns 查询语句 + */ +export const createEditedLyricQueryStatement = () => { + const db = getDB() + return db.prepare<[string]>(` + SELECT "type", "text" + FROM "main"."lyric" + WHERE "id"=? AND "source"='${EDITED_LYRIC}' + `) +} + +/** + * 创建已编辑歌词插入语句 + * @returns 插入语句 + */ +export const createEditedLyricInsertStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.Lyricnfo]>(` + INSERT INTO "main"."lyric" ("id", "type", "text", "source") + VALUES (@id, @type, @text, '${EDITED_LYRIC}')`) +} + +/** + * 创建已编辑歌词清空语句 + * @returns 清空语句 + */ +export const createEditedLyricClearStatement = () => { + const db = getDB() + return db.prepare<[]>(` + DELETE FROM "main"."lyric" + WHERE "source"='${EDITED_LYRIC}' + `) +} + +/** + * 创建已编辑歌词删除语句 + * @returns 删除语句 + */ +export const createEditedLyricDeleteStatement = () => { + const db = getDB() + return db.prepare<[string]>(` + DELETE FROM "main"."lyric" + WHERE "id"=? AND "source"='${EDITED_LYRIC}' + `) +} + +/** + * 创建已编辑歌词更新语句 + * @returns 更新语句 + */ +export const createEditedLyricUpdateStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.Lyricnfo]>(` + UPDATE "main"."lyric" + SET "text"=@text + WHERE "id"=@id AND "source"='${EDITED_LYRIC}' AND "type"=@type`) +} + +/** + * 创建已编辑歌词数量统计语句 + * @returns 统计语句 + */ +export const createEditedLyricCountStatement = () => { + const db = getDB() + return db.prepare<[]>(`SELECT COUNT(*) as count FROM "main"."lyric" WHERE "source"='${EDITED_LYRIC}'`) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_other_source/dbHelper.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_other_source/dbHelper.ts new file mode 100644 index 000000000..f57092823 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_other_source/dbHelper.ts @@ -0,0 +1,59 @@ +import { getDB } from '../../db' +import { + createMusicInfoQueryStatement, + createMusicInfoInsertStatement, + createMusicInfoDeleteStatement, + createMusicInfoClearStatement, + createCountStatement, +} from './statements' + + +/** + * 查询歌曲信息 + * @param id 歌曲id + * @returns 歌曲信息 + */ +export const queryMusicInfo = (id: string) => { + const musicInfoQueryStatement = createMusicInfoQueryStatement() + return musicInfoQueryStatement.all(id) as LX.DBService.MusicInfoOtherSource[] +} + +/** + * 批量插入歌曲信息 + * @param musicInfos 列表 + */ +export const insertMusicInfo = (musicInfos: LX.DBService.MusicInfoOtherSource[]) => { + const db = getDB() + const musicInfoInsertStatement = createMusicInfoInsertStatement() + db.transaction((musicInfos: LX.DBService.MusicInfoOtherSource[]) => { + for (const info of musicInfos) musicInfoInsertStatement.run(info) + })(musicInfos) +} + +/** + * 批量删除歌曲信息 + * @param ids 列表 + */ +export const deleteMusicInfo = (ids: string[]) => { + const db = getDB() + const musicInfoDeleteStatement = createMusicInfoDeleteStatement() + db.transaction((ids: string[]) => { + for (const id of ids) musicInfoDeleteStatement.run(id) + })(ids) +} + +/** + * 清空歌曲信息 + */ +export const clearMusicInfo = () => { + const musicInfoClearStatement = createMusicInfoClearStatement() + musicInfoClearStatement.run() +} + +/** + * 统计歌曲信息数量 + */ +export const countMusicInfo = () => { + const countStatement = createCountStatement() + return (countStatement.get() as { count: number }).count +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_other_source/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_other_source/index.ts new file mode 100644 index 000000000..d4cb934d3 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_other_source/index.ts @@ -0,0 +1,72 @@ +import { + queryMusicInfo, + insertMusicInfo, + deleteMusicInfo, + clearMusicInfo, + countMusicInfo, +} from './dbHelper' + + +const toDBMusicInfo = (id: string, musicInfos: LX.Music.MusicInfo[]): LX.DBService.MusicInfoOtherSource[] => { + return musicInfos.map((info, index) => { + return { + ...info, + meta: JSON.stringify(info.meta), + source_id: id, + order: index, + } + }) +} + +/** + * 获取歌曲信息 + * @param id 歌曲id + * @returns 歌词信息 + */ +export const getMusicInfoOtherSource = (id: string): LX.Music.MusicInfoOnline[] => { + const list = queryMusicInfo(id).sort((a, b) => a.order - b.order).map(info => { + return { + id: info.id, + name: info.name, + singer: info.singer, + source: info.source, + interval: info.interval, + meta: JSON.parse(info.meta), + } + }) + + return list +} + +/** + * 保存歌曲信息信息 + * @param id 歌曲id + * @param musicInfos 歌词信息 + */ +export const musicInfoOtherSourceAdd = (id: string, musicInfos: LX.Music.MusicInfoOnline[]) => { + insertMusicInfo(toDBMusicInfo(id, musicInfos)) +} + +/** + * 删除歌曲信息信息 + * @param ids 歌曲id + */ +export const musicInfoOtherSourceRemove = (ids: string[]) => { + deleteMusicInfo(ids) +} + +/** + * 清空歌曲信息信息 + */ +export const musicInfoOtherSourceClear = () => { + clearMusicInfo() +} + + +/** + * 统计歌曲信息信息数量 + */ +export const musicInfoOtherSourceCount = () => { + return countMusicInfo() +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_other_source/statements.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_other_source/statements.ts new file mode 100644 index 000000000..09a0a6a35 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_other_source/statements.ts @@ -0,0 +1,60 @@ +import { getDB } from '../../db' + + +/** + * 创建歌曲信息查询语句 + * @returns 查询语句 + */ +export const createMusicInfoQueryStatement = () => { + const db = getDB() + return db.prepare<[string]>(` + SELECT "id", "name", "singer", "source", "meta" + FROM "main"."music_info_other_source" + WHERE source_id=? + ORDER BY "order" ASC + `) +} + +/** + * 创建歌曲信息插入语句 + * @returns 插入语句 + */ +export const createMusicInfoInsertStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.MusicInfoOtherSource]>(` + INSERT INTO "main"."music_info_other_source" ("id", "name", "singer", "source", "meta", "source_id", "order") + VALUES (@id, @name, @singer, @source, @meta, @source_id, @order) + `) +} + +/** + * 创建歌曲信息清空语句 + * @returns 清空语句 + */ +export const createMusicInfoClearStatement = () => { + const db = getDB() + return db.prepare<[]>(` + DELETE FROM "main"."music_info_other_source" + `) +} + +/** + * 创建歌曲信息删除语句 + * @returns 删除语句 + */ +export const createMusicInfoDeleteStatement = () => { + const db = getDB() + return db.prepare<[string]>(` + DELETE FROM "main"."music_info_other_source" + WHERE "source_id"=? + `) +} + +/** + * 创建数量统计语句 + * @returns 统计语句 + */ +export const createCountStatement = () => { + const db = getDB() + return db.prepare<[]>('SELECT COUNT(*) as count FROM "main"."music_info_other_source"') +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_url/dbHelper.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_url/dbHelper.ts new file mode 100644 index 000000000..8aa7ccbde --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_url/dbHelper.ts @@ -0,0 +1,75 @@ +import { getDB } from '../../db' +import { + createQueryStatement, + createInsertStatement, + createDeleteStatement, + // createUpdateStatement, + createClearStatement, + createCountStatement, +} from './statements' + +/** + * 查询歌曲url + * @param id 歌曲id + * @returns url + */ +export const queryMusicUrl = (id: string) => { + const queryStatement = createQueryStatement() + return (queryStatement.get(id) as { url: string } | null)?.url ?? null +} + +/** + * 批量插入歌曲url + * @param urlInfo 列表 + */ +export const insertMusicUrl = (urlInfo: LX.DBService.MusicUrlInfo[]) => { + const db = getDB() + const insertStatement = createInsertStatement() + const deleteStatement = createDeleteStatement() + db.transaction((urlInfo: LX.DBService.MusicUrlInfo[]) => { + for (const info of urlInfo) { + deleteStatement.run(info.id) + insertStatement.run(info) + } + })(urlInfo) +} + +/** + * 批量删除歌曲url + * @param ids 列表 + */ +export const deleteMusicUrl = (ids: string[]) => { + const db = getDB() + const deleteStatement = createDeleteStatement() + db.transaction((ids: string[]) => { + for (const id of ids) deleteStatement.run(id) + })(ids) +} + +/** + * 批量更新歌曲url + * @param urlInfo 列表 + */ +// export const updateMusicUrl = (urlInfo: LX.DBService.MusicUrlInfo[]) => { +// const db = getDB() +// const updateStatement = createUpdateStatement() +// db.transaction((urlInfo: LX.DBService.MusicUrlInfo[]) => { +// for (const info of urlInfo) updateStatement.run(info) +// })(urlInfo) +// } + +/** + * 清空歌曲url + */ +export const clearMusicUrl = () => { + const clearStatement = createClearStatement() + clearStatement.run() +} + +/** + * 统计歌曲信息数量 + */ +export const countMusicUrl = () => { + const countStatement = createCountStatement() + return (countStatement.get() as { count: number }).count +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_url/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_url/index.ts new file mode 100644 index 000000000..0eb40c6a2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_url/index.ts @@ -0,0 +1,49 @@ +import { + queryMusicUrl, + insertMusicUrl, + deleteMusicUrl, + clearMusicUrl, + countMusicUrl, +} from './dbHelper' + + +/** + * 获取歌曲url + * @param id 歌曲id + * @returns 歌曲url + */ +export const getMusicUrl = (id: string): string | null => { + const url = queryMusicUrl(id) + return url +} + +/** + * 保存歌曲url + * @param urlInfos url信息 + */ +export const musicUrlSave = (urlInfos: LX.Music.MusicUrlInfo[]) => { + insertMusicUrl(urlInfos) +} + +/** + * 删除歌曲url + * @param ids 歌曲id + */ +export const musicUrlRemove = (ids: string[]) => { + deleteMusicUrl(ids) +} + +/** + * 清空歌曲url + */ +export const musicUrlClear = () => { + clearMusicUrl() +} + +/** + * 统计歌曲url数量 + */ +export const musicUrlCount = () => { + return countMusicUrl() +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_url/statements.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_url/statements.ts new file mode 100644 index 000000000..09a26557e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/modules/music_url/statements.ts @@ -0,0 +1,69 @@ +import { getDB } from '../../db' + +/** + * 创建歌曲url查询语句 + * @returns 查询语句 + */ +export const createQueryStatement = () => { + const db = getDB() + return db.prepare<[string]>(` + SELECT "url" + FROM "main"."music_url" + WHERE "id"=? + `) +} + +/** + * 创建歌曲url插入语句 + * @returns 插入语句 + */ +export const createInsertStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.MusicUrlInfo]>(` + INSERT INTO "main"."music_url" ("id", "url") + VALUES (@id, @url)`) +} + +/** + * 创建歌曲url清空语句 + * @returns 清空语句 + */ +export const createClearStatement = () => { + const db = getDB() + return db.prepare<[]>(` + DELETE FROM "main"."music_url" + `) +} + +/** + * 创建歌曲url删除语句 + * @returns 删除语句 + */ +export const createDeleteStatement = () => { + const db = getDB() + return db.prepare<[string]>(` + DELETE FROM "main"."music_url" + WHERE "id"=? + `) +} + +/** + * 创建歌曲url更新语句 + * @returns 更新语句 + */ +export const createUpdateStatement = () => { + const db = getDB() + return db.prepare<[LX.DBService.MusicUrlInfo]>(` + UPDATE "main"."music_url" + SET "url"=@url + WHERE "id"=@id`) +} + +/** + * 创建数量统计语句 + * @returns 统计语句 + */ +export const createCountStatement = () => { + const db = getDB() + return db.prepare<[]>('SELECT COUNT(*) as count FROM "main"."music_url"') +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/tables.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/tables.ts new file mode 100644 index 000000000..73d559603 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/tables.ts @@ -0,0 +1,231 @@ +// export const sql = ` +// CREATE TABLE "db_info" ( +// "id" INTEGER NOT NULL UNIQUE, +// "field_name" TEXT, +// "field_value" TEXT, +// PRIMARY KEY("id" AUTOINCREMENT) +// ); + +// CREATE TABLE "my_list" ( +// "id" TEXT NOT NULL, +// "name" TEXT NOT NULL, +// "source" TEXT, +// "sourceListId" TEXT, +// "position" INTEGER NOT NULL, +// "locationUpdateTime" INTEGER, +// PRIMARY KEY("id") +// ); + +// CREATE TABLE "my_list_music_info" ( +// "id" TEXT NOT NULL, +// "listId" TEXT NOT NULL, +// "name" TEXT NOT NULL, +// "singer" TEXT NOT NULL, +// "source" TEXT NOT NULL, +// "interval" TEXT, +// "meta" TEXT NOT NULL, +// UNIQUE("id","listId") +// ); +// CREATE INDEX "index_my_list_music_info" ON "my_list_music_info" ( +// "id", +// "listId" +// ); + +// CREATE TABLE "my_list_music_info_order" ( +// "listId" TEXT NOT NULL, +// "musicInfoId" TEXT NOT NULL, +// "order" INTEGER NOT NULL +// ); +// CREATE INDEX "index_my_list_music_info_order" ON "my_list_music_info_order" ( +// "listId", +// "musicInfoId" +// ); + +// CREATE TABLE "music_info_other_source" ( +// "source_id" TEXT NOT NULL, +// "id" TEXT NOT NULL, +// "source" TEXT NOT NULL, +// "name" TEXT NOT NULL, +// "singer" TEXT NOT NULL, +// "meta" TEXT NOT NULL, +// "order" INTEGER NOT NULL, +// UNIQUE("source_id","id") +// ); +// CREATE INDEX "index_music_info_other_source" ON "music_info_other_source" ( +// "source_id", +// "id" +// ); + +// -- TODO "meta" TEXT NOT NULL, +// CREATE TABLE "lyric" ( +// "id" TEXT NOT NULL, +// "source" TEXT NOT NULL, +// "type" TEXT NOT NULL, +// "text" TEXT NOT NULL +// ); + +// CREATE TABLE "music_url" ( +// "id" TEXT NOT NULL, +// "url" TEXT NOT NULL +// ); + +// CREATE TABLE "download_list" ( +// "id" TEXT NOT NULL, +// "isComplate" INTEGER NOT NULL, +// "status" TEXT NOT NULL, +// "statusText" TEXT NOT NULL, +// "progress_downloaded" INTEGER NOT NULL, +// "progress_total" INTEGER NOT NULL, +// "url" TEXT, +// "quality" TEXT NOT NULL, +// "ext" TEXT NOT NULL, +// "fileName" TEXT NOT NULL, +// "filePath" TEXT NOT NULL, +// "musicInfo" TEXT NOT NULL, +// "position" INTEGER NOT NULL, +// PRIMARY KEY("id") +// ); +// ` + +// export const tables = [ +// 'table_db_info', +// 'table_my_list', +// 'table_my_list_music_info', +// 'index_index_my_list_music_info', +// 'table_my_list_music_info_order', +// 'index_index_my_list_music_info_order', +// 'table_music_info_other_source', +// 'index_index_music_info_other_source', +// 'table_lyric', +// 'table_music_url', +// 'table_download_list', +// ] + +type Tables = 'db_info' +| 'my_list' +| 'my_list_music_info' +| 'index_my_list_music_info' +| 'my_list_music_info_order' +| 'index_my_list_music_info_order' +| 'music_info_other_source' +| 'index_music_info_other_source' +| 'lyric' +| 'music_url' +| 'download_list' +| 'dislike_list' + +const tables = new Map() + + +tables.set('db_info', ` + CREATE TABLE "db_info" ( + "id" INTEGER NOT NULL UNIQUE, + "field_name" TEXT, + "field_value" TEXT, + PRIMARY KEY("id" AUTOINCREMENT) + ); +`) +tables.set('my_list', ` + CREATE TABLE "my_list" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "source" TEXT, + "sourceListId" TEXT, + "position" INTEGER NOT NULL, + "locationUpdateTime" INTEGER, + PRIMARY KEY("id") + ); +`) +tables.set('my_list_music_info', ` + CREATE TABLE "my_list_music_info" ( + "id" TEXT NOT NULL, + "listId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "singer" TEXT NOT NULL, + "source" TEXT NOT NULL, + "interval" TEXT, + "meta" TEXT NOT NULL, + UNIQUE("id","listId") + ); +`) +tables.set('index_my_list_music_info', ` + CREATE INDEX "index_my_list_music_info" ON "my_list_music_info" ( + "id", + "listId" + ); +`) +tables.set('my_list_music_info_order', ` + CREATE TABLE "my_list_music_info_order" ( + "listId" TEXT NOT NULL, + "musicInfoId" TEXT NOT NULL, + "order" INTEGER NOT NULL + ); +`) +tables.set('index_my_list_music_info_order', ` + CREATE INDEX "index_my_list_music_info_order" ON "my_list_music_info_order" ( + "listId", + "musicInfoId" + ); +`) +tables.set('music_info_other_source', ` + CREATE TABLE "music_info_other_source" ( + "source_id" TEXT NOT NULL, + "id" TEXT NOT NULL, + "source" TEXT NOT NULL, + "name" TEXT NOT NULL, + "singer" TEXT NOT NULL, + "meta" TEXT NOT NULL, + "order" INTEGER NOT NULL, + UNIQUE("source_id","id") + ); +`) +tables.set('index_music_info_other_source', ` + CREATE INDEX "index_music_info_other_source" ON "music_info_other_source" ( + "source_id", + "id" + ); +`) +tables.set('lyric', ` + -- TODO "meta" TEXT NOT NULL, + CREATE TABLE "lyric" ( + "id" TEXT NOT NULL, + "source" TEXT NOT NULL, + "type" TEXT NOT NULL, + "text" TEXT NOT NULL + ); +`) +tables.set('music_url', ` + CREATE TABLE "music_url" ( + "id" TEXT NOT NULL, + "url" TEXT NOT NULL + ); +`) +tables.set('download_list', ` + CREATE TABLE "download_list" ( + "id" TEXT NOT NULL, + "isComplate" INTEGER NOT NULL, + "status" TEXT NOT NULL, + "statusText" TEXT NOT NULL, + "progress_downloaded" INTEGER NOT NULL, + "progress_total" INTEGER NOT NULL, + "url" TEXT, + "quality" TEXT NOT NULL, + "ext" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "filePath" TEXT NOT NULL, + "musicInfo" TEXT NOT NULL, + "position" INTEGER NOT NULL, + PRIMARY KEY("id") + ); +`) +tables.set('dislike_list', ` + CREATE TABLE "dislike_list" ( + "type" TEXT NOT NULL, + "content" TEXT NOT NULL, + "meta" TEXT + ); +`) + +export default tables + +export const DB_VERSION = '2' diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/verifyDB.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/verifyDB.ts new file mode 100644 index 000000000..cb937491c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/dbService/verifyDB.ts @@ -0,0 +1,29 @@ +import type Database from 'better-sqlite3' +import tables from './tables' + +const rxp = /\n|\s|;|--.+/g +export default (db: Database.Database) => { + const result = db.prepare<[]>('SELECT type,name,tbl_name,sql FROM "main".sqlite_master WHERE sql NOT NULL;').all() as Array<{ type: string, name: string, tbl_name: string, sql: string }> + const dbTableMap = new Map() + for (const info of result) dbTableMap.set(info.name, info.sql.replace(rxp, '')) + return Array.from(tables.entries()).every(([name, sql]) => { + const dbSql = dbTableMap.get(name) + // if (!(dbSql && dbSql == sql.replace(rxp, ''))) { + // console.log('dbSql:', dbSql, '\nsql:', sql.replace(rxp, '')) + // } + // return true + return dbSql && dbSql == sql.replace(rxp, '') + }) + // console.log(dbTableMap) + // for (const [name, sql] of tables.entries()) { + // const dbSql = dbTableMap.get(name) + // if (dbSql) { + // if (dbSql == sql.replace(rxp, '')) continue + // console.log(dbSql) + // console.log(sql.replace(rxp, '')) + // } else { + // console.log(name) + // } + // } + // if (result.every((info) => { tables.includes() })) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/index.ts new file mode 100644 index 000000000..bd9aca90b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/index.ts @@ -0,0 +1,9 @@ +import { createDBServiceWorker } from './utils' + + +export default () => { + return { + dbService: createDBServiceWorker(), + } +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/utils/index.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/utils/index.ts new file mode 100644 index 000000000..845f110c9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/utils/index.ts @@ -0,0 +1,15 @@ +import { Worker } from 'node:worker_threads' +import * as Comlink from 'comlink' +import nodeEndpoint from 'comlink/dist/esm/node-adapter' + +export type DBSeriveTypes = Comlink.Remote + +export const createDBServiceWorker = () => { + const worker: Worker = new Worker(new URL( + /* webpackChunkName: 'dbService.worker' */ + '../dbService', + import.meta.url, + )) + return Comlink.wrap(nodeEndpoint(worker)) +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/main/worker/utils/worker.ts b/plugins/lx-music-ztools/vendor/lx/src/main/worker/utils/worker.ts new file mode 100644 index 000000000..9854f5b8b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/main/worker/utils/worker.ts @@ -0,0 +1,9 @@ +import worker from 'node:worker_threads' +import * as Comlink from 'comlink' +import nodeEndpoint from 'comlink/dist/esm/node-adapter' + + +export const exposeWorker = (obj: any) => { + if (worker.parentPort == null) return + Comlink.expose(obj, nodeEndpoint(worker.parentPort)) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/.eslintrc.cjs b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/.eslintrc.cjs new file mode 100644 index 000000000..47436f7da --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/.eslintrc.cjs @@ -0,0 +1,17 @@ +/* eslint-env node */ +const { base, html, typescript, vue } = require('../../.eslintrc.base.cjs') + +module.exports = { + root: true, + ...base, + overrides: [ + html, + vue, + { + ...typescript, + parserOptions: { + project: './tsconfig.json', + }, + }, + ], +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/App.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/App.vue new file mode 100644 index 000000000..c296753aa --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/App.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/animate.less b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/animate.less new file mode 100644 index 000000000..2258ad964 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/animate.less @@ -0,0 +1,1071 @@ +// https://daneden.github.io/animate.css/ + +@keyframes bounce { + from, + 20%, + 53%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 40%, + 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } +} +@keyframes flash { + from, + 50%, + to { + opacity: 1; + } + + 25%, + 75% { + opacity: 0; + } +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes rubberBand { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + + 65% { + -webkit-transform: scale3d(0.95, 1.05, 1); + transform: scale3d(0.95, 1.05, 1); + } + + 75% { + -webkit-transform: scale3d(1.05, 0.95, 1); + transform: scale3d(1.05, 0.95, 1); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes shake { + from, + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 10%, + 30%, + 50%, + 70%, + 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 20%, + 40%, + 60%, + 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } +} +@keyframes headShake { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 6.5% { + -webkit-transform: translateX(-6px) rotateY(-9deg); + transform: translateX(-6px) rotateY(-9deg); + } + + 18.5% { + -webkit-transform: translateX(5px) rotateY(7deg); + transform: translateX(5px) rotateY(7deg); + } + + 31.5% { + -webkit-transform: translateX(-3px) rotateY(-5deg); + transform: translateX(-3px) rotateY(-5deg); + } + + 43.5% { + -webkit-transform: translateX(2px) rotateY(3deg); + transform: translateX(2px) rotateY(3deg); + } + + 50% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@keyframes swing { + 20% { + -webkit-transform: rotate3d(0, 0, 1, 15deg); + transform: rotate3d(0, 0, 1, 15deg); + } + + 40% { + -webkit-transform: rotate3d(0, 0, 1, -10deg); + transform: rotate3d(0, 0, 1, -10deg); + } + + 60% { + -webkit-transform: rotate3d(0, 0, 1, 5deg); + transform: rotate3d(0, 0, 1, 5deg); + } + + 80% { + -webkit-transform: rotate3d(0, 0, 1, -5deg); + transform: rotate3d(0, 0, 1, -5deg); + } + + to { + -webkit-transform: rotate3d(0, 0, 1, 0deg); + transform: rotate3d(0, 0, 1, 0deg); + } +} +@keyframes tada { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 10%, + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + + 30%, + 50%, + 70%, + 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + + 40%, + 60%, + 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@keyframes wobble { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 15% { + -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + + 30% { + -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + + 45% { + -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + + 60% { + -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + + 75% { + -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +@keyframes jello { + from, + 11.1%, + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 22.2% { + -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); + transform: skewX(-12.5deg) skewY(-12.5deg); + } + + 33.3% { + -webkit-transform: skewX(6.25deg) skewY(6.25deg); + transform: skewX(6.25deg) skewY(6.25deg); + } + + 44.4% { + -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); + transform: skewX(-3.125deg) skewY(-3.125deg); + } + + 55.5% { + -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); + transform: skewX(1.5625deg) skewY(1.5625deg); + } + + 66.6% { + -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); + transform: skewX(-0.78125deg) skewY(-0.78125deg); + } + + 77.7% { + -webkit-transform: skewX(0.390625deg) skewY(0.390625deg); + transform: skewX(0.390625deg) skewY(0.390625deg); + } + + 88.8% { + -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + } +} +@keyframes heartBeat { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 14% { + -webkit-transform: scale(1.3); + transform: scale(1.3); + } + + 28% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 42% { + -webkit-transform: scale(1.3); + transform: scale(1.3); + } + + 70% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes flipInX { + from { + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + animation-timing-function: ease-in; + } + + 60% { + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + + 80% { + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + + to { + transform: perspective(400px); + } +} +@keyframes flipOutX { + from { + transform: perspective(400px); + } + + 30% { + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + + to { + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} +@keyframes flipInY { + from { + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + animation-timing-function: ease-in; + } + + 60% { + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + + 80% { + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + + to { + transform: perspective(400px); + } +} +@keyframes flipOutY { + from { + transform: perspective(400px); + } + + 30% { + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + + to { + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} +@keyframes bounceIn { + from, + 20%, + 40%, + 60%, + 80%, + to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + 0% { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + + 20% { + transform: scale3d(1.1, 1.1, 1.1); + } + + 40% { + transform: scale3d(0.9, 0.9, 0.9); + } + + 60% { + opacity: 1; + transform: scale3d(1.03, 1.03, 1.03); + } + + 80% { + transform: scale3d(0.97, 0.97, 0.97); + } + + to { + opacity: 1; + transform: scale3d(1, 1, 1); + } +} +@keyframes bounceOut { + 20% { + transform: scale3d(0.9, 0.9, 0.9); + } + + 50%, + 55% { + opacity: 1; + transform: scale3d(1.1, 1.1, 1.1); + } + + to { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } +} +@keyframes lightSpeedIn { + from { + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + + 60% { + transform: skewX(20deg); + opacity: 1; + } + + 80% { + transform: skewX(-5deg); + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes lightSpeedOut { + from { + opacity: 1; + } + + to { + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} +@keyframes rotateIn { + from { + transform-origin: center; + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + + to { + transform-origin: center; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInDownLeft { + from { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + + to { + transform-origin: left bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInDownRight { + from { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + transform-origin: right bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInUpLeft { + from { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + transform-origin: left bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInUpRight { + from { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + + to { + transform-origin: right bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateOut { + from { + transform-origin: center; + opacity: 1; + } + + to { + transform-origin: center; + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} +@keyframes rotateOutDownLeft { + from { + transform-origin: left bottom; + opacity: 1; + } + + to { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} +@keyframes rotateOutDownRight { + from { + transform-origin: right bottom; + opacity: 1; + } + + to { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +@keyframes rotateOutUpLeft { + from { + transform-origin: left bottom; + opacity: 1; + } + + to { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +@keyframes rotateOutUpRight { + from { + transform-origin: right bottom; + opacity: 1; + } + + to { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} +@keyframes rollIn { + from { + opacity: 0; + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} +@keyframes rollOut { + from { + opacity: 1; + } + + to { + opacity: 0; + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} +@keyframes zoomIn { + from { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + + 50% { + opacity: 1; + } +} +@keyframes zoomInDown { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInLeft { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInRight { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInUp { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomOut { + from { + opacity: 1; + } + + 50% { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + + to { + opacity: 0; + } +} +@keyframes zoomOutDown { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + to { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + transform-origin: center bottom; + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomOutLeft { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + } + + to { + opacity: 0; + transform: scale(0.1) translate3d(-2000px, 0, 0); + transform-origin: left center; + } +} +@keyframes zoomOutRight { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + } + + to { + opacity: 0; + transform: scale(0.1) translate3d(2000px, 0, 0); + transform-origin: right center; + } +} +@keyframes zoomOutUp { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + to { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + transform-origin: center bottom; + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes slideInDown { + from { + transform: translate3d(0, -100%, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideInLeft { + from { + transform: translate3d(-100%, 0, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideInRight { + from { + transform: translate3d(100%, 0, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideInUp { + from { + transform: translate3d(0, 100%, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideOutDown { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(0, 100%, 0); + } +} +@keyframes slideOutLeft { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(-100%, 0, 0); + } +} +@keyframes slideOutRight { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(100%, 0, 0); + } +} +@keyframes slideOutUp { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(0, -100%, 0); + } +} +@keyframes jackInTheBox { + from { + opacity: 0; + transform: scale(0.1) rotate(30deg); + transform-origin: center bottom; + } + + 50% { + transform: rotate(-10deg); + } + + 70% { + transform: rotate(3deg); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +.flipInX { + backface-visibility: visible !important; + animation-name: flipInX; +} +.flipInY { + backface-visibility: visible !important; + animation-name: flipInY; +} +.fadeIn { + animation-name: fadeIn; +} +.bounceIn { + animation-duration: 0.75s; + animation-name: bounceIn; +} +.lightSpeedIn { + animation-name: lightSpeedIn; + animation-timing-function: ease-out; +} +.rotateIn { + animation-name: rotateIn; +} +.rotateInDownLeft { + animation-name: rotateInDownLeft; +} +.rotateInDownRight { + animation-name: rotateInDownRight; +} +.rotateInUpLeft { + animation-name: rotateInUpLeft; +} +.rotateInUpRight { + animation-name: rotateInUpRight; +} +.rollIn { + animation-name: rollIn; +} +.zoomIn { + animation-name: zoomIn; +} +.zoomInDown { + animation-name: zoomInDown; +} +.zoomInLeft { + animation-name: zoomInLeft; +} +.zoomInRight { + animation-name: zoomInRight; +} +.zoomInUp { + animation-name: zoomInUp; +} +.slideInDown { + animation-name: slideInDown; +} +.slideInLeft { + animation-name: slideInLeft; +} +.slideInRight { + animation-name: slideInRight; +} +.slideInUp { + animation-name: slideInUp; +} +.jackInTheBox { + -webkit-animation-name: jackInTheBox; + animation-name: jackInTheBox; +} + + + +.flipOutX { + animation-duration: 0.75s; + animation-name: flipOutX; + backface-visibility: visible !important; +} +.flipOutY { + animation-duration: 0.75s; + backface-visibility: visible !important; + animation-name: flipOutY; +} +.fadeOut { + animation-name: fadeOut; +} +.bounceOut { + animation-duration: 0.75s; + animation-name: bounceOut; +} +.lightSpeedOut { + animation-name: lightSpeedOut; + animation-timing-function: ease-in; +} +.rotateOut { + animation-name: rotateOut; +} +.rotateOutDownLeft { + animation-name: rotateOutDownLeft; +} +.rotateOutDownRight { + animation-name: rotateOutDownRight; +} +.rotateOutUpLeft { + animation-name: rotateOutUpLeft; +} +.rotateOutUpRight { + animation-name: rotateOutUpRight; +} +.hinge { + animation-duration: 2s; + animation-name: hinge; +} +.rollOut { + animation-name: rollOut; +} +.zoomOut { + animation-name: zoomOut; +} +.zoomOutDown { + animation-name: zoomOutDown; +} +.zoomOutLeft { + animation-name: zoomOutLeft; +} +.zoomOutRight { + animation-name: zoomOutRight; +} +.zoomOutUp { + animation-name: zoomOutUp; +} +.slideOutDown { + animation-name: slideOutDown; +} +.slideOutLeft { + animation-name: slideOutLeft; +} +.slideOutRight { + animation-name: slideOutRight; +} +.slideOutUp { + animation-name: slideOutUp; +} + + +.bounce { + animation-name: bounce; + transform-origin: center bottom; +} +.flash { + animation-name: flash; +} +.pulse { + animation-name: pulse; +} +.rubberBand { + animation-name: rubberBand; +} +.shake { + animation-name: shake; +} +.headShake { + animation-timing-function: ease-in-out; + animation-name: headShake; +} +.swing { + transform-origin: top center; + animation-name: swing; +} +.tada { + animation-name: tada; +} +.wobble { + animation-name: wobble; +} +.jello { + animation-name: jello; + transform-origin: center; +} +.heartBeat { + animation-name: heartBeat; + animation-duration: 1.3s; + animation-timing-function: ease-in-out; +} + +.animated { + animation-duration: 0.5s; + animation-fill-mode: both; +} + +.animated-slow { + animation-duration: 0.8s; + animation-fill-mode: both; +} + +.animated-fast { + animation-duration: 0.3s; + animation-fill-mode: both; +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/colors.less b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/colors.less new file mode 100644 index 000000000..ee810288e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/colors.less @@ -0,0 +1,325 @@ +@red-50: #ffebee; +@red-100: #ffcdd2; +@red-200: #ef9a9a; +@red-300: #e57373; +@red-400: #ef5350; +@red-500: #f44336; +@red-600: #e53935; +@red-700: #d32f2f; +@red-800: #c62828; +@red-900: #b71c1c; +@red-A100: #ff8a80; +@red-A200: #ff5252; +@red-A400: #ff1744; +@red-A700: #d50000; +@red: @red-500; + + +@pink-50: #fce4ec; +@pink-100: #f8bbd0; +@pink-200: #f48fb1; +@pink-300: #f06292; +@pink-400: #ec407a; +@pink-500: #e91e63; +@pink-600: #d81b60; +@pink-700: #c2185b; +@pink-800: #ad1457; +@pink-900: #880e4f; +@pink-A100: #ff80ab; +@pink-A200: #ff4081; +@pink-A400: #f50057; +@pink-A700: #c51162; +@pink: @pink-500; + + +@purple-50: #f3e5f5; +@purple-100: #e1bee7; +@purple-200: #ce93d8; +@purple-300: #ba68c8; +@purple-400: #ab47bc; +@purple-500: #9c27b0; +@purple-600: #8e24aa; +@purple-700: #7b1fa2; +@purple-800: #6a1b9a; +@purple-900: #4a148c; +@purple-A100: #ea80fc; +@purple-A200: #e040fb; +@purple-A400: #d500f9; +@purple-A700: #aa00ff; +@purple: @purple-500; + + +@deep-purple-50: #ede7f6; +@deep-purple-100: #d1c4e9; +@deep-purple-200: #b39ddb; +@deep-purple-300: #9575cd; +@deep-purple-400: #7e57c2; +@deep-purple-500: #673ab7; +@deep-purple-600: #5e35b1; +@deep-purple-700: #512da8; +@deep-purple-800: #4527a0; +@deep-purple-900: #311b92; +@deep-purple-A100: #b388ff; +@deep-purple-A200: #7c4dff; +@deep-purple-A400: #651fff; +@deep-purple-A700: #6200ea; +@deep-purple: @deep-purple-500; + + +@indigo-50: #e8eaf6; +@indigo-100: #c5cae9; +@indigo-200: #9fa8da; +@indigo-300: #7986cb; +@indigo-400: #5c6bc0; +@indigo-500: #3f51b5; +@indigo-600: #3949ab; +@indigo-700: #303f9f; +@indigo-800: #283593; +@indigo-900: #1a237e; +@indigo-A100: #8c9eff; +@indigo-A200: #536dfe; +@indigo-A400: #3d5afe; +@indigo-A700: #304ffe; +@indigo: @indigo-500; + + +@blue-50: #e3f2fd; +@blue-100: #bbdefb; +@blue-200: #90caf9; +@blue-300: #64b5f6; +@blue-400: #42a5f5; +@blue-500: #2196f3; +@blue-600: #1e88e5; +@blue-700: #1976d2; +@blue-800: #1565c0; +@blue-900: #0d47a1; +@blue-A100: #82b1ff; +@blue-A200: #448aff; +@blue-A400: #2979ff; +@blue-A700: #2962ff; +@blue: @blue-500; + + +@light-blue-50: #e1f5fe; +@light-blue-100: #b3e5fc; +@light-blue-200: #81d4fa; +@light-blue-300: #4fc3f7; +@light-blue-400: #29b6f6; +@light-blue-500: #03a9f4; +@light-blue-600: #039be5; +@light-blue-700: #0288d1; +@light-blue-800: #0277bd; +@light-blue-900: #01579b; +@light-blue-A100: #80d8ff; +@light-blue-A200: #40c4ff; +@light-blue-A400: #00b0ff; +@light-blue-A700: #0091ea; +@light-blue: @light-blue-500; + + +@cyan-50: #e0f7fa; +@cyan-100: #b2ebf2; +@cyan-200: #80deea; +@cyan-300: #4dd0e1; +@cyan-400: #26c6da; +@cyan-500: #00bcd4; +@cyan-600: #00acc1; +@cyan-700: #0097a7; +@cyan-800: #00838f; +@cyan-900: #006064; +@cyan-A100: #84ffff; +@cyan-A200: #18ffff; +@cyan-A400: #00e5ff; +@cyan-A700: #00b8d4; +@cyan: @cyan-500; + + +@teal-50: #e0f2f1; +@teal-100: #b2dfdb; +@teal-200: #80cbc4; +@teal-300: #4db6ac; +@teal-400: #26a69a; +@teal-500: #009688; +@teal-600: #00897b; +@teal-700: #00796b; +@teal-800: #00695c; +@teal-900: #004d40; +@teal-A100: #a7ffeb; +@teal-A200: #64ffda; +@teal-A400: #1de9b6; +@teal-A700: #00bfa5; +@teal: @teal-500; + + +@green-50: #e8f5e9; +@green-100: #c8e6c9; +@green-200: #a5d6a7; +@green-300: #81c784; +@green-400: #66bb6a; +@green-500: #4caf50; +@green-600: #43a047; +@green-700: #388e3c; +@green-800: #2e7d32; +@green-900: #1b5e20; +@green-A100: #b9f6ca; +@green-A200: #69f0ae; +@green-A400: #00e676; +@green-A700: #00c853; +@green: @green-500; + + +@light-green-50: #f1f8e9; +@light-green-100: #dcedc8; +@light-green-200: #c5e1a5; +@light-green-300: #aed581; +@light-green-400: #9ccc65; +@light-green-500: #8bc34a; +@light-green-600: #7cb342; +@light-green-700: #689f38; +@light-green-800: #558b2f; +@light-green-900: #33691e; +@light-green-A100: #ccff90; +@light-green-A200: #b2ff59; +@light-green-A400: #76ff03; +@light-green-A700: #64dd17; +@light-green: @light-green-500; + + +@lime-50: #f9fbe7; +@lime-100: #f0f4c3; +@lime-200: #e6ee9c; +@lime-300: #dce775; +@lime-400: #d4e157; +@lime-500: #cddc39; +@lime-600: #c0ca33; +@lime-700: #afb42b; +@lime-800: #9e9d24; +@lime-900: #827717; +@lime-A100: #f4ff81; +@lime-A200: #eeff41; +@lime-A400: #c6ff00; +@lime-A700: #aeea00; +@lime: @lime-500; + + +@yellow-50: #fffde7; +@yellow-100: #fff9c4; +@yellow-200: #fff59d; +@yellow-300: #fff176; +@yellow-400: #ffee58; +@yellow-500: #fec60a; +@yellow-600: #fdd835; +@yellow-700: #fbc02d; +@yellow-800: #f9a825; +@yellow-900: #f57f17; +@yellow-A100: #ffff8d; +@yellow-A200: #ffff00; +@yellow-A400: #ffea00; +@yellow-A700: #ffd600; +@yellow: @yellow-700; + + +@amber-50: #fff8e1; +@amber-100: #ffecb3; +@amber-200: #ffe082; +@amber-300: #ffd54f; +@amber-400: #ffca28; +@amber-500: #ffc107; +@amber-600: #ffb300; +@amber-700: #ffa000; +@amber-800: #ff8f00; +@amber-900: #ff6f00; +@amber-A100: #ffe57f; +@amber-A200: #ffd740; +@amber-A400: #ffc400; +@amber-A700: #ffab00; +@amber: @amber-500; + + +@orange-50: #fff3e0; +@orange-100: #ffe0b2; +@orange-200: #ffcc80; +@orange-300: #ffb74d; +@orange-400: #ffa726; +@orange-500: #ff9800; +@orange-600: #fb8c00; +@orange-700: #f57c00; +@orange-800: #ef6c00; +@orange-900: #e65100; +@orange-A100: #ffd180; +@orange-A200: #ffab40; +@orange-A400: #ff9100; +@orange-A700: #ff6d00; +@orange: @orange-500; + + +@deep-orange-50: #fbe9e7; +@deep-orange-100: #ffccbc; +@deep-orange-200: #ffab91; +@deep-orange-300: #ff8a65; +@deep-orange-400: #ff7043; +@deep-orange-500: #ff5722; +@deep-orange-600: #f4511e; +@deep-orange-700: #e64a19; +@deep-orange-800: #d84315; +@deep-orange-900: #bf360c; +@deep-orange-A100: #ff9e80; +@deep-orange-A200: #ff6e40; +@deep-orange-A400: #ff3d00; +@deep-orange-A700: #dd2c00; +@deep-orange: @deep-orange-500; + + +@brown-50: #efebe9; +@brown-100: #d7ccc8; +@brown-200: #bcaaa4; +@brown-300: #a1887f; +@brown-400: #8d6e63; +@brown-500: #795548; +@brown-600: #6d4c41; +@brown-700: #5d4037; +@brown-800: #4e342e; +@brown-900: #3e2723; +@brown-A100: #d7ccc8; +@brown-A200: #bcaaa4; +@brown-A400: #8d6e63; +@brown-A700: #5d4037; +@brown: @brown-500; + + +@grey-50: #fafafa; +@grey-100: #f5f5f5; +@grey-200: #eeeeee; +@grey-300: #e0e0e0; +@grey-400: #bdbdbd; +@grey-500: #9e9e9e; @rgb-grey-500: "158, 158, 158"; +@grey-600: #757575; +@grey-700: #616161; +@grey-800: #424242; +@grey-900: #212121; +@grey-A100: #f5f5f5; +@grey-A200: #eeeeee; +@grey-A400: #bdbdbd; +@grey-A700: #616161; +@grey: @grey-500; + + +@blue-grey-50: #eceff1; +@blue-grey-100: #cfd8dc; +@blue-grey-200: #b0bec5; +@blue-grey-300: #90a4ae; +@blue-grey-400: #78909c; +@blue-grey-500: #607d8b; +@blue-grey-600: #546e7a; +@blue-grey-700: #455a64; +@blue-grey-800: #37474f; +@blue-grey-900: #263238; +@blue-grey-A100: #cfd8dc; +@blue-grey-A200: #b0bec5; +@blue-grey-A400: #78909c; +@blue-grey-A700: #455a64; +@blue-grey: @blue-grey-500; + + +@black: #000000; @rgb-black: "0,0,0"; +@white: #ffffff; @rgb-white: "255,255,255"; diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/index.less b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/index.less new file mode 100644 index 000000000..d11e7bf0d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/index.less @@ -0,0 +1,63 @@ +@import './reset.less'; +@import './animate.less'; +*, *::after, *::before { + -webkit-user-drag: none; +} + +html { + font-size: 16px; +} + +:root { + --color-lyric-unplay: rgba(255, 255, 255, 1); + --color-lyric-played: rgba(7, 197, 86, 1); + --color-lyric-shadow: rgba(0, 0, 0, 0.14); + --color-lyric-shadow-font-mode: rgba(0, 0, 0, 0.07); +} + +.nobreak { + white-space: nowrap; +} + +.auto-hidden { + .mixin-ellipsis-1(); +} + +.center { + text-align: center; +} + +.break { + word-break: break-all; +} + +.select { + user-select: text; +} +.no-select { + user-select: none; +} + +a { + color: #000; +} + + +small { + font-size: .8em; +} +.small { + font-size: .9em; +} +strong { + font-weight: bold; +} + +.underline { + text-decoration: underline; +} + +svg { + transition: @transition-theme; + transition-property: fill; +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/layout.less b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/layout.less new file mode 100644 index 000000000..9c6316930 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/layout.less @@ -0,0 +1,203 @@ +@import './variables.less'; + + +/*自动隐藏文字*/ +.mixin-ellipsis-1() { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.mixin-ellipsis(@n: 1) { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + word-break: break-all; + white-space: normal !important; + -webkit-line-clamp: @n; + -webkit-box-orient: vertical; +} +.mixin-ellipsis-2() { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + word-break: break-all; + white-space: normal !important; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.stroke(@stroke, @color) { + @maxi: @stroke + 1; + .i-loop (@i) when (@i > 0) { + @maxj: @stroke + 1; + .j-loop (@j) when (@j > 0) { + text-shadow+: (@i - 1)*(1px) (@j - 1)*(1px) 0 @color; + text-shadow+: (@i - 1)*(1px) (@j - 1)*(-1px) 0 @color; + text-shadow+: (@i - 1)*(-1px) (@j - 1)*(-1px) 0 @color; + text-shadow+: (@i - 1)*(-1px) (@j - 1)*(1px) 0 @color; + .j-loop(@j - 1); + } + .j-loop (0) {} + .j-loop(@maxj); + .i-loop(@i - 1); + } + .i-loop (0) {} + .i-loop(@maxi); + text-shadow+: 0 0 0 @color; +} + +.stroke4(@color) { + text-shadow: + 0.02em -0.02em 0 var(--color-lyric-shadow), + // -0.02em 0.02em 0 var(--color-lyric-shadow), + // 0.02em 0.02em 0 var(--color-lyric-shadow), + // 0.02em -0.02em 0 var(--color-lyric-shadow), + -0.02em -1px 0 var(--color-lyric-shadow), + -0.02em 1px 0 var(--color-lyric-shadow), + 0.02em 1px 0 var(--color-lyric-shadow), + 0.02em -1px 0 var(--color-lyric-shadow), + -0.02em 0px 0 var(--color-lyric-shadow), + -0.02em 0px 0 var(--color-lyric-shadow), + 0.02em 0px 0 var(--color-lyric-shadow), + 0.02em 0px 0 var(--color-lyric-shadow), + // -1px -0.02em 0 var(--color-lyric-shadow), + // -1px 0.02em 0 var(--color-lyric-shadow), + // 1px 0.02em 0 var(--color-lyric-shadow), + // 1px -0.02em 0 var(--color-lyric-shadow), + -1px -1px 0 var(--color-lyric-shadow), + -1px 1px 0 var(--color-lyric-shadow), + 1px 1px 0 var(--color-lyric-shadow), + 1px -1px 0 var(--color-lyric-shadow), + -1px 0px 0 var(--color-lyric-shadow), + -1px 0px 0 var(--color-lyric-shadow), + 1px 0px 0 var(--color-lyric-shadow), + 1px 0px 0 var(--color-lyric-shadow) + // 0px -0.02em 0 var(--color-lyric-shadow), + // 0px 0.02em 0 var(--color-lyric-shadow), + // 0px 0.02em 0 var(--color-lyric-shadow), + // 0px -0.02em 0 var(--color-lyric-shadow), + // 0px -1px 0 var(--color-lyric-shadow), + // 0px 1px 0 var(--color-lyric-shadow), + // 0px 1px 0 var(--color-lyric-shadow), + // 0px -1px 0 var(--color-lyric-shadow) + ; + // text-shadow: + // -0.04em -0.04em 0 var(--color-lyric-shadow), + // // -0.04em 0.04em 0 var(--color-lyric-shadow), + // 0.04em 0.04em 0 var(--color-lyric-shadow), + // 0.04em -0.04em 0 var(--color-lyric-shadow), + // -0.04em 0px 0 var(--color-lyric-shadow), + // -0.04em 0px 0 var(--color-lyric-shadow), + // 0.04em 0px 0 var(--color-lyric-shadow), + // 0.04em 0px 0 var(--color-lyric-shadow), + // // 0px -0.03em 0 var(--color-lyric-shadow), + // 0px 0.04em 0 var(--color-lyric-shadow), + // // 0px 0.03em 0 var(--color-lyric-shadow), + // 0px -0.04em 0 var(--color-lyric-shadow); + + // @maxi: 2 + 1; + // .i-loop (@i) when (@i > 0) { + // @maxj: 2 + 1; + // .j-loop (@j) when (@j > 0) { + // text-shadow+: (@i - 1)*(-1px) (@j - 1)*(-1px) 0 @color; + // text-shadow+: (@i - 1)*(-1px) (@j - 1)*(1px) 0 @color; + // text-shadow+: (@i - 1)*(1px) (@j - 1)*(1px) 0 @color; + // text-shadow+: (@i - 1)*(1px) (@j - 1)*(-1px) 0 @color; + // .j-loop(@j - 1); + // } + // .j-loop (0) {} + // .j-loop(@maxj); + // .i-loop(@i - 1); + // } + // .i-loop (0) {} + // .i-loop(@maxi); + // text-shadow+: 0 0 0 @color; +} + +.stroke3(@color) { + text-shadow: 0.04em 0.04em 0 var(--color-lyric-shadow-font-mode), + 0.04em -0.03em 0 var(--color-lyric-shadow-font-mode), + -0.04em -0.03em 0 var(--color-lyric-shadow-font-mode), + -0.04em 0.04em 0 var(--color-lyric-shadow-font-mode), + 0.04em 0.01em 0 var(--color-lyric-shadow-font-mode), + 0.04em -0.01em 0 var(--color-lyric-shadow-font-mode), + -0.04em -0.01em 0 var(--color-lyric-shadow-font-mode), + -0.04em 0.01em 0 var(--color-lyric-shadow-font-mode), + 0.04em 0px 0 var(--color-lyric-shadow-font-mode), + 0.04em 0px 0 var(--color-lyric-shadow-font-mode), + -0.04em 0px 0 var(--color-lyric-shadow-font-mode), + -0.04em 0px 0 var(--color-lyric-shadow-font-mode), + 0.01em 0.04em 0 var(--color-lyric-shadow-font-mode), + 0.01em -0.03em 0 var(--color-lyric-shadow-font-mode), + -0.01em -0.03em 0 var(--color-lyric-shadow-font-mode), + -0.01em 0.04em 0 var(--color-lyric-shadow-font-mode), + 0.01em 0.01em 0 var(--color-lyric-shadow-font-mode), + 0.01em -0.01em 0 var(--color-lyric-shadow-font-mode), + -0.01em -0.01em 0 var(--color-lyric-shadow-font-mode), + -0.01em 0.01em 0 var(--color-lyric-shadow-font-mode), + 0.01em 0px 0 var(--color-lyric-shadow-font-mode), + 0.01em 0px 0 var(--color-lyric-shadow-font-mode), + -0.01em 0px 0 var(--color-lyric-shadow-font-mode), + -0.01em 0px 0 var(--color-lyric-shadow-font-mode), + 0px 0.04em 0 var(--color-lyric-shadow-font-mode), + 0px -0.03em 0 var(--color-lyric-shadow-font-mode), + 0px -0.03em 0 var(--color-lyric-shadow-font-mode), + 0px 0.04em 0 var(--color-lyric-shadow-font-mode), + 0px 0.01em 0 var(--color-lyric-shadow-font-mode), + 0px -0.01em 0 var(--color-lyric-shadow-font-mode), + 0px -0.01em 0 var(--color-lyric-shadow-font-mode), + 0px 0.01em 0 var(--color-lyric-shadow-font-mode); + // text-shadow: 0.04em 0.04em 0 var(--color-lyric-shadow-font-mode), + // 0.04em -0.03em 0 var(--color-lyric-shadow-font-mode), + // -0.04em -0.03em 0 var(--color-lyric-shadow-font-mode), + // -0.04em 0.04em 0 var(--color-lyric-shadow-font-mode), + // 0.04em 0.01em 0 var(--color-lyric-shadow-font-mode), + // 0.04em -0.02em 0 var(--color-lyric-shadow-font-mode), + // -0.04em -0.02em 0 var(--color-lyric-shadow-font-mode), + // -0.04em 0.01em 0 var(--color-lyric-shadow-font-mode), + // 0.04em 0px 0 var(--color-lyric-shadow-font-mode), + // 0.04em 0px 0 var(--color-lyric-shadow-font-mode), + // -0.04em 0px 0 var(--color-lyric-shadow-font-mode), + // -0.04em 0px 0 var(--color-lyric-shadow-font-mode), + // 0.01em 0.04em 0 var(--color-lyric-shadow-font-mode), + // 0.01em -0.03em 0 var(--color-lyric-shadow-font-mode), + // -0.02em -0.03em 0 var(--color-lyric-shadow-font-mode), + // -0.02em 0.04em 0 var(--color-lyric-shadow-font-mode), + // 0.01em 0.01em 0 var(--color-lyric-shadow-font-mode), + // 0.01em -0.02em 0 var(--color-lyric-shadow-font-mode), + // -0.02em -0.02em 0 var(--color-lyric-shadow-font-mode), + // -0.02em 0.01em 0 var(--color-lyric-shadow-font-mode), + // 0.01em 0px 0 var(--color-lyric-shadow-font-mode), + // 0.01em 0px 0 var(--color-lyric-shadow-font-mode), + // -0.02em 0px 0 var(--color-lyric-shadow-font-mode), + // -0.02em 0px 0 var(--color-lyric-shadow-font-mode), + // 0px 0.04em 0 var(--color-lyric-shadow-font-mode), + // 0px -0.03em 0 var(--color-lyric-shadow-font-mode), + // 0px -0.03em 0 var(--color-lyric-shadow-font-mode), + // 0px 0.04em 0 var(--color-lyric-shadow-font-mode), + // 0px 0.01em 0 var(--color-lyric-shadow-font-mode), + // 0px -0.02em 0 var(--color-lyric-shadow-font-mode), + // 0px -0.02em 0 var(--color-lyric-shadow-font-mode), + // 0px 0.01em 0 var(--color-lyric-shadow-font-mode); +} + +.stroke2(@color) { + text-shadow: 2px 0 @color, -2px 0 @color, 0 2px @color, 0 -2px @color, + 1px 1px @color, -1px -1px @color, 1px -1px @color, -1px 1px @color; +} +// .stroke2(@color) { +// // text-shadow: 1px 1px 2px @color; +// // text-shadow: +// // -1px -1px 0px @color, +// // 0px -1px 0px @color, +// // 1px -1px 0px @color, +// // -1px 0px 0px @color, +// // 1px 0px 0px @color, +// // -1px 1px 0px @color, +// // 0px 1px 0px @color, +// // 1px 1px 0px @color; +// // -webkit-text-stroke: 0.03em black; +// // -webkit-text-fill-color: @color; +// } diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/reset.less b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/reset.less new file mode 100644 index 000000000..1ee9db017 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/reset.less @@ -0,0 +1,73 @@ +// https://github.com/microsoft/vscode/blob/2dd0bca3954d4c03c427d6b447205b68817bd000/src/vs/workbench/browser/media/style.css +/* Font Families (with CJK support) */ + +.windows { font-family: "Segoe WPC", "Segoe UI", sans-serif; } +.windows:lang(zh-Hans) { font-family:"Microsoft YaHei", "Segoe WPC", "Segoe UI", sans-serif; } +.windows:lang(zh-Hant) { font-family:"Microsoft Jhenghei", "Segoe WPC", "Segoe UI", sans-serif; } +.windows:lang(ja) { font-family:"Yu Gothic UI", "Meiryo UI", "Segoe WPC", "Segoe UI", sans-serif; } +.windows:lang(ko) { font-family:"Malgun Gothic", "Dotom", "Segoe WPC", "Segoe UI", sans-serif; } + +.mac { font-family: -apple-system, BlinkMacSystemFont, sans-serif; } +.mac:lang(zh-Hans) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; } +.mac:lang(zh-Hant) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; } +.mac:lang(ja) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; } +.mac:lang(ko) { font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Nanum Gothic", "AppleGothic", sans-serif; } + +/* Linux: add `system-ui` as first font and not `Ubuntu` to allow other distribution pick their standard OS font */ +.linux { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; } +.linux:lang(zh-Hans) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } +.linux:lang(zh-Hant) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; } +.linux:lang(ja) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } +.linux:lang(ko) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } + + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + font-size: inherit; + vertical-align: baseline; +} +input, button, textarea { + font-family: inherit; +} + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +// html { +// } +body { + line-height: 1.2; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/variables.less b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/variables.less new file mode 100644 index 000000000..3bfbc680b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/assets/styles/variables.less @@ -0,0 +1,24 @@ +@import './colors.less'; + +// Width +@width-app-left: 6.6%; + +// Height +@height-toolbar: 54px; +@height-player: 60px; + + +// Shadow +@shadow-app: 8px; + + +// Radius +@radius-progress-border: 5px; +@radius-border: 4px; + +@transition-theme: .4s ease; +@transition-slow: .6s ease; +@transition-normal: .4s ease; +@transition-fast: .2s ease; + +@form-radius: 3px; diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/common/AudioVisualizer.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/common/AudioVisualizer.vue new file mode 100644 index 000000000..2fc20264f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/common/AudioVisualizer.vue @@ -0,0 +1,195 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/index.js new file mode 100644 index 000000000..b1d3a6c0c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/index.js @@ -0,0 +1,25 @@ +import upperFirst from 'lodash/upperFirst' +import camelCase from 'lodash/camelCase' + +const requireComponent = require.context('./', true, /\.vue$/) + +const vueFileRxp = /\.vue$/ + +export default app => { + requireComponent.keys().forEach(fileName => { + const filePath = fileName.replace(/^\.\//, '') + + if (!filePath.split('/').every((path, index, arr) => { + const char = path.charAt(0) + return vueFileRxp.test(path) || char.toUpperCase() !== char || arr[index + 1] == 'index.vue' + })) return + + const componentConfig = requireComponent(fileName) + + let componentName = upperFirst(camelCase(filePath.replace(/\.\w+$/, ''))) + + if (componentName.endsWith('Index')) componentName = componentName.replace(/Index$/, '') + + app.component(componentName, componentConfig.default || componentConfig) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/ControlBar.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/ControlBar.vue new file mode 100644 index 000000000..ce1c056bf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/ControlBar.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/Icons.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/Icons.vue new file mode 100644 index 000000000..6e9b9512e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/Icons.vue @@ -0,0 +1,59 @@ + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricHorizontal/index.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricHorizontal/index.vue new file mode 100644 index 000000000..e6bfea00c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricHorizontal/index.vue @@ -0,0 +1,280 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricHorizontal/useLyric.js b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricHorizontal/useLyric.js new file mode 100644 index 000000000..6bd3e2ea4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricHorizontal/useLyric.js @@ -0,0 +1,235 @@ +import { ref, onMounted, onBeforeUnmount, watch, nextTick } from '@common/utils/vueTools' +import { scrollTo } from '@common/utils/renderer' +import { lyric } from '@lyric/store/lyric' +import { isPlay, setting } from '@lyric/store/state' +import { setWindowBounds, setWindowResizeable } from '@lyric/utils/ipc' +import { isWin } from '@common/utils' + +const getOffsetTop = (contentHeight, lineHeight) => { + switch (setting['desktopLyric.scrollAlign']) { + case 'top': return 0 + default: return contentHeight * 0.5 - lineHeight / 2 + } +} + +export default (isComputeHeight) => { + const dom_lyric = ref(null) + const dom_lyric_text = ref(null) + const isMsDown = ref(false) + let isStopScroll = false + + const winEvent = { + isMsDown: false, + msDownX: 0, + msDownY: 0, + windowW: 0, + windowH: 0, + } + + let msDownY = 0 + let msDownScrollY = 0 + let timeout = null + let cancelScrollFn + let dom_lines + let line_heights + let isSetedLines = false + let prevActiveLine = 0 + + + const handleScrollLrc = (duration = 300) => { + if (!dom_lines?.length || !dom_lyric.value) return + if (isStopScroll) return + let dom_p = dom_lines[lyric.line] + + if (dom_p) { + let offset = 0 + if (isComputeHeight.value) { + let prevLineHeight = line_heights[prevActiveLine] ?? 0 + offset = prevActiveLine < lyric.line ? ((dom_lines[prevActiveLine]?.clientHeight ?? 0) - prevLineHeight) : 0 + // console.log(prevActiveLine, dom_lines[prevActiveLine]?.clientHeight ?? 0, prevLineHeight, offset) + } + cancelScrollFn = scrollTo(dom_lyric.value, dom_p ? (dom_p.offsetTop - offset - getOffsetTop(dom_lyric.value.clientHeight, dom_p.clientHeight)) : 0, duration) + } else { + cancelScrollFn = scrollTo(dom_lyric.value, 0, duration) + } + } + const clearLyricScrollTimeout = () => { + if (!timeout) return + clearTimeout(timeout) + timeout = null + } + const startLyricScrollTimeout = () => { + clearLyricScrollTimeout() + timeout = setTimeout(() => { + timeout = null + isStopScroll = false + if (!isPlay.value) return + handleScrollLrc() + }, 3000) + } + + const handleLyricDown = (target, x, y) => { + if (target.classList.contains('font-lrc') || + target.parentNode.classList.contains('font-lrc') || + target.classList.contains('extended') || + target.parentNode.classList.contains('extended') + ) { + if (delayScrollTimeout) { + clearTimeout(delayScrollTimeout) + delayScrollTimeout = null + } + isMsDown.value = true + msDownY = y + msDownScrollY = dom_lyric.value.scrollTop + } else { + winEvent.isMsDown = true + winEvent.msDownX = x + winEvent.msDownY = y + winEvent.windowW = window.innerWidth + winEvent.windowH = window.innerHeight + // https://github.com/lyswhut/lx-music-desktop/issues/2244 + if (isWin) setWindowResizeable(false) + } + } + const handleLyricMouseDown = event => { + handleLyricDown(event.target, event.clientX, event.clientY) + } + const handleLyricTouchStart = event => { + if (event.changedTouches.length) { + const touch = event.changedTouches[0] + handleLyricDown(event.target, touch.clientX, touch.clientY) + } + } + const handleMouseMsUp = () => { + isMsDown.value = false + winEvent.isMsDown = false + if (isWin) setWindowResizeable(true) + } + + const handleMove = (x, y) => { + if (isMsDown.value) { + isStopScroll ||= true + if (cancelScrollFn) { + cancelScrollFn() + cancelScrollFn = null + } + dom_lyric.value.scrollTop = msDownScrollY + msDownY - y + startLyricScrollTimeout() + } else if (winEvent.isMsDown) { + // https://github.com/lyswhut/lx-music-desktop/issues/2244 + if (isWin) { + setWindowBounds({ + x: x - winEvent.msDownX, + y: y - winEvent.msDownY, + w: winEvent.windowW, + h: winEvent.windowH, + }) + } else { + setWindowBounds({ + x: x - winEvent.msDownX, + y: y - winEvent.msDownY, + w: window.innerWidth, + h: window.innerHeight, + }) + } + } + } + const handleMouseMsMove = event => { + handleMove(event.clientX, event.clientY) + } + const handleTouchMove = (e) => { + if (e.changedTouches.length) { + const touch = e.changedTouches[0] + handleMove(touch.clientX, touch.clientY) + } + } + + const handleWheel = (event) => { + console.log(event.deltaY) + if (cancelScrollFn) { + cancelScrollFn() + cancelScrollFn = null + } + dom_lyric.value.scrollTop = dom_lyric.value.scrollTop + event.deltaY + startLyricScrollTimeout() + } + + const setLyric = (lines) => { + const dom_line_content = document.createDocumentFragment() + for (const line of lines) { + dom_line_content.appendChild(line.dom_line) + } + dom_lyric_text.value.textContent = '' + dom_lyric_text.value.appendChild(dom_line_content) + nextTick(() => { + dom_lines = dom_lyric.value.querySelectorAll('.line-content') + line_heights = Array.from(dom_lines).map(l => l.clientHeight) + handleScrollLrc() + }) + } + + const initLrc = (lines, oLines) => { + prevActiveLine = 0 + isSetedLines = true + if (oLines) { + if (lines.length) { + setLyric(lines) + } else { + cancelScrollFn = scrollTo(dom_lyric.value, 0, 300, () => { + if (lyric.lines !== lines) return + setLyric(lines) + }, 50) + } + } else { + setLyric(lines) + } + } + + let delayScrollTimeout + const scrollLine = (line, oldLine) => { + setImmediate(() => { + prevActiveLine = line + }) + if (line < 0 || !lyric.lines.length) return + if (line == 0 && isSetedLines) return isSetedLines = false + isSetedLines &&= false + if (oldLine == null || line - oldLine != 1) return handleScrollLrc() + + if (setting['desktopLyric.isDelayScroll']) { + delayScrollTimeout = setTimeout(() => { + delayScrollTimeout = null + handleScrollLrc(600) + }, 600) + } else { + handleScrollLrc() + } + } + + watch(() => lyric.lines, initLrc) + watch(() => lyric.line, scrollLine) + + onMounted(() => { + document.addEventListener('mousemove', handleMouseMsMove) + document.addEventListener('mouseup', handleMouseMsUp) + document.addEventListener('touchmove', handleTouchMove) + document.addEventListener('touchend', handleMouseMsUp) + + initLrc(lyric.lines, null) + }) + + onBeforeUnmount(() => { + document.removeEventListener('mousemove', handleMouseMsMove) + document.removeEventListener('mouseup', handleMouseMsUp) + document.removeEventListener('touchmove', handleTouchMove) + document.removeEventListener('touchend', handleMouseMsUp) + }) + + return { + dom_lyric, + dom_lyric_text, + isMsDown, + handleLyricMouseDown, + handleLyricTouchStart, + handleWheel, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricVertical/index.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricVertical/index.vue new file mode 100644 index 000000000..86360ffc6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricVertical/index.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricVertical/useLyric.js b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricVertical/useLyric.js new file mode 100644 index 000000000..8d6cf47f3 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/LyricVertical/useLyric.js @@ -0,0 +1,235 @@ +import { ref, onMounted, onBeforeUnmount, watch, nextTick } from '@common/utils/vueTools' +import { scrollXRTo } from '@common/utils/renderer' +import { lyric } from '@lyric/store/lyric' +import { isPlay, setting } from '@lyric/store/state' +import { setWindowBounds, setWindowResizeable } from '@lyric/utils/ipc' +import { isWin } from '@common/utils' + +const getOffsetTop = (contentWidth, lineWidth) => { + switch (setting['desktopLyric.scrollAlign']) { + case 'top': return contentWidth - lineWidth - 2 + default: return contentWidth * 0.5 - lineWidth / 2 + } +} + +export default (isComputeWidth) => { + const dom_lyric = ref(null) + const dom_lyric_text = ref(null) + const isMsDown = ref(false) + let isStopScroll = false + + const winEvent = { + isMsDown: false, + msDownX: 0, + msDownY: 0, + windowW: 0, + windowH: 0, + } + + let msDownX = 0 + let msDownScrollX = 0 + let timeout = null + let cancelScrollFn + let dom_lines + let line_widths + let isSetedLines = false + let prevActiveLine = 0 + + + const handleScrollLrc = (duration = 300) => { + if (!dom_lines?.length || !dom_lyric.value) return + if (isStopScroll) return + let dom_p = dom_lines[lyric.line] + + if (dom_p) { + let offset = 0 + if (isComputeWidth.value) { + let prevLineWidth = line_widths[prevActiveLine] ?? 0 + offset = prevActiveLine < lyric.line ? ((dom_lines[prevActiveLine]?.clientWidth ?? 0) - prevLineWidth) : 0 + // console.log(prevActiveLine, dom_lines[prevActiveLine]?.clientHeight ?? 0, prevLineWidth, offset) + } + cancelScrollFn = scrollXRTo(dom_lyric.value, dom_p ? (dom_p.offsetLeft + offset - getOffsetTop(dom_lyric.value.clientWidth, dom_p.clientWidth)) : 0, duration) + } else { + cancelScrollFn = scrollXRTo(dom_lyric.value, 0, duration) + } + } + const clearLyricScrollTimeout = () => { + if (!timeout) return + clearTimeout(timeout) + timeout = null + } + const startLyricScrollTimeout = () => { + clearLyricScrollTimeout() + timeout = setTimeout(() => { + timeout = null + isStopScroll = false + if (!isPlay.value) return + handleScrollLrc() + }, 3000) + } + + const handleLyricDown = (target, x, y) => { + if (target.classList.contains('font-lrc') || + target.parentNode.classList.contains('font-lrc') || + target.classList.contains('extended') || + target.parentNode.classList.contains('extended') + ) { + if (delayScrollTimeout) { + clearTimeout(delayScrollTimeout) + delayScrollTimeout = null + } + isMsDown.value = true + msDownX = x + msDownScrollX = dom_lyric.value.scrollLeft + } else { + winEvent.isMsDown = true + winEvent.msDownX = x + winEvent.msDownY = y + winEvent.windowW = window.innerWidth + winEvent.windowH = window.innerHeight + // https://github.com/lyswhut/lx-music-desktop/issues/2244 + if (isWin) setWindowResizeable(false) + } + } + const handleLyricMouseDown = event => { + handleLyricDown(event.target, event.clientX, event.clientY) + } + const handleLyricTouchStart = event => { + if (event.changedTouches.length) { + const touch = event.changedTouches[0] + handleLyricDown(event.target, touch.clientX, touch.clientY) + } + } + const handleMouseMsUp = () => { + isMsDown.value = false + winEvent.isMsDown = false + if (isWin) setWindowResizeable(true) + } + + const handleMove = (x, y) => { + if (isMsDown.value) { + isStopScroll ||= true + if (cancelScrollFn) { + cancelScrollFn() + cancelScrollFn = null + } + dom_lyric.value.scrollLeft = msDownScrollX + msDownX - x + startLyricScrollTimeout() + } else if (winEvent.isMsDown) { + // https://github.com/lyswhut/lx-music-desktop/issues/2244 + if (isWin) { + setWindowBounds({ + x: x - winEvent.msDownX, + y: y - winEvent.msDownY, + w: winEvent.windowW, + h: winEvent.windowH, + }) + } else { + setWindowBounds({ + x: x - winEvent.msDownX, + y: y - winEvent.msDownY, + w: window.innerWidth, + h: window.innerHeight, + }) + } + } + } + const handleMouseMsMove = event => { + handleMove(event.clientX, event.clientY) + } + const handleTouchMove = (e) => { + if (e.changedTouches.length) { + const touch = e.changedTouches[0] + handleMove(touch.clientX, touch.clientY) + } + } + + const handleWheel = (event) => { + console.log(event.deltaY) + if (cancelScrollFn) { + cancelScrollFn() + cancelScrollFn = null + } + dom_lyric.value.scrollLeft = dom_lyric.value.scrollLeft - event.deltaY + startLyricScrollTimeout() + } + + const setLyric = (lines) => { + const dom_line_content = document.createDocumentFragment() + for (const line of lines) { + dom_line_content.appendChild(line.dom_line) + } + dom_lyric_text.value.textContent = '' + dom_lyric_text.value.appendChild(dom_line_content) + nextTick(() => { + dom_lines = dom_lyric.value.querySelectorAll('.line-content') + line_widths = Array.from(dom_lines).map(l => l.clientWidth) + handleScrollLrc() + }) + } + + const initLrc = (lines, oLines) => { + prevActiveLine = 0 + isSetedLines = true + if (oLines) { + if (lines.length) { + setLyric(lines) + } else { + cancelScrollFn = scrollXRTo(dom_lyric.value, 0, 300, () => { + if (lyric.lines !== lines) return + setLyric(lines) + }, 50) + } + } else { + setLyric(lines) + } + } + + let delayScrollTimeout + const scrollLine = (line, oldLine) => { + setImmediate(() => { + prevActiveLine = line + }) + if (line < 0) return + if (line == 0 && isSetedLines) return isSetedLines = false + isSetedLines &&= false + if (oldLine == null || line - oldLine != 1) return handleScrollLrc() + + if (setting['desktopLyric.isDelayScroll']) { + delayScrollTimeout = setTimeout(() => { + delayScrollTimeout = null + handleScrollLrc(600) + }, 600) + } else { + handleScrollLrc() + } + } + + watch(() => lyric.lines, initLrc) + watch(() => lyric.line, scrollLine) + + onMounted(() => { + document.addEventListener('mousemove', handleMouseMsMove) + document.addEventListener('mouseup', handleMouseMsUp) + document.addEventListener('touchmove', handleTouchMove) + document.addEventListener('touchend', handleMouseMsUp) + + initLrc(lyric.lines, null) + }) + + onBeforeUnmount(() => { + document.removeEventListener('mousemove', handleMouseMsMove) + document.removeEventListener('mouseup', handleMouseMsUp) + document.removeEventListener('touchmove', handleTouchMove) + document.removeEventListener('touchend', handleMouseMsUp) + }) + + return { + dom_lyric, + dom_lyric_text, + isMsDown, + handleLyricMouseDown, + handleLyricTouchStart, + handleWheel, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/useDrag.js b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/useDrag.js new file mode 100644 index 000000000..85ef8ec37 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/components/layout/useDrag.js @@ -0,0 +1,87 @@ +import { onMounted, onBeforeUnmount } from '@common/utils/vueTools' +import { setWindowBounds, setWindowResizeable } from '@lyric/utils/ipc' +import { isWin } from '@common/utils' + +export default () => { + const winEvent = { + isMsDown: false, + msDownX: 0, + msDownY: 0, + windowW: 0, + windowH: 0, + } + + const handleLyricDown = (target, x, y) => { + winEvent.isMsDown = true + winEvent.msDownX = x + winEvent.msDownY = y + winEvent.windowW = window.innerWidth + winEvent.windowH = window.innerHeight + // https://github.com/lyswhut/lx-music-desktop/issues/2244 + if (isWin) setWindowResizeable(false) + } + const handleLyricMouseDown = event => { + console.log(event.target, event.currentTarget) + if (event.target !== event.currentTarget) return + handleLyricDown(event.target, event.clientX, event.clientY) + } + const handleLyricTouchStart = event => { + if (event.changedTouches.length) { + const touch = event.changedTouches[0] + if (touch.target !== touch.currentTarget) return + handleLyricDown(event.target, touch.clientX, touch.clientY) + } + } + const handleMouseMsUp = () => { + winEvent.isMsDown = false + if (isWin) setWindowResizeable(true) + } + + const handleMove = (x, y) => { + if (!winEvent.isMsDown) return + // https://github.com/lyswhut/lx-music-desktop/issues/2244 + if (isWin) { + setWindowBounds({ + x: x - winEvent.msDownX, + y: y - winEvent.msDownY, + w: winEvent.windowW, + h: winEvent.windowH, + }) + } else { + setWindowBounds({ + x: x - winEvent.msDownX, + y: y - winEvent.msDownY, + w: window.innerWidth, + h: window.innerHeight, + }) + } + } + const handleMouseMsMove = event => { + handleMove(event.clientX, event.clientY) + } + const handleTouchMove = (e) => { + if (e.changedTouches.length) { + const touch = e.changedTouches[0] + handleMove(touch.clientX, touch.clientY) + } + } + + onMounted(() => { + document.addEventListener('mousemove', handleMouseMsMove) + document.addEventListener('mouseup', handleMouseMsUp) + document.addEventListener('touchmove', handleTouchMove) + document.addEventListener('touchend', handleMouseMsUp) + }) + + onBeforeUnmount(() => { + document.removeEventListener('mousemove', handleMouseMsMove) + document.removeEventListener('mouseup', handleMouseMsUp) + document.removeEventListener('touchmove', handleTouchMove) + document.removeEventListener('touchend', handleMouseMsUp) + }) + + return { + handleLyricMouseDown, + handleLyricTouchStart, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/core/lyric.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/core/lyric.ts new file mode 100644 index 000000000..c12d9d16f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/core/lyric.ts @@ -0,0 +1,71 @@ +import Lyric from '@common/utils/lyric-font-player' +import { markRawList } from '@common/utils/vueTools' +import { setLines, setOffset, setTempOffset, setText, lyrics } from '@lyric/store/lyric' +import { musicInfo, setting } from '@lyric/store/state' + +let lrc: Lyric + +export const init = () => { + lrc = new Lyric({ + shadowContent: true, + activeLineClassName: 'active', + rate: setting['player.playbackRate'], + isVertical: setting['desktopLyric.direction'] == 'vertical', + onPlay(line, text) { + setText(text, Math.max(line, 0)) + // console.log(line, text) + }, + onSetLyric(lines, offset) { // listening lyrics seting event + // console.log(lines) // lines is array of all lyric text + setLines(markRawList([...lines])) + setText(lines[0] ?? '', 0) + setOffset(offset) // 歌词延迟 + setTempOffset(0) // 重置临时延迟 + }, + onUpdateLyric(lines) { + setLines(markRawList([...lines])) + setText(lines[0] ?? '', 0) + }, + }) +} + +export const setLyricOffset = (offset: number) => { + setTempOffset(offset) + lrc.setOffset(offset) +} + +export const setPlaybackRate = (rate: number) => { + lrc.setPlaybackRate(rate) +} + +export const setLyric = () => { + if (!musicInfo.id) return + const extendedLyrics = [] + if (setting['player.isShowLyricRoma'] && lyrics.rlyric) extendedLyrics.push(lyrics.rlyric) + if (setting['player.isShowLyricTranslation'] && lyrics.tlyric) extendedLyrics.push(lyrics.tlyric) + if (setting['player.isSwapLyricTranslationAndRoma']) extendedLyrics.reverse() + lrc.setLyric( + setting['player.isPlayLxlrc'] && lyrics.lxlyric ? lyrics.lxlyric : lyrics.lyric, + extendedLyrics, + ) +} + + +export const play = (time: number) => { + if (!lyrics.lyric) return + lrc.play(time) +} + +export const pause = () => { + lrc.pause() +} + +export const stop = () => { + lrc.setLyric('') + // setLines([]) + setText('', 0) +} + +export const setVertical = (isVertical: boolean) => { + lrc.setVertical(isVertical) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/core/mainWindowChannel.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/core/mainWindowChannel.ts new file mode 100644 index 000000000..ff16f2574 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/core/mainWindowChannel.ts @@ -0,0 +1,111 @@ +import { onProvideMainWindowChannel } from '@lyric/utils/ipc' +import { onBeforeUnmount } from '@common/utils/vueTools' +import { setMusicInfo, setIsPlay } from '../store/action' +import { pause, play, setLyric, setLyricOffset, setPlaybackRate, stop } from './lyric' +import { lyrics } from '@lyric/store/lyric' + +let mainWindowPort: Electron.IpcRendererEvent['ports'][0] | null = null +export const sendDesktopLyricInfo = (info: LX.DesktopLyric.WinMainActions) => { + if (mainWindowPort == null) return + mainWindowPort.postMessage({ action: info }) +} + +const listeners: Array<(event: LX.DesktopLyric.LyricActions) => void> = [] + +const handleDesktopLyricMessage = (event: LX.DesktopLyric.LyricActions) => { + switch (event.action) { + case 'set_info': + setMusicInfo({ + id: event.data.id, + singer: event.data.singer, + name: event.data.name, + album: event.data.album, + }) + lyrics.lyric = event.data.lrc ?? '' + lyrics.tlyric = event.data.tlrc + lyrics.rlyric = event.data.rlrc + lyrics.lxlyric = event.data.lxlrc + setLyric() + if (event.data.isPlay) { + setImmediate(() => { + getStatus() + }) + } + break + case 'set_lyric': + lyrics.lyric = event.data.lrc ?? '' + lyrics.tlyric = event.data.tlrc + lyrics.rlyric = event.data.rlrc + lyrics.lxlyric = event.data.lxlrc + setLyric() + break + case 'set_status': + setIsPlay(event.data.isPlay) + if (event.data.isPlay) play(event.data.played_time) + else pause() + break + case 'set_offset': + setLyricOffset(event.data) + break + case 'set_playbackRate': + setPlaybackRate(event.data) + break + case 'set_pause': + setIsPlay(false) + pause() + break + case 'set_play': + setIsPlay(true) + play(event.data) + break + case 'set_stop': + setIsPlay(false) + stop() + break + default: + for (const listener of listeners) { + listener(event) + } + break + } +} + +export const init = () => { + onProvideMainWindowChannel(({ event }) => { + const [port] = event.ports + mainWindowPort = port + + // ... register a handler to receive results ... + port.onmessage = ({ data }) => { + handleDesktopLyricMessage(data) + // console.log('received result:', data) + } + // ... and start sending it work! + + port.onmessageerror = (event) => { + console.log('onmessageerror', event) + } + + getInfo() + }) +} + +export const useEvent = (listener: (event: LX.DesktopLyric.LyricActions) => void) => { + listeners.push(listener) + + onBeforeUnmount(() => { + listeners.splice(listeners.indexOf(listener), 1) + }) +} + +export const getInfo = () => { + sendDesktopLyricInfo('get_info') +} + +export const getAnalyserDataArray = () => { + sendDesktopLyricInfo('get_analyser_data_array') +} + +export const getStatus = () => { + sendDesktopLyricInfo('get_status') +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/index.html b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/index.html new file mode 100644 index 000000000..54ac831b5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/index.html @@ -0,0 +1,42 @@ + + + + + + Lyric - LX Music + + +
    + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/main.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/main.ts new file mode 100644 index 000000000..0ebfe75f5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/main.ts @@ -0,0 +1,41 @@ +import { createApp } from 'vue' + +import { i18nPlugin } from './plugins/i18n' + +import mountComponents from './components' + +import App from './App.vue' + +import '@root/common/error' +import { getSetting, onMainWindowInited, onSettingChanged, sendConnectMainWindowEvent } from './utils/ipc' +import { initSetting, mergeSetting } from './store/action' +import { init as initMainWindowChannel } from './core/mainWindowChannel' + +window.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS + +void getSetting().then((setting) => { + // window.lx.appSetting = setting + // Set language automatically + const languageId = setting['common.langId'] + if (window.i18n.locale !== languageId && languageId != null) { + window.i18n.setLanguage(languageId) + window.setLang(languageId) + } + + // store.commit('setSetting', setting) + initSetting(setting) + + onSettingChanged(({ params: setting }) => { + // console.log('onSettingChanged', setting) + mergeSetting(setting) + }) + onMainWindowInited(() => { + sendConnectMainWindowEvent() + }) + initMainWindowChannel() + + const app = createApp(App) + app.use(i18nPlugin) + mountComponents(app) + app.mount('#root') +}) diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/plugins/i18n.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/plugins/i18n.ts new file mode 100644 index 000000000..5ab892196 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/plugins/i18n.ts @@ -0,0 +1,11 @@ +import type { I18n } from '@root/lang' +import { createI18n, i18nPlugin, useI18n } from '@root/lang' + +window.i18n = createI18n() + +export { + i18nPlugin, + useI18n, +} + +export type { I18n } diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/store/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/store/action.ts new file mode 100644 index 000000000..65f5c983c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/store/action.ts @@ -0,0 +1,34 @@ +import { setting, musicInfo, isPlay } from './state' +import { updateSetting as saveSetting } from '@lyric/utils/ipc' + +export const initSetting = (newSetting: LX.DesktopLyric.Config) => { + mergeSetting(newSetting) +} + +export const mergeSetting = (newSetting: Partial) => { + for (const [key, value] of Object.entries(newSetting)) { + // @ts-expect-error + setting[key] = value + } +} + +export const updateSetting = (setting: Partial) => { + void saveSetting(setting) +} + +type MusicInfoKeys = keyof typeof musicInfo +const musicInfoKeys: MusicInfoKeys[] = Object.keys(musicInfo) as MusicInfoKeys[] + +export const setMusicInfo = (_musicInfo: Partial) => { + for (const key of musicInfoKeys) { + const val = _musicInfo[key] + if (val !== undefined) { + // @ts-expect-error + musicInfo[key] = val + } + } +} + +export const setIsPlay = (_status: boolean) => { + isPlay.value = _status +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/store/lyric.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/store/lyric.ts new file mode 100644 index 000000000..9831720f7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/store/lyric.ts @@ -0,0 +1,50 @@ +import { markRaw, reactive } from '@common/utils/vueTools' + + +export const lyrics = markRaw<{ + lyric: string + tlyric: string | null + rlyric: string | null + lxlyric: string | null +}>({ + lyric: '', + tlyric: '', + rlyric: '', + lxlyric: '', +}) + +interface Line { + text: string + time: number + extendedLyrics: string[] + dom_line: HTMLDivElement +} + +export const lyric = reactive<{ + lines: Line[] + text: string + line: number + offset: number // 歌词延迟 + tempOffset: number // 歌词临时延迟 +}>({ + lines: [], + text: '', + line: 0, + offset: 0, // 歌词延迟 + tempOffset: 0, // 歌词临时延迟 +}) + +export const setLines = (lines: Line[]) => { + if (!lines.length && !lyric.lines.length) return + lyric.lines = lines +} +export const setText = (text: string, line: number) => { + lyric.text = text + lyric.line = line +} +export const setOffset = (offset: number) => { + lyric.offset = offset +} +export const setTempOffset = (offset: number) => { + lyric.tempOffset = offset +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/store/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/store/state.ts new file mode 100644 index 000000000..35c6445c9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/store/state.ts @@ -0,0 +1,100 @@ +import { ref, shallowReactive } from '@common/utils/vueTools' + +export const setting = shallowReactive({ + 'desktopLyric.enable': false, + 'desktopLyric.isLock': false, + 'desktopLyric.isAlwaysOnTop': false, + 'desktopLyric.isAlwaysOnTopLoop': false, + 'desktopLyric.isShowTaskbar': true, + 'desktopLyric.pauseHide': false, + 'desktopLyric.audioVisualization': false, + 'desktopLyric.width': 450, + 'desktopLyric.height': 300, + 'desktopLyric.x': null, + 'desktopLyric.y': null, + 'desktopLyric.isLockScreen': true, + 'desktopLyric.isDelayScroll': true, + 'desktopLyric.scrollAlign': 'center', + 'desktopLyric.isHoverHide': false, + 'desktopLyric.direction': 'horizontal', + 'desktopLyric.style.align': 'center', + '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.font': '', + 'desktopLyric.style.fontSize': 20, + 'desktopLyric.style.lineGap': 15, + // 'desktopLyric.style.fontWeight': true, + 'desktopLyric.style.opacity': 95, + 'desktopLyric.style.ellipsis': false, + 'desktopLyric.style.isFontWeightFont': false, + 'desktopLyric.style.isFontWeightLine': false, + 'desktopLyric.style.isFontWeightExtended': false, + 'desktopLyric.style.isZoomActiveLrc': true, + 'common.langId': 'zh-cn', + 'player.isShowLyricTranslation': false, + 'player.isShowLyricRoma': false, + 'player.isSwapLyricTranslationAndRoma': false, + 'player.isPlayLxlrc': false, + 'player.playbackRate': 1, +}) + +// export const themeList = markRaw([ +// { +// id: 0, +// className: 'green', +// }, +// { +// id: 1, +// className: 'yellow', +// }, +// { +// id: 2, +// className: 'blue', +// }, +// { +// id: 3, +// className: 'red', +// }, +// { +// id: 4, +// className: 'pink', +// }, +// { +// id: 5, +// className: 'purple', +// }, +// { +// id: 6, +// className: 'orange', +// }, +// { +// id: 7, +// className: 'grey', +// }, +// { +// id: 8, +// className: 'ming', +// }, +// { +// id: 9, +// className: 'blue2', +// }, +// ]) + +// export type Status = 'playing' | 'paused' | 'stopped' + +// export const status = ref('stopped') +export const isPlay = ref(false) + +export const musicInfo = shallowReactive<{ + id: string | null + name: string + singer: string + album: string | null +}>({ + id: null, + name: '^', + singer: '^', + album: null, +}) diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/tsconfig.json b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/tsconfig.json new file mode 100644 index 000000000..ee451b7fa --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "isolatedModules": true, + "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ + "@common/*": ["../common/*"], + // "@renderer/*": ["renderer/*"], + "@lyric/*": ["../renderer-lyric/*"], + "@static/*": ["../static/*"], + "@root/*": ["../*"], + }, + "typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */ + "./types" + ], + }, + // "vueCompilerOptions": { + // "plugins": [ + // "@volar/vue-language-plugin-pug" + // ] + // } + // "include": [ + // "./**/*.ts", + // // "./**/*.js", + // "./**/*.vue", + // "./**/*.json", + // ], +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/types/app.d.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/types/app.d.ts new file mode 100644 index 000000000..a75802b65 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/types/app.d.ts @@ -0,0 +1,25 @@ +import { type I18n } from '@lyric/plugins/i18n' + +declare global { + interface Window { + ELECTRON_DISABLE_SECURITY_WARNINGS?: string + + i18n: I18n + + lxData: any + + setTheme: (colors: Record) => void + setLang: (lang?: string) => void + setLyricColor: (colors: Record) => void + os: 'windows' | 'linux' | 'mac' + } + + namespace LX { + + } + +} + + +// declare const ELECTRON_DISABLE_SECURITY_WARNINGS: string +// declare const userApiPath: string diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/types/common.d.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/types/common.d.ts new file mode 100644 index 000000000..ca4c47503 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/types/common.d.ts @@ -0,0 +1,13 @@ +// import '@common/types/app_setting' +// import '@common/types/common' +// import '@common/types/user_api' +// import '@common/types/sync' +// import '@common/types/music' +// import '@common/types/list' +// import '@common/types/download_list' +// import '@common/types/player' +import '@common/types/shims_vue' +// import '@common/types/utils' +import '@common/types/theme' +import '@common/types/desktop_lyric' +import '@common/types/ipc_renderer' diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useCommon.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useCommon.ts new file mode 100644 index 000000000..8b393061b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useCommon.ts @@ -0,0 +1,10 @@ +import { watch } from '@common/utils/vueTools' +import { setting } from '@lyric/store/state' + +export default () => { + watch(() => setting['common.langId'], (id) => { + if (!id) return + window.i18n.setLanguage(id) + window.setLang(id) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useHoverHide.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useHoverHide.ts new file mode 100644 index 000000000..c208b476c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useHoverHide.ts @@ -0,0 +1,41 @@ +import { computed, ref, onBeforeUnmount } from '@common/utils/vueTools' +import { setting } from '@lyric/store/state' +import { sendMouseEnterLeave, onMouseEnterLeave } from '@lyric/utils/ipc' + +export default () => { + const isMouseEnter = ref(false) + + const isHoverHide = computed(() => { + return setting['desktopLyric.isHoverHide'] && isMouseEnter.value + }) + + const handleMouseMove = () => { + handleMouseEnter() + } + const handleMouseEnter = () => { + if (isMouseEnter.value) return + isMouseEnter.value = true + sendMouseEnterLeave(true) + } + const handleMouseLeave = () => { + if (!isMouseEnter.value) return + isMouseEnter.value = false + sendMouseEnterLeave(false) + } + + const removeMouseEnterLeaveListener = onMouseEnterLeave(({ params: isEnter }) => { + isMouseEnter.value = isEnter + }) + document.body.addEventListener('mousemove', handleMouseMove) + document.body.addEventListener('mouseenter', handleMouseEnter) + document.body.addEventListener('mouseleave', handleMouseLeave) + + onBeforeUnmount(() => { + removeMouseEnterLeaveListener() + document.body.removeEventListener('mousemove', handleMouseMove) + document.body.removeEventListener('mouseenter', handleMouseEnter) + document.body.removeEventListener('mouseleave', handleMouseLeave) + }) + + return isHoverHide +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useLyric.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useLyric.ts new file mode 100644 index 000000000..ba26c57a6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useLyric.ts @@ -0,0 +1,23 @@ +import { watch } from '@common/utils/vueTools' +import { setLyric, setVertical, setPlaybackRate } from '@lyric/core/lyric' +import { getStatus } from '@lyric/core/mainWindowChannel' +import { isPlay, setting } from '@lyric/store/state' + +export default () => { + watch(() => setting['player.isShowLyricTranslation'], setLyric) + watch(() => setting['player.isShowLyricRoma'], setLyric) + watch(() => setting['player.isSwapLyricTranslationAndRoma'], setLyric) + watch(() => setting['player.isPlayLxlrc'], setLyric) + watch(() => setting['player.playbackRate'], (rate) => { + setPlaybackRate(rate) + if (isPlay.value) { + setTimeout(() => { + getStatus() + }) + } + }) + watch(() => setting['desktopLyric.direction'], (direction) => { + setVertical(direction == 'vertical') + // if (isPlay.value) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/usePauseHide.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/usePauseHide.ts new file mode 100644 index 000000000..1d82a5d45 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/usePauseHide.ts @@ -0,0 +1,41 @@ +import { ref, watch } from '@common/utils/vueTools' +import { isPlay, setting } from '@lyric/store/state' + +export default () => { + let unWatch: (() => void) | null = null + let isHide = ref(false) + let timeout: NodeJS.Timeout | null = null + const clearIntv = () => { + if (!timeout) return + clearTimeout(timeout) + timeout = null + } + watch(() => setting['desktopLyric.pauseHide'], (enable) => { + if (enable) { + unWatch = watch(isPlay, (isPlay) => { + clearIntv() + if (isPlay) { + isHide.value &&= false + } else { + timeout = setTimeout(() => { + timeout = null + isHide.value = true + }, 200) + } + }, { + immediate: true, + }) + } else { + clearIntv() + isHide.value &&= false + if (unWatch) { + unWatch() + unWatch = null + } + } + }, { + immediate: true, + }) + + return isHide +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useTheme.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useTheme.ts new file mode 100644 index 000000000..a5dc07e03 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useTheme.ts @@ -0,0 +1,24 @@ +import { onBeforeUnmount, watch } from '@common/utils/vueTools' +import { setting } from '@lyric/store/state' +import { onThemeChange } from '@lyric/utils/ipc' +import { RGB_Alpha_Shade } from '@common/theme/colorUtils' + +export default () => { + const rThemeChange = onThemeChange(({ params: setting }) => { + window.setTheme(setting.theme.colors) + }) + watch(() => [setting['desktopLyric.style.lyricUnplayColor'], setting['desktopLyric.style.lyricPlayedColor'], setting['desktopLyric.style.lyricShadowColor']], ([unplayColor, playedColor, shadowColor]) => { + window.setLyricColor({ + '--color-lyric-unplay': unplayColor, + '--color-lyric-played': playedColor, + '--color-lyric-shadow': shadowColor, + '--color-lyric-shadow-font-mode': RGB_Alpha_Shade(0.49, shadowColor), + }) + }, { + immediate: true, + }) + + onBeforeUnmount(() => { + rThemeChange() + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useWindowSize.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useWindowSize.ts new file mode 100644 index 000000000..f4807844b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/useApp/useWindowSize.ts @@ -0,0 +1,138 @@ +import { setting } from '@lyric/store/state' +import { onBeforeUnmount, onMounted } from '@common/utils/vueTools' +import { setWindowBounds } from '@lyric/utils/ipc' + +type Origin = 'left' +| 'top' +| 'right' +| 'bottom' +| 'top-left' +| 'top-right' +| 'bottom-left' +| 'bottom-right' + +export default () => { + const resize: { + origin: Origin | null + msDownX: number + msDownY: number + } = { + origin: null, + msDownX: 0, + msDownY: 0, + } + + const handleMove = (clientX: number, clientY: number) => { + if (!resize.origin || setting['desktopLyric.isLock']) return + // if (!event.target.classList.contains('resize-' + resize.origin)) return + // console.log(event.target) + let bounds: LX.DesktopLyric.NewBounds = { + w: 0, + h: 0, + x: 0, + y: 0, + } + let temp + switch (resize.origin) { + case 'left': + temp = clientX - resize.msDownX + bounds.w = -temp + bounds.x = temp + break + case 'right': + bounds.w = clientX - resize.msDownX + resize.msDownX += bounds.w + break + case 'top': + temp = clientY - resize.msDownY + bounds.y = temp + bounds.h = -temp + break + case 'bottom': + bounds.h = clientY - resize.msDownY + resize.msDownY += bounds.h + break + case 'top-left': + temp = clientX - resize.msDownX + bounds.w = -temp + bounds.x = temp + temp = clientY - resize.msDownY + bounds.y = temp + bounds.h = -temp + break + case 'top-right': + temp = clientY - resize.msDownY + bounds.y = temp + bounds.h = -temp + bounds.w = clientX - resize.msDownX + resize.msDownX += bounds.w + break + case 'bottom-left': + temp = clientX - resize.msDownX + bounds.w = -temp + bounds.x = temp + bounds.h = clientY - resize.msDownY + resize.msDownY += bounds.h + break + case 'bottom-right': + bounds.w = clientX - resize.msDownX + resize.msDownX += bounds.w + bounds.h = clientY - resize.msDownY + resize.msDownY += bounds.h + break + } + // console.log(bounds) + bounds.w = window.innerWidth + bounds.w + bounds.h = window.innerHeight + bounds.h + setWindowBounds(bounds) + } + + const handleDown = (origin: Origin, clientX: number, clientY: number) => { + handleMouseUp() + resize.origin = origin + resize.msDownX = clientX + resize.msDownY = clientY + } + const handleMouseUp = () => { + resize.origin = null + } + + const handleMouseDown = (origin: Origin, event: MouseEvent) => { + handleDown(origin, event.clientX, event.clientY) + } + const handleTouchDown = (origin: Origin, event: TouchEvent) => { + if (event.changedTouches.length) { + const touch = event.changedTouches[0] + handleDown(origin, touch.clientX, touch.clientY) + } + } + + const handleMouseMove = (event: MouseEvent) => { + handleMove(event.clientX, event.clientY) + } + const handleTouchMove = (event: TouchEvent) => { + if (event.changedTouches.length) { + const touch = event.changedTouches[0] + handleMove(touch.clientX, touch.clientY) + } + } + + + onMounted(() => { + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + document.addEventListener('touchmove', handleTouchMove) + document.addEventListener('touchend', handleMouseUp) + }) + onBeforeUnmount(() => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + document.removeEventListener('touchmove', handleTouchMove) + document.removeEventListener('touchend', handleMouseUp) + }) + + return { + handleMouseDown, + handleTouchDown, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/utils/ipc.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/utils/ipc.ts new file mode 100644 index 000000000..89377b714 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer-lyric/utils/ipc.ts @@ -0,0 +1,65 @@ +import { rendererSend, rendererInvoke, rendererOn, rendererOff } from '@common/rendererIpc' +import { CMMON_EVENT_NAME, WIN_LYRIC_RENDERER_EVENT_NAME } from '@common/ipcNames' + +type RemoveListener = () => void + +export const getSetting = async() => { + return rendererInvoke(WIN_LYRIC_RENDERER_EVENT_NAME.get_config) +} +export const updateSetting = async(setting: Partial) => { + await rendererInvoke(WIN_LYRIC_RENDERER_EVENT_NAME.set_config, setting) +} +export const onSettingChanged = (listener: LX.IpcRendererEventListenerParams>): RemoveListener => { + rendererOn>(WIN_LYRIC_RENDERER_EVENT_NAME.on_config_change, listener) + return () => { + rendererOff(WIN_LYRIC_RENDERER_EVENT_NAME.on_config_change, listener) + } +} +export const setWindowBounds = (bounds: LX.DesktopLyric.NewBounds) => { + rendererSend(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_bounds, bounds) +} +let previousResizable: boolean | null = null +export const setWindowResizeable = (resizable: boolean) => { + if (previousResizable === resizable) return + previousResizable = resizable + // https://github.com/electron/electron/issues/48352 + // rendererSend(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_resizeable, resizable) +} + +export const sendConnectMainWindowEvent = () => { + rendererSend(WIN_LYRIC_RENDERER_EVENT_NAME.request_main_window_channel) +} +export const onProvideMainWindowChannel = (listener: LX.IpcRendererEventListener): RemoveListener => { + rendererOn(WIN_LYRIC_RENDERER_EVENT_NAME.provide_main_window_channel, listener) + return () => { + rendererOff(WIN_LYRIC_RENDERER_EVENT_NAME.provide_main_window_channel, listener) + } +} +export const onMainWindowInited = (listener: LX.IpcRendererEventListener): RemoveListener => { + rendererOn(WIN_LYRIC_RENDERER_EVENT_NAME.main_window_inited, listener) + return () => { + rendererOff(WIN_LYRIC_RENDERER_EVENT_NAME.main_window_inited, listener) + } +} + +export const sendMouseEnterLeave = (isEnter: boolean) => { + rendererSend(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, isEnter) +} +export const onMouseEnterLeave = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, listener) + return () => { + rendererOff(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, listener) + } +} + +/** + * On Theme Change + * @param listener LX.IpcRendererEventListenerParams + * @returns RemoveListener Fn + */ +export const onThemeChange = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(CMMON_EVENT_NAME.theme_change, listener) + return () => { + rendererOff(CMMON_EVENT_NAME.theme_change, listener) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/.eslintrc.cjs b/plugins/lx-music-ztools/vendor/lx/src/renderer/.eslintrc.cjs new file mode 100644 index 000000000..f9af3d6dd --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/.eslintrc.cjs @@ -0,0 +1,20 @@ +/* eslint-env node */ +const { base, html, typescript, vue } = require('../../.eslintrc.base.cjs') + +module.exports = { + root: true, + ...base, + overrides: [ + html, + vue, + { + ...typescript, + parserOptions: { + project: './tsconfig.json', + }, + }, + ], + ignorePatterns: [ + 'vendors', + ], +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/App.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/App.vue new file mode 100644 index 000000000..99ac2b9ba --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/App.vue @@ -0,0 +1,171 @@ + + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/defaultUser.jpg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/defaultUser.jpg new file mode 100644 index 000000000..51d5929d2 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/defaultUser.jpg differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/icon.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/icon.svg new file mode 100644 index 000000000..2fb39ebe5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/landingMoon.jpg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/landingMoon.jpg new file mode 100644 index 000000000..5b2a3c4a0 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/landingMoon.jpg differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/landingMoon2.png b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/landingMoon2.png new file mode 100644 index 000000000..9cac49818 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/images/landingMoon2.png differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/Silence02s.mp3 b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/Silence02s.mp3 new file mode 100644 index 000000000..5d384dda2 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/Silence02s.mp3 differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/bright-hall.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/bright-hall.wav new file mode 100644 index 000000000..cc2f47a36 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/bright-hall.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/cardiod-35-10-spread.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/cardiod-35-10-spread.wav new file mode 100644 index 000000000..151c5f1ff Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/cardiod-35-10-spread.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/cinema-diningroom.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/cinema-diningroom.wav new file mode 100644 index 000000000..2d1398016 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/cinema-diningroom.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/dining-living-true-stereo.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/dining-living-true-stereo.wav new file mode 100644 index 000000000..64b973586 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/dining-living-true-stereo.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/feedback-spring.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/feedback-spring.wav new file mode 100644 index 000000000..fb40d6419 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/feedback-spring.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/filter-telephone.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/filter-telephone.wav new file mode 100644 index 000000000..47c50e7ef Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/filter-telephone.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/living-bedroom-leveled.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/living-bedroom-leveled.wav new file mode 100644 index 000000000..43c5be017 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/living-bedroom-leveled.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/matrix-reverb1.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/matrix-reverb1.wav new file mode 100644 index 000000000..58b92eb5b Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/matrix-reverb1.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/matrix-reverb2.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/matrix-reverb2.wav new file mode 100644 index 000000000..15f87b197 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/matrix-reverb2.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/medium-room1.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/medium-room1.wav new file mode 100644 index 000000000..5c3491454 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/medium-room1.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/s2_r4_bd.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/s2_r4_bd.wav new file mode 100644 index 000000000..4bb63883e Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/s2_r4_bd.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/s3_r1_bd.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/s3_r1_bd.wav new file mode 100644 index 000000000..d6276f04f Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/s3_r1_bd.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/spreader50-65ms.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/spreader50-65ms.wav new file mode 100644 index 000000000..f5b091adc Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/spreader50-65ms.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/tim-omni-35-10-magnetic.wav b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/tim-omni-35-10-magnetic.wav new file mode 100644 index 000000000..927c70326 Binary files /dev/null and b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/medias/filters/tim-omni-35-10-magnetic.wav differ diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/animate.less b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/animate.less new file mode 100644 index 000000000..96081c805 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/animate.less @@ -0,0 +1,1106 @@ +// https://daneden.github.io/animate.css/ + +@keyframes bounce { + from, + 20%, + 53%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 40%, + 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } +} +@keyframes flash { + from, + 50%, + to { + opacity: 1; + } + + 25%, + 75% { + opacity: 0; + } +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes rubberBand { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + + 65% { + -webkit-transform: scale3d(0.95, 1.05, 1); + transform: scale3d(0.95, 1.05, 1); + } + + 75% { + -webkit-transform: scale3d(1.05, 0.95, 1); + transform: scale3d(1.05, 0.95, 1); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes shake { + from, + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 10%, + 30%, + 50%, + 70%, + 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 20%, + 40%, + 60%, + 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } +} +@keyframes headShake { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 6.5% { + -webkit-transform: translateX(-6px) rotateY(-9deg); + transform: translateX(-6px) rotateY(-9deg); + } + + 18.5% { + -webkit-transform: translateX(5px) rotateY(7deg); + transform: translateX(5px) rotateY(7deg); + } + + 31.5% { + -webkit-transform: translateX(-3px) rotateY(-5deg); + transform: translateX(-3px) rotateY(-5deg); + } + + 43.5% { + -webkit-transform: translateX(2px) rotateY(3deg); + transform: translateX(2px) rotateY(3deg); + } + + 50% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@keyframes swing { + 20% { + -webkit-transform: rotate3d(0, 0, 1, 15deg); + transform: rotate3d(0, 0, 1, 15deg); + } + + 40% { + -webkit-transform: rotate3d(0, 0, 1, -10deg); + transform: rotate3d(0, 0, 1, -10deg); + } + + 60% { + -webkit-transform: rotate3d(0, 0, 1, 5deg); + transform: rotate3d(0, 0, 1, 5deg); + } + + 80% { + -webkit-transform: rotate3d(0, 0, 1, -5deg); + transform: rotate3d(0, 0, 1, -5deg); + } + + to { + -webkit-transform: rotate3d(0, 0, 1, 0deg); + transform: rotate3d(0, 0, 1, 0deg); + } +} +@keyframes tada { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 10%, + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + + 30%, + 50%, + 70%, + 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + + 40%, + 60%, + 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@keyframes wobble { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 15% { + -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + + 30% { + -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + + 45% { + -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + + 60% { + -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + + 75% { + -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +@keyframes jello { + from, + 11.1%, + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 22.2% { + -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); + transform: skewX(-12.5deg) skewY(-12.5deg); + } + + 33.3% { + -webkit-transform: skewX(6.25deg) skewY(6.25deg); + transform: skewX(6.25deg) skewY(6.25deg); + } + + 44.4% { + -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); + transform: skewX(-3.125deg) skewY(-3.125deg); + } + + 55.5% { + -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); + transform: skewX(1.5625deg) skewY(1.5625deg); + } + + 66.6% { + -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); + transform: skewX(-0.78125deg) skewY(-0.78125deg); + } + + 77.7% { + -webkit-transform: skewX(0.390625deg) skewY(0.390625deg); + transform: skewX(0.390625deg) skewY(0.390625deg); + } + + 88.8% { + -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + } +} +@keyframes heartBeat { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 14% { + -webkit-transform: scale(1.3); + transform: scale(1.3); + } + + 28% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 42% { + -webkit-transform: scale(1.3); + transform: scale(1.3); + } + + 70% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes flipInX { + from { + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + animation-timing-function: ease-in; + } + + 60% { + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + + 80% { + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + + to { + transform: perspective(400px); + } +} +@keyframes flipOutX { + from { + transform: perspective(400px); + } + + 30% { + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + + to { + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} +@keyframes flipInY { + from { + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + animation-timing-function: ease-in; + } + + 60% { + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + + 80% { + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + + to { + transform: perspective(400px); + } +} +@keyframes flipOutY { + from { + transform: perspective(400px); + } + + 30% { + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + + to { + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} +@keyframes bounceIn { + from, + 20%, + 40%, + 60%, + 80%, + to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + 0% { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + + 20% { + transform: scale3d(1.1, 1.1, 1.1); + } + + 40% { + transform: scale3d(0.9, 0.9, 0.9); + } + + 60% { + opacity: 1; + transform: scale3d(1.03, 1.03, 1.03); + } + + 80% { + transform: scale3d(0.97, 0.97, 0.97); + } + + to { + opacity: 1; + transform: scale3d(1, 1, 1); + } +} +@keyframes bounceOut { + 20% { + transform: scale3d(0.9, 0.9, 0.9); + } + + 50%, + 55% { + opacity: 1; + transform: scale3d(1.1, 1.1, 1.1); + } + + to { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } +} +@keyframes lightSpeedIn { + from { + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + + 60% { + transform: skewX(20deg); + opacity: 1; + } + + 80% { + transform: skewX(-5deg); + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes lightSpeedOut { + from { + opacity: 1; + } + + to { + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} +@keyframes rotateIn { + from { + transform-origin: center; + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + + to { + transform-origin: center; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInDownLeft { + from { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + + to { + transform-origin: left bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInDownRight { + from { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + transform-origin: right bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInUpLeft { + from { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + transform-origin: left bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateInUpRight { + from { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + + to { + transform-origin: right bottom; + transform: translate3d(0, 0, 0); + opacity: 1; + } +} +@keyframes rotateOut { + from { + transform-origin: center; + opacity: 1; + } + + to { + transform-origin: center; + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} +@keyframes rotateOutDownLeft { + from { + transform-origin: left bottom; + opacity: 1; + } + + to { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} +@keyframes rotateOutDownRight { + from { + transform-origin: right bottom; + opacity: 1; + } + + to { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +@keyframes rotateOutUpLeft { + from { + transform-origin: left bottom; + opacity: 1; + } + + to { + transform-origin: left bottom; + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +@keyframes rotateOutUpRight { + from { + transform-origin: right bottom; + opacity: 1; + } + + to { + transform-origin: right bottom; + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} +@keyframes rollIn { + from { + opacity: 0; + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} +@keyframes rollOut { + from { + opacity: 1; + } + + to { + opacity: 0; + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} +@keyframes zoomIn { + from { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + + 50% { + opacity: 1; + } +} +@keyframes zoomInDown { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInLeft { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInRight { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInUp { + from { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomOut { + from { + opacity: 1; + } + + 50% { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + + to { + opacity: 0; + } +} +@keyframes zoomOutDown { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + to { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + transform-origin: center bottom; + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomOutLeft { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + } + + to { + opacity: 0; + transform: scale(0.1) translate3d(-2000px, 0, 0); + transform-origin: left center; + } +} +@keyframes zoomOutRight { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + } + + to { + opacity: 0; + transform: scale(0.1) translate3d(2000px, 0, 0); + transform-origin: right center; + } +} +@keyframes zoomOutUp { + 40% { + opacity: 1; + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + to { + opacity: 0; + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + transform-origin: center bottom; + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes slideInDown { + from { + transform: translate3d(0, -100%, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideInLeft { + from { + transform: translate3d(-100%, 0, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideInRight { + from { + transform: translate3d(100%, 0, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideInUp { + from { + transform: translate3d(0, 100%, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} +@keyframes slideOutDown { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(0, 100%, 0); + } +} +@keyframes slideOutLeft { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(-100%, 0, 0); + } +} +@keyframes slideOutRight { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(100%, 0, 0); + } +} +@keyframes slideOutUp { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(0, -100%, 0); + } +} +@keyframes jackInTheBox { + from { + opacity: 0; + transform: scale(0.1) rotate(30deg); + transform-origin: center bottom; + } + + 50% { + transform: rotate(-10deg); + } + + 70% { + transform: rotate(3deg); + } + + to { + opacity: 1; + transform: scale(1); + } +} +@keyframes hinge { + 0% { + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + + 20%, + 60% { + -webkit-transform: rotate3d(0, 0, 1, 80deg); + transform: rotate3d(0, 0, 1, 80deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + + 40%, + 80% { + -webkit-transform: rotate3d(0, 0, 1, 60deg); + transform: rotate3d(0, 0, 1, 60deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + opacity: 1; + } + + to { + -webkit-transform: translate3d(0, 700px, 0); + transform: translate3d(0, 700px, 0); + opacity: 0; + } +} + +.flipInX { + backface-visibility: visible !important; + animation-name: flipInX; +} +.flipInY { + backface-visibility: visible !important; + animation-name: flipInY; +} +.fadeIn { + animation-name: fadeIn; +} +.bounceIn { + animation-duration: 0.75s; + animation-name: bounceIn; +} +.lightSpeedIn { + animation-name: lightSpeedIn; + animation-timing-function: ease-out; +} +.rotateIn { + animation-name: rotateIn; +} +.rotateInDownLeft { + animation-name: rotateInDownLeft; +} +.rotateInDownRight { + animation-name: rotateInDownRight; +} +.rotateInUpLeft { + animation-name: rotateInUpLeft; +} +.rotateInUpRight { + animation-name: rotateInUpRight; +} +.rollIn { + animation-name: rollIn; +} +.zoomIn { + animation-name: zoomIn; +} +.zoomInDown { + animation-name: zoomInDown; +} +.zoomInLeft { + animation-name: zoomInLeft; +} +.zoomInRight { + animation-name: zoomInRight; +} +.zoomInUp { + animation-name: zoomInUp; +} +.slideInDown { + animation-name: slideInDown; +} +.slideInLeft { + animation-name: slideInLeft; +} +.slideInRight { + animation-name: slideInRight; +} +.slideInUp { + animation-name: slideInUp; +} +.jackInTheBox { + -webkit-animation-name: jackInTheBox; + animation-name: jackInTheBox; +} + + + +.flipOutX { + animation-duration: 0.75s; + animation-name: flipOutX; + backface-visibility: visible !important; +} +.flipOutY { + animation-duration: 0.75s; + backface-visibility: visible !important; + animation-name: flipOutY; +} +.fadeOut { + animation-name: fadeOut; +} +.bounceOut { + animation-duration: 0.75s; + animation-name: bounceOut; +} +.lightSpeedOut { + animation-name: lightSpeedOut; + animation-timing-function: ease-in; +} +.rotateOut { + animation-name: rotateOut; +} +.rotateOutDownLeft { + animation-name: rotateOutDownLeft; +} +.rotateOutDownRight { + animation-name: rotateOutDownRight; +} +.rotateOutUpLeft { + animation-name: rotateOutUpLeft; +} +.rotateOutUpRight { + animation-name: rotateOutUpRight; +} +.hinge { + animation-duration: 2s; + animation-name: hinge; +} +.rollOut { + animation-name: rollOut; +} +.zoomOut { + animation-name: zoomOut; +} +.zoomOutDown { + animation-name: zoomOutDown; +} +.zoomOutLeft { + animation-name: zoomOutLeft; +} +.zoomOutRight { + animation-name: zoomOutRight; +} +.zoomOutUp { + animation-name: zoomOutUp; +} +.slideOutDown { + animation-name: slideOutDown; +} +.slideOutLeft { + animation-name: slideOutLeft; +} +.slideOutRight { + animation-name: slideOutRight; +} +.slideOutUp { + animation-name: slideOutUp; +} + + +.bounce { + animation-name: bounce; + transform-origin: center bottom; +} +.flash { + animation-name: flash; +} +.pulse { + animation-name: pulse; +} +.rubberBand { + animation-name: rubberBand; +} +.shake { + animation-name: shake; +} +.headShake { + animation-timing-function: ease-in-out; + animation-name: headShake; +} +.swing { + transform-origin: top center; + animation-name: swing; +} +.tada { + animation-name: tada; +} +.wobble { + animation-name: wobble; +} +.jello { + animation-name: jello; + transform-origin: center; +} +.heartBeat { + animation-name: heartBeat; + animation-duration: 1.3s; + animation-timing-function: ease-in-out; +} + +.animated { + animation-duration: 0.5s; + animation-fill-mode: both; +} + +.animated-slow { + animation-duration: 0.8s; + animation-fill-mode: both; +} + +.animated-fast { + animation-duration: 0.3s; + animation-fill-mode: both; +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/colors.less b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/colors.less new file mode 100644 index 000000000..ee810288e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/colors.less @@ -0,0 +1,325 @@ +@red-50: #ffebee; +@red-100: #ffcdd2; +@red-200: #ef9a9a; +@red-300: #e57373; +@red-400: #ef5350; +@red-500: #f44336; +@red-600: #e53935; +@red-700: #d32f2f; +@red-800: #c62828; +@red-900: #b71c1c; +@red-A100: #ff8a80; +@red-A200: #ff5252; +@red-A400: #ff1744; +@red-A700: #d50000; +@red: @red-500; + + +@pink-50: #fce4ec; +@pink-100: #f8bbd0; +@pink-200: #f48fb1; +@pink-300: #f06292; +@pink-400: #ec407a; +@pink-500: #e91e63; +@pink-600: #d81b60; +@pink-700: #c2185b; +@pink-800: #ad1457; +@pink-900: #880e4f; +@pink-A100: #ff80ab; +@pink-A200: #ff4081; +@pink-A400: #f50057; +@pink-A700: #c51162; +@pink: @pink-500; + + +@purple-50: #f3e5f5; +@purple-100: #e1bee7; +@purple-200: #ce93d8; +@purple-300: #ba68c8; +@purple-400: #ab47bc; +@purple-500: #9c27b0; +@purple-600: #8e24aa; +@purple-700: #7b1fa2; +@purple-800: #6a1b9a; +@purple-900: #4a148c; +@purple-A100: #ea80fc; +@purple-A200: #e040fb; +@purple-A400: #d500f9; +@purple-A700: #aa00ff; +@purple: @purple-500; + + +@deep-purple-50: #ede7f6; +@deep-purple-100: #d1c4e9; +@deep-purple-200: #b39ddb; +@deep-purple-300: #9575cd; +@deep-purple-400: #7e57c2; +@deep-purple-500: #673ab7; +@deep-purple-600: #5e35b1; +@deep-purple-700: #512da8; +@deep-purple-800: #4527a0; +@deep-purple-900: #311b92; +@deep-purple-A100: #b388ff; +@deep-purple-A200: #7c4dff; +@deep-purple-A400: #651fff; +@deep-purple-A700: #6200ea; +@deep-purple: @deep-purple-500; + + +@indigo-50: #e8eaf6; +@indigo-100: #c5cae9; +@indigo-200: #9fa8da; +@indigo-300: #7986cb; +@indigo-400: #5c6bc0; +@indigo-500: #3f51b5; +@indigo-600: #3949ab; +@indigo-700: #303f9f; +@indigo-800: #283593; +@indigo-900: #1a237e; +@indigo-A100: #8c9eff; +@indigo-A200: #536dfe; +@indigo-A400: #3d5afe; +@indigo-A700: #304ffe; +@indigo: @indigo-500; + + +@blue-50: #e3f2fd; +@blue-100: #bbdefb; +@blue-200: #90caf9; +@blue-300: #64b5f6; +@blue-400: #42a5f5; +@blue-500: #2196f3; +@blue-600: #1e88e5; +@blue-700: #1976d2; +@blue-800: #1565c0; +@blue-900: #0d47a1; +@blue-A100: #82b1ff; +@blue-A200: #448aff; +@blue-A400: #2979ff; +@blue-A700: #2962ff; +@blue: @blue-500; + + +@light-blue-50: #e1f5fe; +@light-blue-100: #b3e5fc; +@light-blue-200: #81d4fa; +@light-blue-300: #4fc3f7; +@light-blue-400: #29b6f6; +@light-blue-500: #03a9f4; +@light-blue-600: #039be5; +@light-blue-700: #0288d1; +@light-blue-800: #0277bd; +@light-blue-900: #01579b; +@light-blue-A100: #80d8ff; +@light-blue-A200: #40c4ff; +@light-blue-A400: #00b0ff; +@light-blue-A700: #0091ea; +@light-blue: @light-blue-500; + + +@cyan-50: #e0f7fa; +@cyan-100: #b2ebf2; +@cyan-200: #80deea; +@cyan-300: #4dd0e1; +@cyan-400: #26c6da; +@cyan-500: #00bcd4; +@cyan-600: #00acc1; +@cyan-700: #0097a7; +@cyan-800: #00838f; +@cyan-900: #006064; +@cyan-A100: #84ffff; +@cyan-A200: #18ffff; +@cyan-A400: #00e5ff; +@cyan-A700: #00b8d4; +@cyan: @cyan-500; + + +@teal-50: #e0f2f1; +@teal-100: #b2dfdb; +@teal-200: #80cbc4; +@teal-300: #4db6ac; +@teal-400: #26a69a; +@teal-500: #009688; +@teal-600: #00897b; +@teal-700: #00796b; +@teal-800: #00695c; +@teal-900: #004d40; +@teal-A100: #a7ffeb; +@teal-A200: #64ffda; +@teal-A400: #1de9b6; +@teal-A700: #00bfa5; +@teal: @teal-500; + + +@green-50: #e8f5e9; +@green-100: #c8e6c9; +@green-200: #a5d6a7; +@green-300: #81c784; +@green-400: #66bb6a; +@green-500: #4caf50; +@green-600: #43a047; +@green-700: #388e3c; +@green-800: #2e7d32; +@green-900: #1b5e20; +@green-A100: #b9f6ca; +@green-A200: #69f0ae; +@green-A400: #00e676; +@green-A700: #00c853; +@green: @green-500; + + +@light-green-50: #f1f8e9; +@light-green-100: #dcedc8; +@light-green-200: #c5e1a5; +@light-green-300: #aed581; +@light-green-400: #9ccc65; +@light-green-500: #8bc34a; +@light-green-600: #7cb342; +@light-green-700: #689f38; +@light-green-800: #558b2f; +@light-green-900: #33691e; +@light-green-A100: #ccff90; +@light-green-A200: #b2ff59; +@light-green-A400: #76ff03; +@light-green-A700: #64dd17; +@light-green: @light-green-500; + + +@lime-50: #f9fbe7; +@lime-100: #f0f4c3; +@lime-200: #e6ee9c; +@lime-300: #dce775; +@lime-400: #d4e157; +@lime-500: #cddc39; +@lime-600: #c0ca33; +@lime-700: #afb42b; +@lime-800: #9e9d24; +@lime-900: #827717; +@lime-A100: #f4ff81; +@lime-A200: #eeff41; +@lime-A400: #c6ff00; +@lime-A700: #aeea00; +@lime: @lime-500; + + +@yellow-50: #fffde7; +@yellow-100: #fff9c4; +@yellow-200: #fff59d; +@yellow-300: #fff176; +@yellow-400: #ffee58; +@yellow-500: #fec60a; +@yellow-600: #fdd835; +@yellow-700: #fbc02d; +@yellow-800: #f9a825; +@yellow-900: #f57f17; +@yellow-A100: #ffff8d; +@yellow-A200: #ffff00; +@yellow-A400: #ffea00; +@yellow-A700: #ffd600; +@yellow: @yellow-700; + + +@amber-50: #fff8e1; +@amber-100: #ffecb3; +@amber-200: #ffe082; +@amber-300: #ffd54f; +@amber-400: #ffca28; +@amber-500: #ffc107; +@amber-600: #ffb300; +@amber-700: #ffa000; +@amber-800: #ff8f00; +@amber-900: #ff6f00; +@amber-A100: #ffe57f; +@amber-A200: #ffd740; +@amber-A400: #ffc400; +@amber-A700: #ffab00; +@amber: @amber-500; + + +@orange-50: #fff3e0; +@orange-100: #ffe0b2; +@orange-200: #ffcc80; +@orange-300: #ffb74d; +@orange-400: #ffa726; +@orange-500: #ff9800; +@orange-600: #fb8c00; +@orange-700: #f57c00; +@orange-800: #ef6c00; +@orange-900: #e65100; +@orange-A100: #ffd180; +@orange-A200: #ffab40; +@orange-A400: #ff9100; +@orange-A700: #ff6d00; +@orange: @orange-500; + + +@deep-orange-50: #fbe9e7; +@deep-orange-100: #ffccbc; +@deep-orange-200: #ffab91; +@deep-orange-300: #ff8a65; +@deep-orange-400: #ff7043; +@deep-orange-500: #ff5722; +@deep-orange-600: #f4511e; +@deep-orange-700: #e64a19; +@deep-orange-800: #d84315; +@deep-orange-900: #bf360c; +@deep-orange-A100: #ff9e80; +@deep-orange-A200: #ff6e40; +@deep-orange-A400: #ff3d00; +@deep-orange-A700: #dd2c00; +@deep-orange: @deep-orange-500; + + +@brown-50: #efebe9; +@brown-100: #d7ccc8; +@brown-200: #bcaaa4; +@brown-300: #a1887f; +@brown-400: #8d6e63; +@brown-500: #795548; +@brown-600: #6d4c41; +@brown-700: #5d4037; +@brown-800: #4e342e; +@brown-900: #3e2723; +@brown-A100: #d7ccc8; +@brown-A200: #bcaaa4; +@brown-A400: #8d6e63; +@brown-A700: #5d4037; +@brown: @brown-500; + + +@grey-50: #fafafa; +@grey-100: #f5f5f5; +@grey-200: #eeeeee; +@grey-300: #e0e0e0; +@grey-400: #bdbdbd; +@grey-500: #9e9e9e; @rgb-grey-500: "158, 158, 158"; +@grey-600: #757575; +@grey-700: #616161; +@grey-800: #424242; +@grey-900: #212121; +@grey-A100: #f5f5f5; +@grey-A200: #eeeeee; +@grey-A400: #bdbdbd; +@grey-A700: #616161; +@grey: @grey-500; + + +@blue-grey-50: #eceff1; +@blue-grey-100: #cfd8dc; +@blue-grey-200: #b0bec5; +@blue-grey-300: #90a4ae; +@blue-grey-400: #78909c; +@blue-grey-500: #607d8b; +@blue-grey-600: #546e7a; +@blue-grey-700: #455a64; +@blue-grey-800: #37474f; +@blue-grey-900: #263238; +@blue-grey-A100: #cfd8dc; +@blue-grey-A200: #b0bec5; +@blue-grey-A400: #78909c; +@blue-grey-A700: #455a64; +@blue-grey: @blue-grey-500; + + +@black: #000000; @rgb-black: "0,0,0"; +@white: #ffffff; @rgb-white: "255,255,255"; diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/index.less b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/index.less new file mode 100644 index 000000000..1e15d457e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/index.less @@ -0,0 +1,618 @@ +@import './reset.less'; +@import './animate.less'; +@import './layout.less'; + +*, *::after, *::before { + -webkit-user-drag: none; +} + +:root { + --color-primary: rgb(77, 175, 124); + --color-primary-alpha-100: rgba(77, 175, 124, 0.90); + --color-primary-alpha-200: rgba(77, 175, 124, 0.80); + --color-primary-alpha-300: rgba(77, 175, 124, 0.70); + --color-primary-alpha-400: rgba(77, 175, 124, 0.60); + --color-primary-alpha-500: rgba(77, 175, 124, 0.50); + --color-primary-alpha-600: rgba(77, 175, 124, 0.40); + --color-primary-alpha-700: rgba(77, 175, 124, 0.30); + --color-primary-alpha-800: rgba(77, 175, 124, 0.20); + --color-primary-alpha-900: rgba(77, 175, 124, 0.10); + --color-primary-dark-100: rgb(69,158,112); + --color-primary-dark-100-alpha-100: rgba(69, 158, 112, 0.90); + --color-primary-dark-100-alpha-200: rgba(69, 158, 112, 0.80); + --color-primary-dark-100-alpha-300: rgba(69, 158, 112, 0.70); + --color-primary-dark-100-alpha-400: rgba(69, 158, 112, 0.60); + --color-primary-dark-100-alpha-500: rgba(69, 158, 112, 0.50); + --color-primary-dark-100-alpha-600: rgba(69, 158, 112, 0.40); + --color-primary-dark-100-alpha-700: rgba(69, 158, 112, 0.30); + --color-primary-dark-100-alpha-800: rgba(69, 158, 112, 0.20); + --color-primary-dark-100-alpha-900: rgba(69, 158, 112, 0.10); + --color-primary-dark-200: rgb(62,142,101); + --color-primary-dark-200-alpha-100: rgba(62, 142, 101, 0.90); + --color-primary-dark-200-alpha-200: rgba(62, 142, 101, 0.80); + --color-primary-dark-200-alpha-300: rgba(62, 142, 101, 0.70); + --color-primary-dark-200-alpha-400: rgba(62, 142, 101, 0.60); + --color-primary-dark-200-alpha-500: rgba(62, 142, 101, 0.50); + --color-primary-dark-200-alpha-600: rgba(62, 142, 101, 0.40); + --color-primary-dark-200-alpha-700: rgba(62, 142, 101, 0.30); + --color-primary-dark-200-alpha-800: rgba(62, 142, 101, 0.20); + --color-primary-dark-200-alpha-900: rgba(62, 142, 101, 0.10); + --color-primary-dark-300: rgb(56,128,91); + --color-primary-dark-300-alpha-100: rgba(56, 128, 91, 0.90); + --color-primary-dark-300-alpha-200: rgba(56, 128, 91, 0.80); + --color-primary-dark-300-alpha-300: rgba(56, 128, 91, 0.70); + --color-primary-dark-300-alpha-400: rgba(56, 128, 91, 0.60); + --color-primary-dark-300-alpha-500: rgba(56, 128, 91, 0.50); + --color-primary-dark-300-alpha-600: rgba(56, 128, 91, 0.40); + --color-primary-dark-300-alpha-700: rgba(56, 128, 91, 0.30); + --color-primary-dark-300-alpha-800: rgba(56, 128, 91, 0.20); + --color-primary-dark-300-alpha-900: rgba(56, 128, 91, 0.10); + --color-primary-dark-400: rgb(50,115,82); + --color-primary-dark-400-alpha-100: rgba(50, 115, 82, 0.90); + --color-primary-dark-400-alpha-200: rgba(50, 115, 82, 0.80); + --color-primary-dark-400-alpha-300: rgba(50, 115, 82, 0.70); + --color-primary-dark-400-alpha-400: rgba(50, 115, 82, 0.60); + --color-primary-dark-400-alpha-500: rgba(50, 115, 82, 0.50); + --color-primary-dark-400-alpha-600: rgba(50, 115, 82, 0.40); + --color-primary-dark-400-alpha-700: rgba(50, 115, 82, 0.30); + --color-primary-dark-400-alpha-800: rgba(50, 115, 82, 0.20); + --color-primary-dark-400-alpha-900: rgba(50, 115, 82, 0.10); + --color-primary-dark-500: rgb(45,104,74); + --color-primary-dark-500-alpha-100: rgba(45, 104, 74, 0.90); + --color-primary-dark-500-alpha-200: rgba(45, 104, 74, 0.80); + --color-primary-dark-500-alpha-300: rgba(45, 104, 74, 0.70); + --color-primary-dark-500-alpha-400: rgba(45, 104, 74, 0.60); + --color-primary-dark-500-alpha-500: rgba(45, 104, 74, 0.50); + --color-primary-dark-500-alpha-600: rgba(45, 104, 74, 0.40); + --color-primary-dark-500-alpha-700: rgba(45, 104, 74, 0.30); + --color-primary-dark-500-alpha-800: rgba(45, 104, 74, 0.20); + --color-primary-dark-500-alpha-900: rgba(45, 104, 74, 0.10); + --color-primary-dark-600: rgb(41,94,67); + --color-primary-dark-600-alpha-100: rgba(41, 94, 67, 0.90); + --color-primary-dark-600-alpha-200: rgba(41, 94, 67, 0.80); + --color-primary-dark-600-alpha-300: rgba(41, 94, 67, 0.70); + --color-primary-dark-600-alpha-400: rgba(41, 94, 67, 0.60); + --color-primary-dark-600-alpha-500: rgba(41, 94, 67, 0.50); + --color-primary-dark-600-alpha-600: rgba(41, 94, 67, 0.40); + --color-primary-dark-600-alpha-700: rgba(41, 94, 67, 0.30); + --color-primary-dark-600-alpha-800: rgba(41, 94, 67, 0.20); + --color-primary-dark-600-alpha-900: rgba(41, 94, 67, 0.10); + --color-primary-dark-700: rgb(37,85,60); + --color-primary-dark-700-alpha-100: rgba(37, 85, 60, 0.90); + --color-primary-dark-700-alpha-200: rgba(37, 85, 60, 0.80); + --color-primary-dark-700-alpha-300: rgba(37, 85, 60, 0.70); + --color-primary-dark-700-alpha-400: rgba(37, 85, 60, 0.60); + --color-primary-dark-700-alpha-500: rgba(37, 85, 60, 0.50); + --color-primary-dark-700-alpha-600: rgba(37, 85, 60, 0.40); + --color-primary-dark-700-alpha-700: rgba(37, 85, 60, 0.30); + --color-primary-dark-700-alpha-800: rgba(37, 85, 60, 0.20); + --color-primary-dark-700-alpha-900: rgba(37, 85, 60, 0.10); + --color-primary-dark-800: rgb(33,77,54); + --color-primary-dark-800-alpha-100: rgba(33, 77, 54, 0.90); + --color-primary-dark-800-alpha-200: rgba(33, 77, 54, 0.80); + --color-primary-dark-800-alpha-300: rgba(33, 77, 54, 0.70); + --color-primary-dark-800-alpha-400: rgba(33, 77, 54, 0.60); + --color-primary-dark-800-alpha-500: rgba(33, 77, 54, 0.50); + --color-primary-dark-800-alpha-600: rgba(33, 77, 54, 0.40); + --color-primary-dark-800-alpha-700: rgba(33, 77, 54, 0.30); + --color-primary-dark-800-alpha-800: rgba(33, 77, 54, 0.20); + --color-primary-dark-800-alpha-900: rgba(33, 77, 54, 0.10); + --color-primary-dark-900: rgb(30,69,49); + --color-primary-dark-900-alpha-100: rgba(30, 69, 49, 0.90); + --color-primary-dark-900-alpha-200: rgba(30, 69, 49, 0.80); + --color-primary-dark-900-alpha-300: rgba(30, 69, 49, 0.70); + --color-primary-dark-900-alpha-400: rgba(30, 69, 49, 0.60); + --color-primary-dark-900-alpha-500: rgba(30, 69, 49, 0.50); + --color-primary-dark-900-alpha-600: rgba(30, 69, 49, 0.40); + --color-primary-dark-900-alpha-700: rgba(30, 69, 49, 0.30); + --color-primary-dark-900-alpha-800: rgba(30, 69, 49, 0.20); + --color-primary-dark-900-alpha-900: rgba(30, 69, 49, 0.10); + --color-primary-dark-1000: rgb(27,62,44); + --color-primary-dark-1000-alpha-100: rgba(27, 62, 44, 0.90); + --color-primary-dark-1000-alpha-200: rgba(27, 62, 44, 0.80); + --color-primary-dark-1000-alpha-300: rgba(27, 62, 44, 0.70); + --color-primary-dark-1000-alpha-400: rgba(27, 62, 44, 0.60); + --color-primary-dark-1000-alpha-500: rgba(27, 62, 44, 0.50); + --color-primary-dark-1000-alpha-600: rgba(27, 62, 44, 0.40); + --color-primary-dark-1000-alpha-700: rgba(27, 62, 44, 0.30); + --color-primary-dark-1000-alpha-800: rgba(27, 62, 44, 0.20); + --color-primary-dark-1000-alpha-900: rgba(27, 62, 44, 0.10); + --color-primary-light-100: rgb(113,191,150); + --color-primary-light-100-alpha-100: rgba(113, 191, 150, 0.90); + --color-primary-light-100-alpha-200: rgba(113, 191, 150, 0.80); + --color-primary-light-100-alpha-300: rgba(113, 191, 150, 0.70); + --color-primary-light-100-alpha-400: rgba(113, 191, 150, 0.60); + --color-primary-light-100-alpha-500: rgba(113, 191, 150, 0.50); + --color-primary-light-100-alpha-600: rgba(113, 191, 150, 0.40); + --color-primary-light-100-alpha-700: rgba(113, 191, 150, 0.30); + --color-primary-light-100-alpha-800: rgba(113, 191, 150, 0.20); + --color-primary-light-100-alpha-900: rgba(113, 191, 150, 0.10); + --color-primary-light-200: rgb(141,204,171); + --color-primary-light-200-alpha-100: rgba(141, 204, 171, 0.90); + --color-primary-light-200-alpha-200: rgba(141, 204, 171, 0.80); + --color-primary-light-200-alpha-300: rgba(141, 204, 171, 0.70); + --color-primary-light-200-alpha-400: rgba(141, 204, 171, 0.60); + --color-primary-light-200-alpha-500: rgba(141, 204, 171, 0.50); + --color-primary-light-200-alpha-600: rgba(141, 204, 171, 0.40); + --color-primary-light-200-alpha-700: rgba(141, 204, 171, 0.30); + --color-primary-light-200-alpha-800: rgba(141, 204, 171, 0.20); + --color-primary-light-200-alpha-900: rgba(141, 204, 171, 0.10); + --color-primary-light-300: rgb(164,214,188); + --color-primary-light-300-alpha-100: rgba(164, 214, 188, 0.90); + --color-primary-light-300-alpha-200: rgba(164, 214, 188, 0.80); + --color-primary-light-300-alpha-300: rgba(164, 214, 188, 0.70); + --color-primary-light-300-alpha-400: rgba(164, 214, 188, 0.60); + --color-primary-light-300-alpha-500: rgba(164, 214, 188, 0.50); + --color-primary-light-300-alpha-600: rgba(164, 214, 188, 0.40); + --color-primary-light-300-alpha-700: rgba(164, 214, 188, 0.30); + --color-primary-light-300-alpha-800: rgba(164, 214, 188, 0.20); + --color-primary-light-300-alpha-900: rgba(164, 214, 188, 0.10); + --color-primary-light-400: rgb(182,222,201); + --color-primary-light-400-alpha-100: rgba(182, 222, 201, 0.90); + --color-primary-light-400-alpha-200: rgba(182, 222, 201, 0.80); + --color-primary-light-400-alpha-300: rgba(182, 222, 201, 0.70); + --color-primary-light-400-alpha-400: rgba(182, 222, 201, 0.60); + --color-primary-light-400-alpha-500: rgba(182, 222, 201, 0.50); + --color-primary-light-400-alpha-600: rgba(182, 222, 201, 0.40); + --color-primary-light-400-alpha-700: rgba(182, 222, 201, 0.30); + --color-primary-light-400-alpha-800: rgba(182, 222, 201, 0.20); + --color-primary-light-400-alpha-900: rgba(182, 222, 201, 0.10); + --color-primary-light-500: rgb(197,229,212); + --color-primary-light-500-alpha-100: rgba(197, 229, 212, 0.90); + --color-primary-light-500-alpha-200: rgba(197, 229, 212, 0.80); + --color-primary-light-500-alpha-300: rgba(197, 229, 212, 0.70); + --color-primary-light-500-alpha-400: rgba(197, 229, 212, 0.60); + --color-primary-light-500-alpha-500: rgba(197, 229, 212, 0.50); + --color-primary-light-500-alpha-600: rgba(197, 229, 212, 0.40); + --color-primary-light-500-alpha-700: rgba(197, 229, 212, 0.30); + --color-primary-light-500-alpha-800: rgba(197, 229, 212, 0.20); + --color-primary-light-500-alpha-900: rgba(197, 229, 212, 0.10); + --color-primary-light-600: rgb(209,234,221); + --color-primary-light-600-alpha-100: rgba(209, 234, 221, 0.90); + --color-primary-light-600-alpha-200: rgba(209, 234, 221, 0.80); + --color-primary-light-600-alpha-300: rgba(209, 234, 221, 0.70); + --color-primary-light-600-alpha-400: rgba(209, 234, 221, 0.60); + --color-primary-light-600-alpha-500: rgba(209, 234, 221, 0.50); + --color-primary-light-600-alpha-600: rgba(209, 234, 221, 0.40); + --color-primary-light-600-alpha-700: rgba(209, 234, 221, 0.30); + --color-primary-light-600-alpha-800: rgba(209, 234, 221, 0.20); + --color-primary-light-600-alpha-900: rgba(209, 234, 221, 0.10); + --color-primary-light-700: rgb(218,238,228); + --color-primary-light-700-alpha-100: rgba(218, 238, 228, 0.90); + --color-primary-light-700-alpha-200: rgba(218, 238, 228, 0.80); + --color-primary-light-700-alpha-300: rgba(218, 238, 228, 0.70); + --color-primary-light-700-alpha-400: rgba(218, 238, 228, 0.60); + --color-primary-light-700-alpha-500: rgba(218, 238, 228, 0.50); + --color-primary-light-700-alpha-600: rgba(218, 238, 228, 0.40); + --color-primary-light-700-alpha-700: rgba(218, 238, 228, 0.30); + --color-primary-light-700-alpha-800: rgba(218, 238, 228, 0.20); + --color-primary-light-700-alpha-900: rgba(218, 238, 228, 0.10); + --color-primary-light-800: rgb(225,241,233); + --color-primary-light-800-alpha-100: rgba(225, 241, 233, 0.90); + --color-primary-light-800-alpha-200: rgba(225, 241, 233, 0.80); + --color-primary-light-800-alpha-300: rgba(225, 241, 233, 0.70); + --color-primary-light-800-alpha-400: rgba(225, 241, 233, 0.60); + --color-primary-light-800-alpha-500: rgba(225, 241, 233, 0.50); + --color-primary-light-800-alpha-600: rgba(225, 241, 233, 0.40); + --color-primary-light-800-alpha-700: rgba(225, 241, 233, 0.30); + --color-primary-light-800-alpha-800: rgba(225, 241, 233, 0.20); + --color-primary-light-800-alpha-900: rgba(225, 241, 233, 0.10); + --color-primary-light-900: rgb(231,244,237); + --color-primary-light-900-alpha-100: rgba(231, 244, 237, 0.90); + --color-primary-light-900-alpha-200: rgba(231, 244, 237, 0.80); + --color-primary-light-900-alpha-300: rgba(231, 244, 237, 0.70); + --color-primary-light-900-alpha-400: rgba(231, 244, 237, 0.60); + --color-primary-light-900-alpha-500: rgba(231, 244, 237, 0.50); + --color-primary-light-900-alpha-600: rgba(231, 244, 237, 0.40); + --color-primary-light-900-alpha-700: rgba(231, 244, 237, 0.30); + --color-primary-light-900-alpha-800: rgba(231, 244, 237, 0.20); + --color-primary-light-900-alpha-900: rgba(231, 244, 237, 0.10); + --color-primary-light-1000: rgb(255,255,255); + --color-primary-light-1000-alpha-100: rgba(255, 255, 255, 0.90); + --color-primary-light-1000-alpha-200: rgba(255, 255, 255, 0.80); + --color-primary-light-1000-alpha-300: rgba(255, 255, 255, 0.70); + --color-primary-light-1000-alpha-400: rgba(255, 255, 255, 0.60); + --color-primary-light-1000-alpha-500: rgba(255, 255, 255, 0.50); + --color-primary-light-1000-alpha-600: rgba(255, 255, 255, 0.40); + --color-primary-light-1000-alpha-700: rgba(255, 255, 255, 0.30); + --color-primary-light-1000-alpha-800: rgba(255, 255, 255, 0.20); + --color-primary-light-1000-alpha-900: rgba(255, 255, 255, 0.10); + --color-theme: rgb(77, 175, 124); + // --color-scrollbar-track: + + // --color-900: #fff; + // --color-800: #fafafa; + // --color-700: #f5f5f5; + // --color-600: #eeeeee; + // --color-500: #e0e0e0; + // --color-400: #bdbdbd; + // --color-300: #9e9e9e; + // --color-200: #757575; + // --color-100: #616161; + // --color-050: #424242; + // --color-000: #212121; + // --color-000: #fff; + // --color-050: #fafafa; + // --color-100: #f5f5f5; + // --color-200: #eeeeee; + // --color-300: #e0e0e0; + // --color-400: #bdbdbd; + // --color-500: #9e9e9e; + // --color-600: #757575; + // --color-700: #616161; + // --color-800: #424242; + // --color-900: #212121; + + --color-000: rgb(255,255,255); + --color-050: rgb(244,244,244); + --color-100: rgb(233,233,233); + --color-150: rgb(222,222,222); + --color-200: rgb(211,211,211); + --color-250: rgb(200,200,200); + --color-300: rgb(188,188,188); + --color-350: rgb(177,177,177); + --color-400: rgb(166,166,166); + --color-450: rgb(155,155,155); + --color-500: rgb(144,144,144); + --color-550: rgb(133,133,133); + --color-600: rgb(122,122,122); + --color-650: rgb(111,111,111); + --color-700: rgb(100,100,100); + --color-750: rgb(89,89,89); + --color-800: rgb(77,77,77); + --color-850: rgb(66,66,66); + --color-900: rgb(55,55,55); + --color-950: rgb(44,44,44); + --color-1000: rgb(33, 33, 33); + + --color-app-background: var(--color-primary-light-600-alpha-600); + --color-main-background: rgba(255, 255, 255, 0.9); + --color-nav-font: var(--color-primary); + // --color-app-background: rgba(0, 0, 0, .5); + // --color-main-background: rgba(0, 0, 0, 0.26); + --color-btn-hide: #3bc2b2; + --color-btn-min: #85c43b; + // --color-btn-max: #e7aa36; + --color-btn-close: #fab4a0; + + --color-badge-primary: var(--color-primary); + --color-badge-secondary: #4baed5; + --color-badge-tertiary: #e7aa36; + + --color-font: var(--color-850); + --color-font-label: var(--color-450); + --color-primary-font: var(--color-primary); + --color-primary-font-hover: var(--color-primary-alpha-300); + --color-primary-font-active: var(--color-primary-dark-100-alpha-200); + --color-primary-background: var(--color-primary-light-400-alpha-700); + --color-primary-background-hover: var(--color-primary-light-300-alpha-800); + --color-primary-background-active: var(--color-primary-light-100-alpha-800); + --color-button-font: var(--color-primary-alpha-100); + --color-button-font-selected: var(--color-primary-dark-100-alpha-100); + --color-button-background: var(--color-primary-light-400-alpha-700); + --color-button-background-selected: var(--color-primary-alpha-600); + --color-button-background-hover: var(--color-primary-light-300-alpha-600); + --color-button-background-active: var(--color-primary-light-100-alpha-600); + --color-list-header-border-bottom: 1px solid var(--color-primary-alpha-900); + --color-content-background: var(--color-primary-light-1000); + + + --background-image: none; + --background-image-position: center; + --background-image-size: cover; +} + +html { + font-size: 16px; +} + +.nobreak { + white-space: nowrap; +} + +.auto-hidden { + .mixin-ellipsis-1(); +} + +.center { + text-align: center; +} + +.break { + word-break: break-all; +} + +.select { + user-select: text; +} +.no-select { + user-select: none; +} + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: .7em; + // font-weight: 700; + line-height: 1.2; + text-align: center; + white-space: nowrap; + // vertical-align: baseline; + vertical-align: text-top; + // border-radius: 2px; + + // &.badge-light { + // background-color: #f8f9fa; + // } + // &.badge-secondary { + // color: #fff; + // background-color: #6c757d; + // } + // &.badge-info { + // color: #fff; + // background-color: #4baed5; + // } + // &.badge-warning { + // color: #fff; + // background-color: #ffa45a; + // } + // &.badge-danger { + // color: #fff; + // background-color: #ff705a; + // } + // &.badge-success { + // color: #fff; + // background-color: #32bc63; + // } + &.badge-theme-primary { + color: var(--color-badge-primary); + } + &.badge-theme-secondary { + color: var(--color-badge-secondary); + } + &.badge-theme-tertiary { + color: var(--color-badge-tertiary); + } +} + +small { + font-size: .8em; +} +.small { + font-size: .9em; +} +.tip { + color: var(--color-label); +} +strong { + font-weight: bold; +} + +.underline { + text-decoration: underline; +} + +svg { + transition: @transition-normal; + transition-property: fill; +} + +button, input, textarea, a { + color: var(--color-font); +} + +input, textarea { + &::placeholder { + color: var(--color-font-label); + } +} +::selection { + color: var(--color-primary-dark-500-alpha-200); + background: var(--color-primary-light-100-alpha-500); +} + +.hover, a { + cursor: pointer; + transition: color .2s ease; + &:hover { + color: var(--color-primary-font-hover); + } + &:active { + color: var(--color-primary-font-active); + } +} + +.scroll { + overflow: auto; + &::-webkit-scrollbar { + width: 6px; + height: 6px; + background-color: rgba(0, 0, 0, 0); + } + &::-webkit-scrollbar-track { + background-color: var(--color-primary-light-100-alpha-800); + border-radius: 3px; + // background-color: rgba(0, 0, 0, 0.1); + } + &::-webkit-scrollbar-thumb { + border-radius: 3px; + background-color: var(--color-primary-alpha-600); + // background-color: rgba(0, 0, 0, 0.2); + transition: background-color 0.4s ease; + } + &::-webkit-scrollbar-thumb:hover { + border-radius: 3px; + background-color: var(--color-primary-alpha-400); + // background-color: rgba(0, 0, 0, 0.4); + transition: background-color 0.4s ease; + } +} + + +.thead { + border-bottom: var(--color-list-header-border-bottom); + padding-right: 6px; + flex: none; + + .num { + .nobreak(); + .center(); + color: var(--color-font-label); + } + // box-shadow: 0 0 2px var(--color-primary-dark-500-alpha-800); + // position: relative; + // z-index: 2; +} +table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + overflow: hidden; + color: var(--color-font); + th { + font-size: 12px; + text-align: left; + line-height: 38px; + padding: 0 6px; + } +} + + +.list { + width: 100%; + overflow: hidden; + color: var(--color-font); + flex: auto; + + + .list-item { + height: 100%; + display: flex; + flex-flow: row nowrap; + align-items: center; + // border-top: 1px solid rgba(0, 0, 0, 0.12); + transition: 0.2s ease; + transition-property: background-color, color; + // border-bottom: 1px solid @color-theme_2-line; + box-sizing: border-box; + font-size: 12px; + + &:hover { + background-color: var(--color-primary-background-hover); + + // .list-item-cell-action { + // display: block; + // } + } + &.active { + background-color: var(--color-primary-background-active); + } + &.selected { + background-color: var(--color-primary-background-hover); + } + &.disabled { + opacity: .5; + } + .list-item-cell { + flex: none; + padding: 0 6px; + position: relative; + // transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1); + line-height: 16px; + vertical-align: middle; + box-sizing: border-box; + .mixin-ellipsis-1(); + + &.auto { + flex: auto; + } + + &.num, .num { + .nobreak(); + .center(); + padding-left: 3px; + padding-right: 3px; + font-size: 11px; + color: var(--color-font-label); + } + + &.name { + display: flex; + flex-flow: row nowrap; + overflow: hidden; + white-space: initial; + text-overflow: initial; + align-items: center; + + >.name { + .mixin-ellipsis-1(); + } + } + .badge { + margin-left: 3px; + opacity: .85; + } + } + + // .list-item-cell-action { + // white-space: nowrap; + // display: none; + // flex: auto; + // text-align: right; + // // position: absolute; + // // right: 5px; + // // top: -2px; + // // opacity: 0; + // // transition: opacity .1s ease; + // } + } +} + +.copying { + .no-select { + display: none !important; + } +} + +.gap-left { + + .gap-left { + margin-left: 20px; + } +} +.gap-top { + &.top { + margin-top: 25px; + } + + + .gap-top { + margin-top: 10px; + } +} + +.color-picker { + border-radius: @radius-border !important; +} + +.list-active-enter-active, +.list-active-leave-active { + transition: .13s ease; + transition-property: width, opacity; +} + +.list-active-enter-from, +.list-active-leave-to { + width: 0.25em !important; + opacity: 0; +} + +.play-active-enter-active, +.play-active-leave-active { + transition: .13s ease; + transition-property: transform, opacity; +} + +.play-active-enter-from, +.play-active-leave-to { + transform: scale(0.3); + opacity: 0; +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/layout.less b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/layout.less new file mode 100644 index 000000000..7819c1ec3 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/layout.less @@ -0,0 +1,35 @@ +@import './variables.less'; + + +/*自动隐藏文字*/ +.mixin-ellipsis-1() { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.mixin-ellipsis(@n: 1) { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + word-break: break-all; + white-space: normal !important; + -webkit-line-clamp: @n; + -webkit-box-orient: vertical; +} +.mixin-ellipsis-2() { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + word-break: break-all; + white-space: normal !important; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.mixin-after() { + display: block; + position: absolute; + content: ''; +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/reset.less b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/reset.less new file mode 100644 index 000000000..a7122da7d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/reset.less @@ -0,0 +1,100 @@ +// https://github.com/microsoft/vscode/blob/2dd0bca3954d4c03c427d6b447205b68817bd000/src/vs/workbench/browser/media/style.css +/* Font Families (with CJK support) */ + +.windows { font-family: "Segoe WPC", "Segoe UI", sans-serif; } +.windows:lang(zh-Hans) { font-family:"Microsoft YaHei", "Segoe WPC", "Segoe UI", sans-serif; } +.windows:lang(zh-Hant) { font-family:"Microsoft Jhenghei", "Segoe WPC", "Segoe UI", sans-serif; } +.windows:lang(ja) { font-family:"Yu Gothic UI", "Meiryo UI", "Segoe WPC", "Segoe UI", sans-serif; } +.windows:lang(ko) { font-family:"Malgun Gothic", "Dotom", "Segoe WPC", "Segoe UI", sans-serif; } + +.mac { font-family: -apple-system, BlinkMacSystemFont, sans-serif; } +.mac:lang(zh-Hans) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; } +.mac:lang(zh-Hant) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; } +.mac:lang(ja) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; } +.mac:lang(ko) { font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Nanum Gothic", "AppleGothic", sans-serif; } + +/* Linux: add `system-ui` as first font and not `Ubuntu` to allow other distribution pick their standard OS font */ +.linux { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; } +.linux:lang(zh-Hans) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } +.linux:lang(zh-Hant) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; } +.linux:lang(ja) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } +.linux:lang(ko) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } + + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + font-size: inherit; + vertical-align: baseline; +} +input, button, textarea { + font-family: inherit; +} +// html { +// font-family: +// // windows +// Segoe WPC,Segoe UI, +// Microsoft YaHei, +// Microsoft Jhenghei, +// Yu Gothic UI,Meiryo UI, +// Malgun Gothic,Dotom, + +// // mac +// -apple-system,BlinkMacSystemFont, +// PingFang SC, +// PingFang TC, +// Hiragino Kaku Gothic Pro, +// Apple SD Gothic Neo, +// Hiragino Sans GB,Nanum Gothic,AppleGothic, + +// // linux +// system-ui,Ubuntu,Droid Sans, +// Source Han Sans SC,Source Han Sans CN, +// Source Han Sans TC,Source Han Sans TW, +// Source Han Sans J,Source Han Sans JP, +// Source Han Sans K,Source Han Sans JR, +// Source Han Sans,UnDotum,FBaekmuk Gulim, + +// sans-serif; +// } + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +// html { +// } +body { + line-height: 1.2; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/variables.less b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/variables.less new file mode 100644 index 000000000..7672a97d1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/styles/variables.less @@ -0,0 +1,24 @@ +@import './colors.less'; + + +// Width +@width-app-left: 6.6%; + +// Height +@height-toolbar: 54px; +@height-player: 66px; + + +// Shadow +@shadow-app: 8px; + + +// Radius +@radius-progress-border: 5px; +@radius-border: 4px; + +@transition-slow: .6s ease; +@transition-normal: .4s ease; +@transition-fast: .3s ease; + +@form-radius: 3px; diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/angle-right-solid.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/angle-right-solid.svg new file mode 100644 index 000000000..470821a72 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/angle-right-solid.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/checkbox-blank.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/checkbox-blank.svg new file mode 100644 index 000000000..7d4be8154 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/checkbox-blank.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/fullscreen-exit.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/fullscreen-exit.svg new file mode 100644 index 000000000..347d636d7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/fullscreen-exit.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/headphones.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/headphones.svg new file mode 100644 index 000000000..a4fe31eac --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/headphones.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/help-circle-outline.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/help-circle-outline.svg new file mode 100644 index 000000000..ff236f9ad --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/help-circle-outline.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/information-slab-circle-outline.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/information-slab-circle-outline.svg new file mode 100644 index 000000000..2a15ec1b1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/information-slab-circle-outline.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/music.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/music.svg new file mode 100644 index 000000000..63f53bcfb --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/music.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/phone.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/phone.svg new file mode 100644 index 000000000..9614c3355 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/phone.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/play-outline.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/play-outline.svg new file mode 100644 index 000000000..1be9a9405 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/play-outline.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/plex.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/plex.svg new file mode 100644 index 000000000..8cfe565df --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/plex.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/plus.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/plus.svg new file mode 100644 index 000000000..bb280a85d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/share.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/share.svg new file mode 100644 index 000000000..c4f4b440c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/share.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/tune-variant.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/tune-variant.svg new file mode 100644 index 000000000..948d9191e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/tune-variant.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-high-outline.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-high-outline.svg new file mode 100644 index 000000000..0c3a83691 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-high-outline.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-low-outline.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-low-outline.svg new file mode 100644 index 000000000..52fa8a6b2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-low-outline.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-medium-outline.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-medium-outline.svg new file mode 100644 index 000000000..1610c1b06 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-medium-outline.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-mute-outline.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-mute-outline.svg new file mode 100644 index 000000000..ef3a87eca --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-mute-outline.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-off-outline.svg b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-off-outline.svg new file mode 100644 index 000000000..2ddc5b6d1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/assets/svgs/volume-off-outline.svg @@ -0,0 +1 @@ + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Btn.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Btn.vue new file mode 100644 index 000000000..38f5eee0b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Btn.vue @@ -0,0 +1,66 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Checkbox.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Checkbox.vue new file mode 100644 index 000000000..3a36d523b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Checkbox.vue @@ -0,0 +1,191 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Input.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Input.vue new file mode 100644 index 000000000..5f440f890 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Input.vue @@ -0,0 +1,128 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Menu.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Menu.vue new file mode 100644 index 000000000..f86972699 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Menu.vue @@ -0,0 +1,131 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/MusicList.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/MusicList.vue new file mode 100644 index 000000000..9b30af800 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/MusicList.vue @@ -0,0 +1,304 @@ + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Popup.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Popup.vue new file mode 100644 index 000000000..12032d576 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Popup.vue @@ -0,0 +1,172 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Selection.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Selection.vue new file mode 100644 index 000000000..f75088b96 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Selection.vue @@ -0,0 +1,217 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/SliderBar.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/SliderBar.vue new file mode 100644 index 000000000..0842c19a6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/SliderBar.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Tab.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Tab.vue new file mode 100644 index 000000000..8c2e8690d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/Tab.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/VirtualizedList.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/VirtualizedList.vue new file mode 100644 index 000000000..964ecab01 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/VirtualizedList.vue @@ -0,0 +1,325 @@ + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/useVirtualizedList.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/useVirtualizedList.ts new file mode 100644 index 000000000..daa3dfa06 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/base/useVirtualizedList.ts @@ -0,0 +1,228 @@ +// import { +// computed, +// ref, +// nextTick, +// watch, +// onMounted, +// onBeforeUnmount, +// } from 'vue' +// import { scrollTo } from '@common/utils/renderer' + +// interface ListItem { +// item: LX.Music.MusicInfo +// top: number +// style: { +// position: string +// left: number +// right: number +// top: string +// height: string +// } +// index: number +// key: string +// } + +// export default (props: { list: LX.Music.MusicInfo[], itemHeight: number }) => { +// const dom_scrollContainer = ref(null) +// const dom_list = ref(null) +// let startIndex = -1 +// let endIndex = -1 +// let scrollTop = -1 +// let cachedList: ListItem[] = [] +// let cancelScroll: null | (() => void) = null +// let isScrolling = false +// let scrollToValue = 0 + +// const createList = (startIndex: number, endIndex: number) => { +// if (startIndex == endIndex) return [] +// console.log(startIndex, endIndex) +// const cache = cachedList.slice(startIndex, endIndex) +// const list = props.list.slice(startIndex, endIndex).map((item, i) => { +// if (cache[i]) return cache[i] +// const top = (startIndex + i) * props.itemHeight +// const index = startIndex + i +// return cachedList[index] = { +// item, +// top, +// style: { position: 'absolute', left: 0, right: 0, top: `${top}px`, height: `${props.itemHeight}px` }, +// index, +// key: item.id, +// } +// }) +// return list +// } + +// // div.list-item(@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)" +// // :class="[{ selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]") +// // div.list-item-cell.nobreak.center(:style="{ width: rowWidth.r1 }" style="padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{ index + 1 }} +// // div.list-item-cell.auto(:style="{ width: rowWidth.r2 }" :aria-label="item.name + (item.meta._qualitys.flac32bit ? ` - ${$t('tag__lossless_24bit')}` : (item.meta._qualitys.ape || item.meta._qualitys.flac || item.meta._qualitys.wav) ? ` - ${$t('tag__lossless')}` : item.meta._qualitys['320k'] ? ` - ${$t('tag__high_quality')}` : '') + (sourceTag ? ` - ${item.source}` : '')") +// // span.select {{ item.name }} +// // span.badge.badge-theme-primary(:class="[$style.labelQuality, $style.noSelect]" v-if="item.meta._qualitys.flac32bit") {{ $t('tag__lossless_24bit') }} +// // span.badge.badge-theme-primary(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item.meta._qualitys.ape || item.meta._qualitys.flac || item.meta._qualitys.wav") {{ $t('tag__lossless') }} +// // span.badge.badge-theme-secondary(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item.meta._qualitys['320k']") {{ $t('tag__high_quality') }} +// // span.badge.badge-theme-tertiary(:class="[$style.labelQuality, $style.noSelect]" v-if="sourceTag") {{ item.source }} +// // div.list-item-cell(:style="{ width: rowWidth.r3 }" :aria-label="item.singer") +// // span.select {{ item.singer }} +// // div.list-item-cell(:style="{ width: rowWidth.r4 }" :aria-label="item.albumName") +// // span.select {{ item.meta.albumName }} +// // div.list-item-cell(:style="{ width: rowWidth.r5 }") +// // span(:class="[$style.time, $style.noSelect]") {{ item.meta.interval || '--/--' }} +// // div.list-item-cell(:style="{ width: rowWidth.r6 }" style="padding-left: 0; padding-right: 0;") +// // material-list-buttons(:index="index" :class="$style.btns" +// // :remove-btn="false" @btn-click="handleListBtnClick" +// // :download-btn="assertApiSupport(item.source)") +// const renderListItem = (list: ListItem) => { +// const dom_listItem = document.createElement('div') +// dom_listItem.className = 'list-item' +// } + +// const renderList = (list: ListItem[], type?: 'up' | 'down') => { +// if (!list.length) return +// console.log(list) +// const dom = document.createDocumentFragment() +// for (const item of list) { +// dom.appendChild(renderListItem(item)) +// } +// switch (type) { +// case 'up': +// break +// case 'down': +// break +// default: +// // console.log() +// break +// } +// } + +// const updateView = (force = false, currentScrollTop = dom_scrollContainer.value.scrollTop) => { +// // const currentScrollTop = this.$refs.dom_scrollContainer.scrollTop +// const itemHeight = props.itemHeight +// const currentStartIndex = Math.floor(currentScrollTop / itemHeight) +// const scrollContainerHeight = dom_scrollContainer.value.clientHeight +// const currentEndIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight) +// const continuous = currentStartIndex <= endIndex && currentEndIndex >= startIndex +// const currentStartRenderIndex = Math.max(currentStartIndex, 0) +// const currentEndRenderIndex = currentEndIndex + 1 +// // console.log(continuous, currentStartIndex, endIndex, currentEndIndex, startIndex) +// // debugger +// if (!force && continuous) { +// // if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * 0.6) return +// // console.log('update') +// if (currentScrollTop > scrollTop) { // scroll down +// console.log('scroll down') +// renderList(createList(endIndex + 1, currentEndRenderIndex)) +// // // views.value.push(...list.slice(list.indexOf(views.value[views.value.length - 1]) + 1)) +// // // // if (this.views.length > 100) { +// // // nextTick(() => { +// // // views.value.splice(0, views.value.indexOf(list[0])) +// // // }) +// // // } +// } else if (currentScrollTop < scrollTop) { // scroll up +// console.log('scroll up') +// renderList(createList(currentStartRenderIndex, startIndex)) +// // views.value = createList(currentStartRenderIndex, currentEndRenderIndex) +// } else return +// // if (currentScrollTop == scrollTop && endIndex >= currentEndIndex) return +// // views.value = createList(currentStartRenderIndex, currentEndRenderIndex) +// } else { +// renderList(createList(currentStartRenderIndex, currentEndRenderIndex)) +// } +// startIndex = currentStartIndex +// endIndex = currentEndIndex +// scrollTop = currentScrollTop +// } + +// const onScroll = event => { +// const currentScrollTop = dom_scrollContainer.value.scrollTop +// if (Math.abs(currentScrollTop - scrollTop) > props.itemHeight * 0.6) { +// updateView(false, currentScrollTop) +// } +// emit('scroll', event) +// } + +// const scrollTo = async(scrollTop, animate = false) => { +// return new Promise(resolve => { +// if (cancelScroll) { +// cancelScroll(resolve) +// } else { +// resolve() +// } +// }).then(async() => { +// return new Promise((resolve, reject) => { +// if (animate) { +// isScrolling = true +// scrollToValue = scrollTop +// cancelScroll = handleScroll(dom_scrollContainer.value, scrollTop, 300, () => { +// cancelScroll = null +// isScrolling = false +// resolve() +// }, () => { +// cancelScroll = null +// isScrolling = false +// reject('canceled') +// }) +// } else { +// dom_scrollContainer.value.scrollTop = scrollTop +// } +// }) +// }) +// } + +// const scrollToIndex = async(index, offset = 0, animate = false) => { +// return scrollTo(Math.max(index * props.itemHeight + offset, 0), animate) +// } + +// const getScrollTop = () => { +// return isScrolling ? scrollToValue : dom_scrollContainer.value.scrollTop +// } + +// const handleResize = () => { +// setTimeout(updateView) +// } + +// const contentStyle = computed(() => ({ +// display: 'block', +// height: props.list.length * props.itemHeight + 'px', +// })) + +// const handleReset = list => { +// cachedList = Array(list.length) +// startIndex = -1 +// endIndex = -1 +// void nextTick(() => { +// updateView(true) +// }) +// } +// watch(() => props.itemHeight, () => { +// handleReset(props.list) +// }) +// watch(() => props.list, (list) => { +// handleReset(list) +// }, { +// deep: true, +// }) + +// onMounted(() => { +// dom_scrollContainer.value!.addEventListener('scroll', onScroll, false) +// cachedList = Array(props.list.length) +// startIndex = -1 +// endIndex = -1 +// updateView(true) +// window.addEventListener('resize', handleResize) +// }) +// onBeforeUnmount(() => { +// dom_scrollContainer.value!.removeEventListener('scroll', onScroll) +// window.removeEventListener('resize', handleResize) +// if (cancelScroll) cancelScroll() +// }) + +// return { +// dom_scrollContainer, +// dom_list, +// contentStyle, +// scrollTo, +// scrollToIndex, +// getScrollTop, +// } +// } +export {} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/AudioVisualizer.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/AudioVisualizer.vue new file mode 100644 index 000000000..5aed4a44e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/AudioVisualizer.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/DownloadModal.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/DownloadModal.vue new file mode 100644 index 000000000..c2f3c44be --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/DownloadModal.vue @@ -0,0 +1,114 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/DownloadMultipleModal.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/DownloadMultipleModal.vue new file mode 100644 index 000000000..d4cb899ec --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/DownloadMultipleModal.vue @@ -0,0 +1,83 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/ListAddModal.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/ListAddModal.vue new file mode 100644 index 000000000..03cb5e302 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/ListAddModal.vue @@ -0,0 +1,293 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/ListAddMultipleModal.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/ListAddMultipleModal.vue new file mode 100644 index 000000000..4d587ecf2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/ListAddMultipleModal.vue @@ -0,0 +1,249 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/PlaybackRateBtn.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/PlaybackRateBtn.vue new file mode 100644 index 000000000..50cb02350 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/PlaybackRateBtn.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/ProgressBar.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/ProgressBar.vue new file mode 100644 index 000000000..19885cd2e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/ProgressBar.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AddConvolutionPresetBtn.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AddConvolutionPresetBtn.vue new file mode 100644 index 000000000..14336c9b6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AddConvolutionPresetBtn.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AddEQPresetBtn.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AddEQPresetBtn.vue new file mode 100644 index 000000000..5ce31709c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AddEQPresetBtn.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AudioConvolution.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AudioConvolution.vue new file mode 100644 index 000000000..2356d31a9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AudioConvolution.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AudioPanner.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AudioPanner.vue new file mode 100644 index 000000000..3f52cc340 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/AudioPanner.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/BiquadFilter.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/BiquadFilter.vue new file mode 100644 index 000000000..6d0946e2b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/BiquadFilter.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/PitchShifter.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/PitchShifter.vue new file mode 100644 index 000000000..53a4e7066 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/PitchShifter.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/index.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/index.vue new file mode 100644 index 000000000..e709fc337 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/SoundEffectBtn/index.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/TogglePlayModeBtn.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/TogglePlayModeBtn.vue new file mode 100644 index 000000000..ab5f2e505 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/TogglePlayModeBtn.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/VolumeBtn.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/VolumeBtn.vue new file mode 100644 index 000000000..58c8e1814 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/common/VolumeBtn.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/index.js new file mode 100644 index 000000000..b1d3a6c0c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/index.js @@ -0,0 +1,25 @@ +import upperFirst from 'lodash/upperFirst' +import camelCase from 'lodash/camelCase' + +const requireComponent = require.context('./', true, /\.vue$/) + +const vueFileRxp = /\.vue$/ + +export default app => { + requireComponent.keys().forEach(fileName => { + const filePath = fileName.replace(/^\.\//, '') + + if (!filePath.split('/').every((path, index, arr) => { + const char = path.charAt(0) + return vueFileRxp.test(path) || char.toUpperCase() !== char || arr[index + 1] == 'index.vue' + })) return + + const componentConfig = requireComponent(fileName) + + let componentName = upperFirst(camelCase(filePath.replace(/\.\w+$/, ''))) + + if (componentName.endsWith('Index')) componentName = componentName.replace(/Index$/, '') + + app.component(componentName, componentConfig.default || componentConfig) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Aside/ControlBtns.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Aside/ControlBtns.vue new file mode 100644 index 000000000..3a211bf5e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Aside/ControlBtns.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Aside/NavBar.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Aside/NavBar.vue new file mode 100644 index 000000000..c0f7826d8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Aside/NavBar.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Aside/index.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Aside/index.vue new file mode 100644 index 000000000..50f47c573 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Aside/index.vue @@ -0,0 +1,55 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/ChangeLogModal.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/ChangeLogModal.vue new file mode 100644 index 000000000..9d78c5d5c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/ChangeLogModal.vue @@ -0,0 +1,211 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Icons.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Icons.vue new file mode 100644 index 000000000..3ee78a985 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Icons.vue @@ -0,0 +1,226 @@ + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PactModal.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PactModal.vue new file mode 100644 index 000000000..866072146 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PactModal.vue @@ -0,0 +1,164 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/ControlBtns.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/ControlBtns.vue new file mode 100644 index 000000000..4601342fd --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/ControlBtns.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/FullWidthProgress.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/FullWidthProgress.vue new file mode 100644 index 000000000..2f5666d8b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/FullWidthProgress.vue @@ -0,0 +1,310 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/MiddleWidthProgress.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/MiddleWidthProgress.vue new file mode 100644 index 000000000..d101bd05c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/MiddleWidthProgress.vue @@ -0,0 +1,325 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/MiniWidthProgress.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/MiniWidthProgress.vue new file mode 100644 index 000000000..0a440dbe5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/MiniWidthProgress.vue @@ -0,0 +1,340 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/PlayProgress.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/PlayProgress.vue new file mode 100644 index 000000000..e84c03f1b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/PlayProgress.vue @@ -0,0 +1,185 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/index.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/index.vue new file mode 100644 index 000000000..51d23ef79 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayBar/index.vue @@ -0,0 +1,13 @@ + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/ControlBtnsLeftHeader.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/ControlBtnsLeftHeader.vue new file mode 100644 index 000000000..d510f84e9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/ControlBtnsLeftHeader.vue @@ -0,0 +1,163 @@ + + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/ControlBtnsRightHeader.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/ControlBtnsRightHeader.vue new file mode 100644 index 000000000..75481e0d1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/ControlBtnsRightHeader.vue @@ -0,0 +1,146 @@ + + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/LyricPlayer.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/LyricPlayer.vue new file mode 100644 index 000000000..d694b04d9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/LyricPlayer.vue @@ -0,0 +1,361 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/PlayBar.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/PlayBar.vue new file mode 100644 index 000000000..b0f5bd3d7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/PlayBar.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/autoHideMounse.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/autoHideMounse.js new file mode 100644 index 000000000..7aa271006 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/autoHideMounse.js @@ -0,0 +1,62 @@ +import { debounce } from '@common/utils/common' +let isAutoHide = false +let isLockedPointer = false +// let dom = null +let event = null +let isMouseDown = false + +const isControl = dom => { + if (!dom || dom === document.body) return false + // console.log(dom) + if (dom.getAttribute('aria-label') || dom.tagName == 'BUTTON') return true + return isControl(dom.parentNode) +} + +const lockPointer = () => { + if (!isAutoHide || isMouseDown) return + if (event && isControl(document.elementFromPoint(event.clientX, event.clientY))) return + + document.body.requestPointerLock() + isLockedPointer = true +} +const unLockPointer = () => { + if (!isLockedPointer) return + document.exitPointerLock() + isLockedPointer = false +} + +const startTimeout = debounce(lockPointer, 3000) + +const handleMouseMove = (_event) => { + event = _event + startTimeout() + unLockPointer() +} + +const handleMouseDown = () => { + isMouseDown = true +} +const handleMouseUp = () => { + isMouseDown = false + startTimeout() +} + +export const registerAutoHideMounse = () => { + if (isAutoHide) return + // if (!dom) dom = document.getElementById('root') + isAutoHide = true + document.body.addEventListener('mousemove', handleMouseMove) + document.body.addEventListener('mousedown', handleMouseDown) + document.body.addEventListener('mouseup', handleMouseUp) + startTimeout() +} + +export const unregisterAutoHideMounse = () => { + if (!isAutoHide) return + isAutoHide = false + // console.log(dom) + document.body.removeEventListener('mousemove', handleMouseMove) + document.body.removeEventListener('mousedown', handleMouseDown) + document.body.removeEventListener('mouseup', handleMouseUp) + unLockPointer() +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/ControlBtns.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/ControlBtns.vue new file mode 100644 index 000000000..1b06b7fa8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/ControlBtns.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/LyricMenu.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/LyricMenu.vue new file mode 100644 index 000000000..597523b4f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/LyricMenu.vue @@ -0,0 +1,296 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/MusicComment/CommentFloor.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/MusicComment/CommentFloor.vue new file mode 100644 index 000000000..1b60eb75f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/MusicComment/CommentFloor.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/MusicComment/index.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/MusicComment/index.vue new file mode 100644 index 000000000..6e33ab127 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/components/MusicComment/index.vue @@ -0,0 +1,367 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/index.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/index.vue new file mode 100644 index 000000000..a6631c0b8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/index.vue @@ -0,0 +1,282 @@ + + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/useSelectAllLrc.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/useSelectAllLrc.js new file mode 100644 index 000000000..80b706f47 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/PlayDetail/useSelectAllLrc.js @@ -0,0 +1,25 @@ +import { ref, onBeforeUnmount, onMounted } from '@common/utils/vueTools' + +export default () => { + const dom_lrc_select_content = ref() + const handle_key_mod_a_down = ({ event }) => { + if (event.target.tagName == 'INPUT' || !dom_lrc_select_content.value || document.activeElement != dom_lrc_select_content.value) return + event.preventDefault() + if (event.repeat) return + + let selection = window.getSelection() + let range = document.createRange() + range.selectNodeContents(dom_lrc_select_content.value) + selection.removeAllRanges() + selection.addRange(range) + } + + onMounted(() => { + window.key_event.on('key_mod+a_down', handle_key_mod_a_down) + }) + onBeforeUnmount(() => { + window.key_event.off('key_mod+a_down', handle_key_mod_a_down) + }) + + return dom_lrc_select_content +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/SyncAuthCodeModal.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/SyncAuthCodeModal.vue new file mode 100644 index 000000000..7f3463224 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/SyncAuthCodeModal.vue @@ -0,0 +1,109 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/SyncModeModal.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/SyncModeModal.vue new file mode 100644 index 000000000..5ed86f9de --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/SyncModeModal.vue @@ -0,0 +1,196 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Toolbar/ControlBtns.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Toolbar/ControlBtns.vue new file mode 100644 index 000000000..fe5dbfe3a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Toolbar/ControlBtns.vue @@ -0,0 +1,94 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Toolbar/SearchInput.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Toolbar/SearchInput.vue new file mode 100644 index 000000000..172b4f5bf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Toolbar/SearchInput.vue @@ -0,0 +1,113 @@ + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Toolbar/index.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Toolbar/index.vue new file mode 100644 index 000000000..b9a9e0479 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/Toolbar/index.vue @@ -0,0 +1,59 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/UpdateModal.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/UpdateModal.vue new file mode 100644 index 000000000..82d9f8415 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/UpdateModal.vue @@ -0,0 +1,319 @@ + + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/View.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/View.vue new file mode 100644 index 000000000..9eb44a6ae --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/layout/View.vue @@ -0,0 +1,28 @@ + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/ListButtons.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/ListButtons.vue new file mode 100644 index 000000000..afb2f1b70 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/ListButtons.vue @@ -0,0 +1,137 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/Modal.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/Modal.vue new file mode 100644 index 000000000..b594ce44c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/Modal.vue @@ -0,0 +1,316 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/index.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/index.vue new file mode 100644 index 000000000..fcdf52728 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/index.vue @@ -0,0 +1,340 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useList.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useList.ts new file mode 100644 index 000000000..52e47e8bc --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useList.ts @@ -0,0 +1,115 @@ +import { computed, watch, ref, onBeforeUnmount, type Ref } from '@common/utils/vueTools' +import { isFullscreen } from '@renderer/store' +import { appSetting } from '@renderer/store/setting' +import { getFontSizeWithScreen } from '@renderer/utils' + +const useKeyEvent = ({ handleSelectAllData, listRef }: { + handleSelectAllData: () => void + listRef: Ref +}) => { + const keyEvent = { + isShiftDown: false, + isModDown: false, + } + + const handle_key_shift_down = () => { + keyEvent.isShiftDown ||= true + } + const handle_key_shift_up = () => { + keyEvent.isShiftDown &&= false + } + const handle_key_mod_down = () => { + keyEvent.isModDown ||= true + } + const handle_key_mod_up = () => { + keyEvent.isModDown &&= false + } + const handle_key_mod_a_down = ({ event }: LX.KeyDownEevent) => { + if (!event || (event.target as HTMLElement).tagName == 'INPUT' || document.activeElement != listRef.value?.$el) return + event.preventDefault() + if (event.repeat) return + keyEvent.isModDown = false + handleSelectAllData() + } + + onBeforeUnmount(() => { + window.key_event.off('key_shift_down', handle_key_shift_down) + window.key_event.off('key_shift_up', handle_key_shift_up) + window.key_event.off('key_mod_down', handle_key_mod_down) + window.key_event.off('key_mod_up', handle_key_mod_up) + window.key_event.off('key_mod+a_down', handle_key_mod_a_down) + }) + window.key_event.on('key_shift_down', handle_key_shift_down) + window.key_event.on('key_shift_up', handle_key_shift_up) + window.key_event.on('key_mod_down', handle_key_mod_down) + window.key_event.on('key_mod_up', handle_key_mod_up) + window.key_event.on('key_mod+a_down', handle_key_mod_a_down) + + return keyEvent +} + + +export default ({ props, listRef }: { + props: { + list: LX.Music.MusicInfoOnline[] + } + listRef: Ref +}) => { + const selectedList = ref([]) + let lastSelectIndex = -1 + const listItemHeight = computed(() => { + return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : appSetting['common.fontSize']) * 2.3) + }) + + const removeAllSelect = () => { + selectedList.value = [] + } + const handleSelectAllData = () => { + removeAllSelect() + selectedList.value = [...props.list] + } + const keyEvent = useKeyEvent({ handleSelectAllData, listRef }) + + const handleSelectData = (clickIndex: number) => { + if (keyEvent.isShiftDown) { + if (selectedList.value.length) { + removeAllSelect() + if (lastSelectIndex != clickIndex) { + let isNeedReverse = false + let _lastSelectIndex = lastSelectIndex + if (clickIndex < _lastSelectIndex) { + let temp = _lastSelectIndex + _lastSelectIndex = clickIndex + clickIndex = temp + isNeedReverse = true + } + selectedList.value = props.list.slice(_lastSelectIndex, clickIndex + 1) + if (isNeedReverse) selectedList.value.reverse() + } + } else { + selectedList.value.push(props.list[clickIndex]) + lastSelectIndex = clickIndex + } + } else if (keyEvent.isModDown) { + lastSelectIndex = clickIndex + let item = props.list[clickIndex] + let index = selectedList.value.indexOf(item) + if (index < 0) { + selectedList.value.push(item) + } else { + selectedList.value.splice(index, 1) + } + } else if (selectedList.value.length) { + removeAllSelect() + } + } + + watch(() => props.list, removeAllSelect) + + return { + selectedList, + listItemHeight, + removeAllSelect, + handleSelectData, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMenu.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMenu.js new file mode 100644 index 000000000..e13ebff0e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMenu.js @@ -0,0 +1,137 @@ +import { computed, ref, reactive, nextTick } from '@common/utils/vueTools' +import musicSdk from '@renderer/utils/musicSdk' +import { useI18n } from '@renderer/plugins/i18n' +import { hasDislike } from '@renderer/core/dislikeList' + +export default ({ + props, + assertApiSupport, + emit, + + handleShowDownloadModal, + handlePlayMusic, + handlePlayMusicLater, + handleSearch, + handleShowMusicAddModal, + handleOpenMusicDetail, + handleDislikeMusic, +}) => { + const itemMenuControl = reactive({ + play: true, + addTo: true, + playLater: true, + download: true, + search: true, + sourceDetail: true, + dislike: true, + }) + const t = useI18n() + const menuLocation = reactive({ x: 0, y: 0 }) + const isShowItemMenu = ref(false) + + const menus = computed(() => { + return [ + { + name: t('list__play'), + action: 'play', + disabled: !itemMenuControl.play, + }, + { + name: t('list__download'), + action: 'download', + disabled: !itemMenuControl.download, + }, + { + name: t('list__play_later'), + action: 'playLater', + disabled: !itemMenuControl.playLater, + }, + { + name: t('list__search'), + action: 'search', + disabled: !itemMenuControl.search, + }, + { + name: t('list__add_to'), + action: 'addTo', + disabled: !itemMenuControl.addTo, + }, + { + name: t('list__source_detail'), + action: 'sourceDetail', + disabled: !itemMenuControl.sourceDetail, + }, + { + name: t('list__dislike'), + action: 'dislike', + disabled: !itemMenuControl.dislike, + }, + ] + }) + + const showMenu = (event, musicInfo) => { + itemMenuControl.sourceDetail = !!musicSdk[musicInfo.source]?.getMusicDetailPageUrl + // this.listMenu.itemMenuControl.play = + // this.listMenu.itemMenuControl.playLater = + itemMenuControl.download = assertApiSupport(musicInfo.source) + + itemMenuControl.dislike = !hasDislike(musicInfo) + + if (props.checkApiSource) { + itemMenuControl.playLater = + itemMenuControl.play = + itemMenuControl.download + } + + menuLocation.x = event.pageX + menuLocation.y = event.pageY + + if (isShowItemMenu.value) return + emit('show-menu') + nextTick(() => { + isShowItemMenu.value = true + }) + } + + const hideMenu = () => { + isShowItemMenu.value = false + } + + const menuClick = (action, index) => { + // console.log(action) + hideMenu() + if (!action) return + + switch (action.action) { + case 'download': + handleShowDownloadModal(index) + break + case 'play': + handlePlayMusic(index) + break + case 'playLater': + handlePlayMusicLater(index) + break + case 'search': + handleSearch(index) + break + case 'addTo': + handleShowMusicAddModal(index) + break + case 'sourceDetail': + handleOpenMusicDetail(index) + break + case 'dislike': + handleDislikeMusic(index) + break + } + } + + return { + menus, + menuLocation, + isShowItemMenu, + showMenu, + menuClick, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMusicActions.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMusicActions.js new file mode 100644 index 000000000..dd2dd7338 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMusicActions.js @@ -0,0 +1,53 @@ +import { useRouter } from '@common/utils/vueRouter' +import musicSdk from '@renderer/utils/musicSdk' +import { openUrl } from '@common/utils/electron' +import { toOldMusicInfo } from '@renderer/utils' +import { addDislikeInfo, hasDislike } from '@renderer/core/dislikeList' +import { playNext } from '@renderer/core/player' +import { playMusicInfo } from '@renderer/store/player/state' +import { dialog } from '@renderer/plugins/Dialog' +import { useI18n } from '@renderer/plugins/i18n' + + +export default ({ props }) => { + const router = useRouter() + const t = useI18n() + + const handleSearch = index => { + const info = props.list[index] + router.push({ + path: '/search', + query: { + text: `${info.name} ${info.singer}`, + }, + }) + } + + const handleOpenMusicDetail = index => { + const minfo = props.list[index] + const url = musicSdk[minfo.source]?.getMusicDetailPageUrl?.(toOldMusicInfo(minfo)) + if (!url) return + openUrl(url) + } + + const handleDislikeMusic = async(index) => { + const minfo = props.list[index] + const confirm = await dialog.confirm({ + message: minfo.singer ? t('lists__dislike_music_singer_tip', { name: minfo.name, singer: minfo.singer }) : t('lists__dislike_music_tip', { name: minfo.name }), + cancelButtonText: t('cancel_button_text_2'), + confirmButtonText: t('confirm_button_text'), + }) + if (!confirm) return + await addDislikeInfo([{ name: minfo.name, singer: minfo.singer }]) + if (hasDislike(playMusicInfo.musicInfo)) { + playNext(true) + } + } + + + return { + handleSearch, + handleOpenMusicDetail, + handleDislikeMusic, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMusicAdd.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMusicAdd.js new file mode 100644 index 000000000..7ecc38569 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMusicAdd.js @@ -0,0 +1,25 @@ +import { ref, nextTick } from '@common/utils/vueTools' + +export default ({ selectedList, props }) => { + const isShowListAdd = ref(false) + const isShowListAddMultiple = ref(false) + const selectedAddMusicInfo = ref(null) + + const handleShowMusicAddModal = (index, single) => { + if (selectedList.value.length && !single) { + isShowListAddMultiple.value = true + } else { + selectedAddMusicInfo.value = props.list[index] + nextTick(() => { + isShowListAdd.value = true + }) + } + } + + return { + isShowListAdd, + isShowListAddMultiple, + selectedAddMusicInfo, + handleShowMusicAddModal, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMusicDownload.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMusicDownload.js new file mode 100644 index 000000000..2cd50b216 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/useMusicDownload.js @@ -0,0 +1,25 @@ +import { ref, nextTick } from '@common/utils/vueTools' + +export default ({ selectedList, props }) => { + const isShowDownload = ref(false) + const isShowDownloadMultiple = ref(false) + const musicInfo = ref(null) + + const handleShowDownloadModal = (index, single) => { + if (selectedList.value.length && !single) { + isShowDownloadMultiple.value = true + } else { + musicInfo.value = props.list[index] + nextTick(() => { + isShowDownload.value = true + }) + } + } + + return { + isShowDownload, + isShowDownloadMultiple, + selectedDownloadMusicInfo: musicInfo, + handleShowDownloadModal, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/usePlay.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/usePlay.ts new file mode 100644 index 000000000..a7f59417e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/OnlineList/usePlay.ts @@ -0,0 +1,68 @@ +// import { useCommit } from '@common/utils/vueTools' +import { defaultList } from '@renderer/store/list/state' +import { getListMusics, addListMusics } from '@renderer/store/list/action' +import { addTempPlayList } from '@renderer/store/player/action' +import { appSetting } from '@renderer/store/setting' +import { type Ref } from '@common/utils/vueTools' +import { playList } from '@renderer/core/player' +import { LIST_IDS } from '@common/constants' + +export default ({ selectedList, props, removeAllSelect, emit }: { + selectedList: Ref + props: { + list: LX.Music.MusicInfoOnline[] + } + removeAllSelect: () => void + emit: (event: 'show-menu' | 'play-list' | 'togglePage', ...args: any[]) => void +}) => { + let clickTime = 0 + let clickIndex = -1 + + const handlePlayMusic = async(index: number, single: boolean) => { + let targetSong = props.list[index] + const defaultListMusics = await getListMusics(defaultList.id) + if (selectedList.value.length && !single) { + await addListMusics(defaultList.id, [...selectedList.value]) + removeAllSelect() + } else { + await addListMusics(defaultList.id, [targetSong]) + } + let targetIndex = defaultListMusics.findIndex(s => s.id === targetSong.id) + if (targetIndex > -1) { + playList(defaultList.id, targetIndex) + } + } + + const handlePlayMusicLater = (index: number, single: boolean) => { + if (selectedList.value.length && !single) { + addTempPlayList(selectedList.value.map(s => ({ listId: LIST_IDS.PLAY_LATER, musicInfo: s }))) + removeAllSelect() + } else { + addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo: props.list[index] }]) + } + } + + const doubleClickPlay = (index: number) => { + if ( + window.performance.now() - clickTime > 400 || + clickIndex !== index + ) { + clickTime = window.performance.now() + clickIndex = index + return + } + if (appSetting['list.isClickPlayList']) { + emit('play-list', index) + } else { + void handlePlayMusic(index, true) + } + clickTime = 0 + clickIndex = -1 + } + + return { + handlePlayMusic, + handlePlayMusicLater, + doubleClickPlay, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/Pagination.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/Pagination.vue new file mode 100644 index 000000000..4a04a5ed1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/Pagination.vue @@ -0,0 +1,191 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/PopupBtn.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/PopupBtn.vue new file mode 100644 index 000000000..2c594a19a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/PopupBtn.vue @@ -0,0 +1,66 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/SearchInput.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/SearchInput.vue new file mode 100644 index 000000000..2cef384e8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/SearchInput.vue @@ -0,0 +1,325 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/SongList.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/SongList.vue new file mode 100644 index 000000000..db17392a3 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/components/material/SongList.vue @@ -0,0 +1,18 @@ + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/apiSource.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/apiSource.ts new file mode 100644 index 000000000..e757da5c8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/apiSource.ts @@ -0,0 +1,48 @@ +import { apiSource, qualityList, userApi } from '@renderer/store' +import { appSetting, setApiSource } from '@renderer/store/setting' +import { setUserApi as setUserApiAction } from '@renderer/utils/ipc' +import musicSdk from '@renderer/utils/musicSdk' +import apiSourceInfo from '@renderer/utils/musicSdk/api-source-info' + +let prevId = '' +export const setUserApi = async(apiId: string) => { + if (prevId == apiId) return + prevId = apiId + if (window.lx.apiInitPromise[1]) { + window.lx.apiInitPromise[0] = new Promise(resolve => { + window.lx.apiInitPromise[1] = false + window.lx.apiInitPromise[2] = (result: boolean) => { + window.lx.apiInitPromise[1] = true + resolve(result) + } + }) + } + + if (/^user_api/.test(apiId)) { + qualityList.value = {} + userApi.status = false + userApi.message = 'initing' + + await setUserApiAction(apiId).then(() => { + if (prevId != apiId) return + apiSource.value = apiId + }).catch(err => { + if (prevId != apiId) return + if (!window.lx.apiInitPromise[1]) window.lx.apiInitPromise[2](false) + console.log(err) + let api = apiSourceInfo.find(api => !api.disabled) + if (!api) return + apiSource.value = api.id + if (api.id != appSetting['common.apiSource']) setApiSource(api.id) + }) + } else { + // @ts-expect-error + qualityList.value = musicSdk.supportQuality[apiId] ?? {} + apiSource.value = apiId + void setUserApiAction(apiId) + if (!window.lx.apiInitPromise[1]) window.lx.apiInitPromise[2](true) + } + + if (prevId != apiId) return + if (apiId != appSetting['common.apiSource']) setApiSource(apiId) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/dislikeList.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/dislikeList.ts new file mode 100644 index 000000000..501f43397 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/dislikeList.ts @@ -0,0 +1,51 @@ +// import { toRaw } from '@common/utils/vueTools' +import { DISLIKE_EVENT_NAME } from '@common/ipcNames' +import { rendererInvoke, rendererOff, rendererOn } from '@common/rendererIpc' +import { action } from '@renderer/store/dislikeList' + + +export const initDislikeInfo = async() => { + action.initDislikeInfo(await rendererInvoke(DISLIKE_EVENT_NAME.get_dislike_music_infos)) +} + +export const hasDislike = (info: LX.Music.MusicInfo | LX.Download.ListItem | null) => { + if (!info) return false + return action.hasDislike(info) +} + +export const addDislikeInfo = async(infos: LX.Dislike.DislikeMusicInfo[]) => { + await rendererInvoke(DISLIKE_EVENT_NAME.add_dislike_music_infos, infos) +} + +export const overwirteDislikeInfo = async(rules: string) => { + await rendererInvoke(DISLIKE_EVENT_NAME.overwrite_dislike_music_infos, rules) +} + +export const clearDislikeInfo = async() => { + await rendererInvoke(DISLIKE_EVENT_NAME.clear_dislike_music_infos) +} + + +const noop = () => {} + +export const registerRemoteDislikeAction = (onListChanged: (listIds: string[]) => void = noop) => { + const add_dislike_music_infos = ({ params: datas }: LX.IpcRendererEventParams) => { + action.addDislikeInfo(datas) + } + const overwrite_dislike_music_infos = ({ params: datas }: LX.IpcRendererEventParams) => { + action.overwirteDislikeInfo(datas) + } + const clear_dislike_music_infos = () => { + return action.clearDislikeInfo() + } + + rendererOn(DISLIKE_EVENT_NAME.add_dislike_music_infos, add_dislike_music_infos) + rendererOn(DISLIKE_EVENT_NAME.overwrite_dislike_music_infos, overwrite_dislike_music_infos) + rendererOn(DISLIKE_EVENT_NAME.clear_dislike_music_infos, clear_dislike_music_infos) + + return () => { + rendererOff(DISLIKE_EVENT_NAME.add_dislike_music_infos, add_dislike_music_infos) + rendererOff(DISLIKE_EVENT_NAME.overwrite_dislike_music_infos, overwrite_dislike_music_infos) + rendererOff(DISLIKE_EVENT_NAME.clear_dislike_music_infos, clear_dislike_music_infos) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/globalData.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/globalData.ts new file mode 100644 index 000000000..c9d27171e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/globalData.ts @@ -0,0 +1,34 @@ +// import defaultSetting from '@common/defaultSetting' +import createWorkers from '@renderer/worker' + +window.lx = { + // appSetting: defaultSetting, + isEditingHotKey: false, + isPlayedStop: false, + appHotKeyConfig: { + local: { + enable: false, + keys: {}, + }, + global: { + enable: false, + keys: {}, + }, + }, + songListInfo: { + fromName: '', + searchKey: '', + searchPosition: 0, + songlistKey: '', + songlistPosition: 0, + }, + restorePlayInfo: null, + worker: createWorkers(), + isProd: process.env.NODE_ENV == 'production', + rootOffset: window.dt ? 0 : 8, + apiInitPromise: [Promise.resolve(false), true, () => {}], +} + +window.lxData = {} + +window.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/lyric.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/lyric.ts new file mode 100644 index 000000000..fc5840fe2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/lyric.ts @@ -0,0 +1,243 @@ +import Lyric from '@common/utils/lyric-font-player' +import { getAnalyser, getCurrentTime as getPlayerCurrentTime } from '@renderer/plugins/player' +import { lyric, setLines, setOffset, setTempOffset, setText } from '@renderer/store/player/lyric' +import { isPlay, musicInfo } from '@renderer/store/player/state' +import { setStatusText } from '@renderer/store/player/action' +import { markRawList } from '@common/utils/vueTools' +import { appSetting } from '@renderer/store/setting' +import { onNewDesktopLyricProcess } from '@renderer/utils/ipc' + +const getCurrentTime = () => { + return getPlayerCurrentTime() * 1000 +} + +let lrc: Lyric +let desktopLyricPort: Electron.IpcRendererEvent['ports'][0] | null = null +const analyserTools: { + dataArray: Uint8Array + bufferLength: number + analyser: AnalyserNode | null + sendDataArray: () => void +} = { + dataArray: new Uint8Array(), + bufferLength: 0, + analyser: null, + sendDataArray() { + if (this.analyser == null) { + this.analyser = getAnalyser() + // console.log(this.analyser) + if (!this.analyser) return + this.bufferLength = this.analyser.frequencyBinCount + } + const dataArray = new Uint8Array(this.bufferLength) + this.analyser.getByteFrequencyData(dataArray) + sendDesktopLyricInfo({ + action: 'send_analyser_data_array', + data: dataArray, + }, [dataArray.buffer]) + }, +} + +export const sendDesktopLyricInfo = (info: LX.DesktopLyric.LyricActions, transferList?: Transferable[]) => { + if (desktopLyricPort == null) return + if (transferList) desktopLyricPort.postMessage(info, transferList) + else desktopLyricPort.postMessage(info) +} +const handleDesktopLyricMessage = (action: LX.DesktopLyric.WinMainActions) => { + switch (action) { + case 'get_info': + sendDesktopLyricInfo({ + action: 'set_info', + data: { + id: musicInfo.id, + singer: musicInfo.singer, + name: musicInfo.name, + album: musicInfo.album, + lrc: musicInfo.lrc, + tlrc: musicInfo.tlrc, + rlrc: musicInfo.rlrc, + lxlrc: musicInfo.lxlrc, + // pic: musicInfo.pic, + isPlay: isPlay.value, + line: lyric.line, + played_time: getCurrentTime(), + }, + }) + break + case 'get_status': + sendDesktopLyricInfo({ + action: 'set_status', + data: { + isPlay: isPlay.value, + line: lyric.line, + played_time: getCurrentTime(), + }, + }) + break + case 'get_analyser_data_array': + analyserTools.sendDataArray() + break + default: + break + } +} +export const init = () => { + lrc = new Lyric({ + shadowContent: false, + onPlay(line, text) { + setText(text, Math.max(line, 0)) + setStatusText(text) + window.app_event.lyricLinePlay(text, line) + // console.log(line, text) + }, + onSetLyric(lines, offset) { // listening lyrics seting event + // console.log(lines) // lines is array of all lyric text + setLines(markRawList([...lines])) + setText(lines[0] ?? '', 0) + setOffset(offset) // 歌词延迟 + setTempOffset(0) // 重置临时延迟 + }, + onUpdateLyric(lines) { + setLines(markRawList([...lines])) + setText(lines[0] ?? '', 0) + }, + rate: appSetting['player.playbackRate'], + // offset: 80, + }) + + onNewDesktopLyricProcess(({ event }) => { + console.log('onNewDesktopLyricProcess') + const [port] = event.ports + desktopLyricPort = port + + port.onmessage = ({ data }) => { + handleDesktopLyricMessage(data.action) + // The event data can be any serializable object (and the event could even + // carry other MessagePorts with it!) + // const result = doWork(event.data) + // port.postMessage(result) + } + + port.onmessageerror = (event) => { + console.log('onmessageerror', event) + } + }) +} + +export const setLyricOffset = (offset: number) => { + const tempOffset = offset - lyric.offset + setTempOffset(tempOffset) + lrc.setOffset(tempOffset) + sendDesktopLyricInfo({ + action: 'set_offset', + data: tempOffset, + }) + + if (isPlay.value) { + setTimeout(() => { + const time = getCurrentTime() + sendDesktopLyricInfo({ + action: 'set_play', + data: time, + }) + lrc.play(time) + }) + } +} + +export const setPlaybackRate = (rate: number) => { + lrc.setPlaybackRate(rate) + + if (isPlay.value) { + setTimeout(() => { + const time = getCurrentTime() + lrc.play(time) + }) + } +} + +export const setLyric = () => { + if (!musicInfo.id) return + if (musicInfo.lrc) { + const extendedLyrics = [] + if (appSetting['player.isShowLyricRoma'] && musicInfo.rlrc) extendedLyrics.push(musicInfo.rlrc) + if (appSetting['player.isShowLyricTranslation'] && musicInfo.tlrc) extendedLyrics.push(musicInfo.tlrc) + if (appSetting['player.isSwapLyricTranslationAndRoma']) extendedLyrics.reverse() + + lrc.setLyric( + appSetting['player.isPlayLxlrc'] && musicInfo.lxlrc ? musicInfo.lxlrc : musicInfo.lrc, + extendedLyrics, + ) + sendDesktopLyricInfo({ + action: 'set_lyric', + data: { + lrc: musicInfo.lrc, + tlrc: musicInfo.tlrc, + rlrc: musicInfo.rlrc, + lxlrc: musicInfo.lxlrc, + }, + }) + } + + if (isPlay.value) { + setTimeout(() => { + const time = getCurrentTime() + sendDesktopLyricInfo({ action: 'set_play', data: time }) + lrc.play(time) + }) + } +} + +export const setDisabledAutoPause = (disabledAutoPause: boolean) => { + lrc.setDisabledAutoPause(disabledAutoPause) +} + +let sources = new Map() +let prevDisabled = false +export const setDisableAutoPauseBySource = (disabled: boolean, source: string) => { + sources.set(source, disabled) + const currentDisabled = Array.from(sources.values()).some(e => e) + if (prevDisabled == currentDisabled) return + prevDisabled = currentDisabled + setDisabledAutoPause(currentDisabled) +} + + +export const play = () => { + // if (!musicInfo.lrc) return + const currentTime = getCurrentTime() + lrc.play(currentTime) + sendDesktopLyricInfo({ action: 'set_play', data: currentTime }) +} + +export const pause = () => { + lrc.pause() + sendDesktopLyricInfo({ action: 'set_pause' }) +} + +export const stop = () => { + lrc.setLyric('') + sendDesktopLyricInfo({ action: 'set_stop' }) + // setLines([]) + setText('', 0) +} + +export const sendInfo = () => { + sendDesktopLyricInfo({ + action: 'set_info', + data: { + id: musicInfo.id, + singer: musicInfo.singer, + name: musicInfo.name, + album: musicInfo.album, + lrc: musicInfo.lrc, + tlrc: musicInfo.tlrc, + rlrc: musicInfo.rlrc, + lxlrc: musicInfo.lxlrc, + // pic: musicInfo.pic, + isPlay: isPlay.value, + line: lyric.line, + played_time: getCurrentTime(), + }, + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/download.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/download.ts new file mode 100644 index 000000000..2a8a9327e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/download.ts @@ -0,0 +1,73 @@ +import { getDownloadFilePath } from '@renderer/utils/music' + +import { + getMusicUrl as getOnlineMusicUrl, + getPicUrl as getOnlinePicUrl, + getLyricInfo as getOnlineLyricInfo, +} from './online' +import { buildLyricInfo, getCachedLyricInfo } from './utils' +import { buildSavePath } from '@renderer/store/download/utils' + +export const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: { + musicInfo: LX.Download.ListItem + isRefresh: boolean + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void + allowToggleSource?: boolean +}): Promise => { + if (!isRefresh) { + const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo)) + if (path) return path + } + + return getOnlineMusicUrl({ musicInfo: musicInfo.metadata.musicInfo, isRefresh, onToggleSource, allowToggleSource }) +} + +export const getPicUrl = async({ musicInfo, isRefresh, listId, onToggleSource = () => {} }: { + musicInfo: LX.Download.ListItem + isRefresh: boolean + listId?: string | null + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise => { + if (!isRefresh) { + const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo)) + if (path) { + const pic = await window.lx.worker.main.getMusicFilePic(path) + if (pic) return pic + } + + const onlineMusicInfo = musicInfo.metadata.musicInfo + if (onlineMusicInfo.meta.picUrl) return onlineMusicInfo.meta.picUrl + } + + return getOnlinePicUrl({ musicInfo: musicInfo.metadata.musicInfo, isRefresh, onToggleSource }).then((url) => { + // TODO: when listId required save url (update downloadInfo) + + return url + }) +} + +export const getLyricInfo = async({ musicInfo, isRefresh, onToggleSource = () => {} }: { + musicInfo: LX.Download.ListItem + isRefresh: boolean + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise => { + if (!isRefresh) { + const lyricInfo = await getCachedLyricInfo(musicInfo.metadata.musicInfo) + if (lyricInfo) return buildLyricInfo(lyricInfo) + } + + return getOnlineLyricInfo({ + musicInfo: musicInfo.metadata.musicInfo, + isRefresh, + onToggleSource, + }).catch(async() => { + // 尝试读取文件内歌词 + const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo)) + if (path) { + const rawlrcInfo = await window.lx.worker.main.getMusicFileLyric(path) + if (rawlrcInfo) return buildLyricInfo(rawlrcInfo) + } + + throw new Error('failed') + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/index.ts new file mode 100644 index 000000000..5826b34bd --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/index.ts @@ -0,0 +1,80 @@ +// if (targetSong.key) { // 如果是已下载的歌曲 +// const filePath = path.join(appSetting['download.savePath'], targetSong.metadata.fileName) +// // console.log(filePath) + +import { + getMusicUrl as getOnlineMusicUrl, + getPicUrl as getOnlinePicUrl, + getLyricInfo as getOnlineLyricInfo, +} from './online' +import { + getMusicUrl as getDownloadMusicUrl, + getPicUrl as getDownloadPicUrl, + getLyricInfo as getDownloadLyricInfo, +} from './download' +import { + getMusicUrl as getLocalMusicUrl, + getPicUrl as getLocalPicUrl, + getLyricInfo as getLocalLyricInfo, +} from './local' + + +export const getMusicUrl = async({ + musicInfo, + quality, + isRefresh = false, + onToggleSource, + allowToggleSource, +}: { + musicInfo: LX.Music.MusicInfo | LX.Download.ListItem + isRefresh?: boolean + quality?: LX.Quality + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void + allowToggleSource?: boolean +}): Promise => { + if ('progress' in musicInfo) { + return getDownloadMusicUrl({ musicInfo, isRefresh, onToggleSource, allowToggleSource }) + } else if (musicInfo.source == 'local') { + return getLocalMusicUrl({ musicInfo, isRefresh, onToggleSource, allowToggleSource }) + } else { + return getOnlineMusicUrl({ musicInfo, isRefresh, quality, onToggleSource, allowToggleSource }) + } +} + +export const getPicPath = async({ + musicInfo, + isRefresh = false, + listId, + onToggleSource, +}: { + musicInfo: LX.Music.MusicInfo | LX.Download.ListItem + listId?: string | null + isRefresh?: boolean + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise => { + if ('progress' in musicInfo) { + return getDownloadPicUrl({ musicInfo, isRefresh, listId, onToggleSource }) + } else if (musicInfo.source == 'local') { + return getLocalPicUrl({ musicInfo, isRefresh, listId, onToggleSource }) + } else { + return getOnlinePicUrl({ musicInfo, isRefresh, listId, onToggleSource }) + } +} + +export const getLyricInfo = async({ + musicInfo, + isRefresh = false, + onToggleSource, +}: { + musicInfo: LX.Music.MusicInfo | LX.Download.ListItem + isRefresh?: boolean + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise => { + if ('progress' in musicInfo) { + return getDownloadLyricInfo({ musicInfo, isRefresh, onToggleSource }) + } else if (musicInfo.source == 'local') { + return getLocalLyricInfo({ musicInfo, isRefresh, onToggleSource }) + } else { + return getOnlineLyricInfo({ musicInfo, isRefresh, onToggleSource }) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/local.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/local.ts new file mode 100644 index 000000000..76113f657 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/local.ts @@ -0,0 +1,169 @@ +import { encodePath } from '@common/utils/common' +import { updateListMusics } from '@renderer/store/list/action' +import { saveLyric, saveMusicUrl } from '@renderer/utils/ipc' +import { getLocalFilePath } from '@renderer/utils/music' + +import { + buildLyricInfo, + getCachedLyricInfo, + getOnlineOtherSourceLyricByLocal, + getOnlineOtherSourceLyricInfo, + getOnlineOtherSourceMusicUrl, + getOnlineOtherSourceMusicUrlByLocal, + getOnlineOtherSourcePicByLocal, + getOnlineOtherSourcePicUrl, + getOtherSource, +} from './utils' + + +const getOtherSourceByLocal = async(musicInfo: LX.Music.MusicInfoLocal, handler: (infos: LX.Music.MusicInfoOnline[]) => Promise) => { + let result: LX.Music.MusicInfoOnline[] = [] + result = await getOtherSource(musicInfo) + if (result.length) try { return await handler(result) } catch {} + if (musicInfo.name.includes('-')) { + const [name, singer] = musicInfo.name.split('-').map(val => val.trim()) + result = await getOtherSource({ + ...musicInfo, + name, + singer, + }, true) + if (result.length) try { return await handler(result) } catch {} + result = await getOtherSource({ + ...musicInfo, + name: singer, + singer: name, + }, true) + if (result.length) try { return await handler(result) } catch {} + } + let fileName = musicInfo.meta.filePath.split(/\/|\\/).at(-1) + if (fileName) { + fileName = fileName.substring(0, fileName.lastIndexOf('.')) + if (fileName != musicInfo.name) { + if (fileName.includes('-')) { + const [name, singer] = fileName.split('-').map(val => val.trim()) + result = await getOtherSource({ + ...musicInfo, + name, + singer, + }, true) + if (result.length) try { return await handler(result) } catch {} + result = await getOtherSource({ + ...musicInfo, + name: singer, + singer: name, + }, true) + } else { + result = await getOtherSource({ + ...musicInfo, + name: fileName, + singer: '', + }, true) + } + if (result.length) try { return await handler(result) } catch {} + } + } + + throw new Error('source not found') +} + +export const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: { + musicInfo: LX.Music.MusicInfoLocal + isRefresh: boolean + allowToggleSource?: boolean + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise => { + if (!isRefresh) { + const path = await getLocalFilePath(musicInfo) + if (path) return encodePath(path) + } + + try { + return await getOnlineOtherSourceMusicUrlByLocal(musicInfo, isRefresh).then(({ url, quality, isFromCache }) => { + if (!isFromCache) void saveMusicUrl(musicInfo, quality, url) + return url + }) + } catch {} + + if (!allowToggleSource) throw new Error('failed') + + onToggleSource() + return getOtherSourceByLocal(musicInfo, async(otherSource) => { + return getOnlineOtherSourceMusicUrl({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => { + // saveLyric(musicInfo, data.lyricInfo) + if (!isFromCache) void saveMusicUrl(targetMusicInfo, targetQuality, url) + + // TODO: save url ? + return url + }) + }) +} + +export const getPicUrl = async({ musicInfo, listId, isRefresh, onToggleSource = () => {} }: { + musicInfo: LX.Music.MusicInfoLocal + listId?: string | null + isRefresh: boolean + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise => { + if (!isRefresh) { + const pic = await window.lx.worker.main.getMusicFilePic(musicInfo.meta.filePath) + if (pic) return pic + + if (musicInfo.meta.picUrl) return musicInfo.meta.picUrl + } + + try { + return await getOnlineOtherSourcePicByLocal(musicInfo).then(({ url }) => { + return url + }) + } catch {} + + onToggleSource() + return getOtherSourceByLocal(musicInfo, async(otherSource) => { + return getOnlineOtherSourcePicUrl({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(({ url, musicInfo: targetMusicInfo, isFromCache }) => { + if (listId) { + musicInfo.meta.picUrl = url + void updateListMusics([{ id: listId, musicInfo }]) + } + + return url + }) + }) +} + +export const getLyricInfo = async({ musicInfo, isRefresh, onToggleSource = () => {} }: { + musicInfo: LX.Music.MusicInfoLocal + isRefresh: boolean + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise => { + if (!isRefresh) { + const [lyricInfo, fileLyricInfo] = await Promise.all([getCachedLyricInfo(musicInfo), window.lx.worker.main.getMusicFileLyric(musicInfo.meta.filePath)]) + // console.log(lyricInfo, fileLyricInfo) + if (lyricInfo?.lyric && lyricInfo.lyric != fileLyricInfo?.lyric) { + // 存在已编辑歌词 + return buildLyricInfo({ ...lyricInfo, rawlrcInfo: fileLyricInfo ?? lyricInfo.rawlrcInfo }) + } + + if (fileLyricInfo) return buildLyricInfo(fileLyricInfo) + if (lyricInfo?.lyric) return buildLyricInfo(lyricInfo) + } + + try { + // eslint-disable-next-line @typescript-eslint/promise-function-async + return await getOnlineOtherSourceLyricByLocal(musicInfo, isRefresh).then(({ lyricInfo, isFromCache }) => { + if (!isFromCache) void saveLyric(musicInfo, lyricInfo) + return buildLyricInfo(lyricInfo) + }) + } catch {} + + onToggleSource() + return getOtherSourceByLocal(musicInfo, async(otherSource) => { + return getOnlineOtherSourceLyricInfo({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(async({ lyricInfo, musicInfo: targetMusicInfo, isFromCache }) => { + void saveLyric(musicInfo, lyricInfo) + + if (isFromCache) return buildLyricInfo(lyricInfo) + void saveLyric(targetMusicInfo, lyricInfo) + + return buildLyricInfo(lyricInfo) + }) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/online.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/online.ts new file mode 100644 index 000000000..cd991dea8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/online.ts @@ -0,0 +1,103 @@ +import { updateListMusics } from '@renderer/store/list/action' +import { appSetting } from '@renderer/store/setting' +import { + saveLyric, + saveMusicUrl, + getMusicUrl as getStoreMusicUrl, +} from '@renderer/utils/ipc' +import { + buildLyricInfo, + getPlayQuality, + handleGetOnlineLyricInfo, + handleGetOnlineMusicUrl, + handleGetOnlinePicUrl, + getCachedLyricInfo, +} from './utils' + +/* export const setMusicUrl = ({ musicInfo, type, url }: { + musicInfo: LX.Music.MusicInfo + type: LX.Quality + url: string +}) => { + saveMusicUrl(musicInfo, type, url) +} + +export const setPic = (datas: { + listId: string + musicInfo: LX.Music.MusicInfo + url: string +}) => { + datas.musicInfo.img = datas.url + updateMusicInfo({ + listId: datas.listId, + id: datas.musicInfo.songmid, + data: { img: datas.url }, + musicInfo: datas.musicInfo, + }) +} + */ + + +export const getMusicUrl = async({ musicInfo, quality, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: { + musicInfo: LX.Music.MusicInfoOnline + quality?: LX.Quality + isRefresh: boolean + allowToggleSource?: boolean + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise => { + // if (!musicInfo._types[type]) { + // // 兼容旧版酷我源搜索列表过滤128k音质的bug + // if (!(musicInfo.source == 'kw' && type == '128k')) throw new Error('该歌曲没有可播放的音频') + + // // return Promise.reject(new Error('该歌曲没有可播放的音频')) + // } + const targetQuality = quality ?? getPlayQuality(appSetting['player.playQuality'], musicInfo) + const cachedUrl = await getStoreMusicUrl(musicInfo, targetQuality) + if (cachedUrl && !isRefresh) return cachedUrl + + return handleGetOnlineMusicUrl({ musicInfo, quality, onToggleSource, isRefresh, allowToggleSource }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => { + if (targetMusicInfo.id != musicInfo.id && !isFromCache) void saveMusicUrl(targetMusicInfo, targetQuality, url) + void saveMusicUrl(musicInfo, targetQuality, url) + return url + }) +} + +export const getPicUrl = async({ musicInfo, listId, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: { + musicInfo: LX.Music.MusicInfoOnline + listId?: string | null + isRefresh: boolean + allowToggleSource?: boolean + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise => { + if (musicInfo.meta.picUrl && !isRefresh) return musicInfo.meta.picUrl + return handleGetOnlinePicUrl({ musicInfo, onToggleSource, isRefresh, allowToggleSource }).then(({ url, musicInfo: targetMusicInfo, isFromCache }) => { + // picRequest = null + if (listId) { + musicInfo.meta.picUrl = url + void updateListMusics([{ id: listId, musicInfo }]) + } + // savePic({ musicInfo, url, listId }) + return url + }) +} +export const getLyricInfo = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: { + musicInfo: LX.Music.MusicInfoOnline + isRefresh: boolean + allowToggleSource?: boolean + onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise => { + if (!isRefresh) { + const lyricInfo = await getCachedLyricInfo(musicInfo) + if (lyricInfo) return buildLyricInfo(lyricInfo) + } + + // lrcRequest = music[musicInfo.source].getLyric(musicInfo) + return handleGetOnlineLyricInfo({ musicInfo, onToggleSource, isRefresh, allowToggleSource }).then(async({ lyricInfo, musicInfo: targetMusicInfo, isFromCache }) => { + // lrcRequest = null + if (isFromCache) return buildLyricInfo(lyricInfo) + if (targetMusicInfo.id == musicInfo.id) void saveLyric(musicInfo, lyricInfo) + else void saveLyric(targetMusicInfo, lyricInfo) + + return buildLyricInfo(lyricInfo) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/utils.ts new file mode 100644 index 000000000..561ab26e2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/music/utils.ts @@ -0,0 +1,516 @@ +import { qualityList } from '@renderer/store' +import { assertApiSupport } from '@renderer/store/utils' +import musicSdk from '@renderer/utils/musicSdk' +import { + // getOtherSource as getOtherSourceFromStore, + // saveOtherSource as saveOtherSourceFromStore, + getMusicUrl as getStoreMusicUrl, + getPlayerLyric as getStoreLyric, +} from '@renderer/utils/ipc' +import { appSetting } from '@renderer/store/setting' +import { langS2T, toNewMusicInfo, toOldMusicInfo } from '@renderer/utils' +import { requestMsg } from '@renderer/utils/message' +import { apis } from '@renderer/utils/musicSdk/api-source' + + +const getOtherSourcePromises = new Map() +const otherSourceCache = new Map() +export const existTimeExp = /\[\d{1,2}:.*\d{1,4}\]/ + +export const getOtherSource = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false): Promise => { + // if (!isRefresh && musicInfo.id) { + // const cachedInfo = await getOtherSourceFromStore(musicInfo.id) + // if (cachedInfo.length) return cachedInfo + // } + if (otherSourceCache.has(musicInfo)) return otherSourceCache.get(musicInfo)! + let key: string + let searchMusicInfo: { + name: string + singer: string + source: string + albumName: string + interval: string + } + if ('progress' in musicInfo) { + key = `local_${musicInfo.id}` + searchMusicInfo = { + name: musicInfo.metadata.musicInfo.name, + singer: musicInfo.metadata.musicInfo.singer, + source: musicInfo.metadata.musicInfo.source, + albumName: musicInfo.metadata.musicInfo.meta.albumName, + interval: musicInfo.metadata.musicInfo.interval ?? '', + } + } else { + key = `${musicInfo.source}_${musicInfo.id}` + searchMusicInfo = { + name: musicInfo.name, + singer: musicInfo.singer, + source: musicInfo.source, + albumName: musicInfo.meta.albumName, + interval: musicInfo.interval ?? '', + } + } + if (getOtherSourcePromises.has(key)) return getOtherSourcePromises.get(key) + + const promise = new Promise((resolve, reject) => { + let timeout: null | NodeJS.Timeout = setTimeout(() => { + timeout = null + reject(new Error('find music timeout')) + }, 15_000) + musicSdk.findMusic(searchMusicInfo).then((otherSource) => { + if (otherSourceCache.size > 10) otherSourceCache.clear() + const source = otherSource.map(toNewMusicInfo) as LX.Music.MusicInfoOnline[] + otherSourceCache.set(musicInfo, source) + resolve(source) + }).catch(reject).finally(() => { + if (timeout) clearTimeout(timeout) + }) + }).then((otherSource) => { + // if (otherSource.length) void saveOtherSourceFromStore(musicInfo.id, otherSource) + return otherSource + }).finally(() => { + if (getOtherSourcePromises.has(key)) getOtherSourcePromises.delete(key) + }) + getOtherSourcePromises.set(key, promise) + return promise +} + + +export const buildLyricInfo = async(lyricInfo: MakeOptional): Promise => { + if (!appSetting['player.isS2t']) { + // @ts-expect-error + if (lyricInfo.rawlrcInfo) return lyricInfo + return { ...lyricInfo, rawlrcInfo: { ...lyricInfo } } + } + + if (appSetting['player.isS2t']) { + const tasks = [ + lyricInfo.lyric ? langS2T(lyricInfo.lyric) : Promise.resolve(''), + lyricInfo.tlyric ? langS2T(lyricInfo.tlyric) : Promise.resolve(''), + lyricInfo.rlyric ? langS2T(lyricInfo.rlyric) : Promise.resolve(''), + lyricInfo.lxlyric ? langS2T(lyricInfo.lxlyric) : Promise.resolve(''), + ] + if (lyricInfo.rawlrcInfo) { + tasks.push(lyricInfo.lyric ? langS2T(lyricInfo.lyric) : Promise.resolve('')) + tasks.push(lyricInfo.tlyric ? langS2T(lyricInfo.tlyric) : Promise.resolve('')) + tasks.push(lyricInfo.rlyric ? langS2T(lyricInfo.rlyric) : Promise.resolve('')) + tasks.push(lyricInfo.lxlyric ? langS2T(lyricInfo.lxlyric) : Promise.resolve('')) + } + return Promise.all(tasks).then(([lyric, tlyric, rlyric, lxlyric, lyric_raw, tlyric_raw, rlyric_raw, lxlyric_raw]) => { + const rawlrcInfo = lyric_raw ? { + lyric: lyric_raw, + tlyric: tlyric_raw, + rlyric: rlyric_raw, + lxlyric: lxlyric_raw, + } : { + lyric, + tlyric, + rlyric, + lxlyric, + } + return { + lyric, + tlyric, + rlyric, + lxlyric, + rawlrcInfo, + } + }) + } + + // @ts-expect-error + return lyricInfo.rawlrcInfo ? lyricInfo : { ...lyricInfo, rawlrcInfo: { ...lyricInfo } } +} + +export const getCachedLyricInfo = async(musicInfo: LX.Music.MusicInfo): Promise => { + let lrcInfo = await getStoreLyric(musicInfo) + // lrcInfo = {} as unknown as LX.Player.LyricInfo + if (existTimeExp.test(lrcInfo.lyric)) { + if (lrcInfo.tlyric != null) { + // if (musicInfo.lrc.startsWith('\ufeff[id:$00000000]')) { + // let str = musicInfo.lrc.replace('\ufeff[id:$00000000]\n', '') + // commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc }) + // } else if (musicInfo.lrc.startsWith('[id:$00000000]')) { + // let str = musicInfo.lrc.replace('[id:$00000000]\n', '') + // commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc }) + // } + + if (lrcInfo.lxlyric == null) { + switch (musicInfo.source) { // 以下源支持lxlyric 重新获取 + case 'kg': + case 'kw': + case 'mg': + case 'wy': + case 'tx': + break + default: + return lrcInfo + } + } else if (lrcInfo.rlyric == null) { + // 以下源支持 rlyric 重新获取 + if (!['wy', 'kg', 'tx'].includes(musicInfo.source)) return lrcInfo + } else return lrcInfo + } + if (musicInfo.source == 'local') return lrcInfo + } + return null +} + +export const getOnlineOtherSourceMusicUrlByLocal = async(musicInfo: LX.Music.MusicInfoLocal, isRefresh: boolean): Promise<{ + url: string + quality: LX.Quality + isFromCache: boolean +}> => { + if (!await window.lx.apiInitPromise[0]) throw new Error('source init failed') + + const quality = '128k' + + const cachedUrl = await getStoreMusicUrl(musicInfo, quality) + if (cachedUrl && !isRefresh) return { url: cachedUrl, quality, isFromCache: true } + + let reqPromise + try { + reqPromise = apis('local').getMusicUrl(toOldMusicInfo(musicInfo), null).promise + } catch (err: any) { + reqPromise = Promise.reject(err) + } + + return reqPromise.then(({ url }: { url: string }) => { + return { url, quality, isFromCache: false } + }) +} + +export const getOnlineOtherSourceLyricByLocal = async(musicInfo: LX.Music.MusicInfoLocal, isRefresh: boolean): Promise<{ + lyricInfo: LX.Music.LyricInfo + isFromCache: boolean +}> => { + if (!await window.lx.apiInitPromise[0]) throw new Error('source init failed') + + const lyricInfo = await getCachedLyricInfo(musicInfo) + if (lyricInfo && !isRefresh) return { lyricInfo, isFromCache: true } + + let reqPromise + try { + reqPromise = apis('local').getLyric(toOldMusicInfo(musicInfo)).promise + } catch (err: any) { + reqPromise = Promise.reject(err) + } + + return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => { + return { lyricInfo, isFromCache: false } + }) +} + +export const getOnlineOtherSourcePicByLocal = async(musicInfo: LX.Music.MusicInfoLocal): Promise<{ + url: string +}> => { + if (!await window.lx.apiInitPromise[0]) throw new Error('source init failed') + + let reqPromise + try { + reqPromise = apis('local').getPic(toOldMusicInfo(musicInfo)).promise + } catch (err: any) { + reqPromise = Promise.reject(err) + } + + return reqPromise.then((url: string) => { + return { url } + }) +} + +export const TRY_QUALITYS_LIST = ['flac24bit', 'flac', '320k'] as const +type TryQualityType = typeof TRY_QUALITYS_LIST[number] +export const getPlayQuality = (highQuality: LX.Quality, musicInfo: LX.Music.MusicInfoOnline): LX.Quality => { + let type: LX.Quality = '128k' + if (TRY_QUALITYS_LIST.includes(highQuality as TryQualityType)) { + let list = qualityList.value[musicInfo.source] + + let t = TRY_QUALITYS_LIST + .slice(TRY_QUALITYS_LIST.indexOf(highQuality as TryQualityType)) + .find(q => musicInfo.meta._qualitys[q] && list?.includes(q)) + + if (t) type = t + } + return type +} + +export const getOnlineOtherSourceMusicUrl = async({ musicInfos, quality, onToggleSource, isRefresh, retryedSource = [] }: { + musicInfos: LX.Music.MusicInfoOnline[] + quality?: LX.Quality + onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void + isRefresh: boolean + retryedSource?: LX.OnlineSource[] +}): Promise<{ + url: string + musicInfo: LX.Music.MusicInfoOnline + quality: LX.Quality + isFromCache: boolean +}> => { + if (!await window.lx.apiInitPromise[0]) throw new Error('source init failed') + + let musicInfo: LX.Music.MusicInfoOnline | null = null + let itemQuality: LX.Quality | null = null + // eslint-disable-next-line no-cond-assign + while (musicInfo = (musicInfos.shift()!)) { + if (retryedSource.includes(musicInfo.source)) continue + retryedSource.push(musicInfo.source) + if (!assertApiSupport(musicInfo.source)) continue + itemQuality = quality ?? getPlayQuality(appSetting['player.playQuality'], musicInfo) + if (!musicInfo.meta._qualitys[itemQuality]) continue + + console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval) + onToggleSource(musicInfo) + break + } + if (!musicInfo || !itemQuality) throw new Error(window.i18n.t('toggle_source_failed')) + + const cachedUrl = await getStoreMusicUrl(musicInfo, itemQuality) + if (cachedUrl && !isRefresh) return { url: cachedUrl, musicInfo, quality: itemQuality, isFromCache: true } + + let reqPromise + try { + reqPromise = musicSdk[musicInfo.source].getMusicUrl(toOldMusicInfo(musicInfo), itemQuality).promise + } catch (err: any) { + reqPromise = Promise.reject(err) + } + // retryedSource.includes(musicInfo.source) + // eslint-disable-next-line @typescript-eslint/promise-function-async + return reqPromise.then(({ url, type }: { url: string, type: LX.Quality }) => { + return { musicInfo, url, quality: type, isFromCache: false } + // eslint-disable-next-line @typescript-eslint/promise-function-async + }).catch((err: any) => { + if (err.message == requestMsg.tooManyRequests) throw err + console.log(err) + return getOnlineOtherSourceMusicUrl({ musicInfos, quality, onToggleSource, isRefresh, retryedSource }) + }) +} + +/** + * 获取在线音乐URL + */ +export const handleGetOnlineMusicUrl = async({ musicInfo, quality, onToggleSource, isRefresh, allowToggleSource }: { + musicInfo: LX.Music.MusicInfoOnline + quality?: LX.Quality + isRefresh: boolean + allowToggleSource: boolean + onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void +}): Promise<{ + url: string + musicInfo: LX.Music.MusicInfoOnline + quality: LX.Quality + isFromCache: boolean +}> => { + if (!await window.lx.apiInitPromise[0]) throw new Error('source init failed') + // console.log(musicInfo.source) + const targetQuality = quality ?? getPlayQuality(appSetting['player.playQuality'], musicInfo) + + let reqPromise + try { + reqPromise = musicSdk[musicInfo.source].getMusicUrl(toOldMusicInfo(musicInfo), targetQuality).promise + } catch (err: any) { + reqPromise = Promise.reject(err) + } + return reqPromise.then(({ url, type }: { url: string, type: LX.Quality }) => { + return { musicInfo, url, quality: type, isFromCache: false } + }).catch(async(err: any) => { + console.log(err) + if (!allowToggleSource || err.message == requestMsg.tooManyRequests) throw err + onToggleSource() + // eslint-disable-next-line @typescript-eslint/promise-function-async + return getOtherSource(musicInfo).then(otherSource => { + console.log('find otherSource', otherSource) + if (otherSource.length) { + return getOnlineOtherSourceMusicUrl({ + musicInfos: [...otherSource], + onToggleSource, + quality, + isRefresh, + retryedSource: [musicInfo.source], + }) + } + throw err + }) + }) +} + + +export const getOnlineOtherSourcePicUrl = async({ musicInfos, onToggleSource, isRefresh, retryedSource = [] }: { + musicInfos: LX.Music.MusicInfoOnline[] + onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void + isRefresh: boolean + retryedSource?: LX.OnlineSource[] +}): Promise<{ + url: string + musicInfo: LX.Music.MusicInfoOnline + isFromCache: boolean +}> => { + let musicInfo: LX.Music.MusicInfoOnline | null = null + // eslint-disable-next-line no-cond-assign + while (musicInfo = (musicInfos.shift()!)) { + if (retryedSource.includes(musicInfo.source)) continue + retryedSource.push(musicInfo.source) + // if (!assertApiSupport(musicInfo.source)) continue + console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval) + onToggleSource(musicInfo) + break + } + if (!musicInfo) throw new Error(window.i18n.t('toggle_source_failed')) + + if (musicInfo.meta.picUrl && !isRefresh) return { musicInfo, url: musicInfo.meta.picUrl, isFromCache: true } + + let reqPromise + try { + reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo)) + } catch (err: any) { + reqPromise = Promise.reject(err) + } + // retryedSource.includes(musicInfo.source) + return reqPromise.then((url: string) => { + return { musicInfo, url, isFromCache: false } + // eslint-disable-next-line @typescript-eslint/promise-function-async + }).catch((err: any) => { + console.log(err) + return getOnlineOtherSourcePicUrl({ musicInfos, onToggleSource, isRefresh, retryedSource }) + }) +} + +/** + * 获取在线歌曲封面 + */ +export const handleGetOnlinePicUrl = async({ musicInfo, isRefresh, onToggleSource, allowToggleSource }: { + musicInfo: LX.Music.MusicInfoOnline + onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void + isRefresh: boolean + allowToggleSource: boolean +}): Promise<{ + url: string + musicInfo: LX.Music.MusicInfoOnline + isFromCache: boolean +}> => { + // console.log(musicInfo.source) + let reqPromise + try { + reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo)) + } catch (err) { + reqPromise = Promise.reject(err) + } + return reqPromise.then((url: string) => { + return { musicInfo, url, isFromCache: false } + }).catch(async(err: any) => { + console.log(err) + if (!allowToggleSource) throw err + onToggleSource() + // eslint-disable-next-line @typescript-eslint/promise-function-async + return getOtherSource(musicInfo).then(otherSource => { + console.log('find otherSource', otherSource) + if (otherSource.length) { + return getOnlineOtherSourcePicUrl({ + musicInfos: [...otherSource], + onToggleSource, + isRefresh, + retryedSource: [musicInfo.source], + }) + } + throw err + }) + }) +} + + +export const getOnlineOtherSourceLyricInfo = async({ musicInfos, onToggleSource, isRefresh, retryedSource = [] }: { + musicInfos: LX.Music.MusicInfoOnline[] + onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void + isRefresh: boolean + retryedSource?: LX.OnlineSource[] +}): Promise<{ + lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo + musicInfo: LX.Music.MusicInfoOnline + isFromCache: boolean +}> => { + let musicInfo: LX.Music.MusicInfoOnline | null = null + // eslint-disable-next-line no-cond-assign + while (musicInfo = (musicInfos.shift()!)) { + if (retryedSource.includes(musicInfo.source)) continue + retryedSource.push(musicInfo.source) + // if (!assertApiSupport(musicInfo.source)) continue + console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval) + onToggleSource(musicInfo) + break + } + if (!musicInfo) throw new Error(window.i18n.t('toggle_source_failed')) + + if (!isRefresh) { + const lyricInfo = await getCachedLyricInfo(musicInfo) + if (lyricInfo) return { musicInfo, lyricInfo, isFromCache: true } + } + + let reqPromise + try { + // TODO: remove any type + reqPromise = (musicSdk[musicInfo.source].getLyric(toOldMusicInfo(musicInfo)) as any).promise + } catch (err: any) { + reqPromise = Promise.reject(err) + } + // retryedSource.includes(musicInfo.source) + // eslint-disable-next-line @typescript-eslint/promise-function-async + return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => { + return existTimeExp.test(lyricInfo.lyric) ? { + lyricInfo, + musicInfo, + isFromCache: false, + } : Promise.reject(new Error('failed')) + // eslint-disable-next-line @typescript-eslint/promise-function-async + }).catch((err: any) => { + console.log(err) + return getOnlineOtherSourceLyricInfo({ musicInfos, onToggleSource, isRefresh, retryedSource }) + }) +} + +/** + * 获取在线歌词信息 + */ +export const handleGetOnlineLyricInfo = async({ musicInfo, onToggleSource, isRefresh, allowToggleSource }: { + musicInfo: LX.Music.MusicInfoOnline + onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void + isRefresh: boolean + allowToggleSource: boolean +}): Promise<{ + musicInfo: LX.Music.MusicInfoOnline + lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo + isFromCache: boolean +}> => { + // console.log(musicInfo.source) + let reqPromise + try { + // TODO: remove any type + reqPromise = (musicSdk[musicInfo.source].getLyric(toOldMusicInfo(musicInfo)) as any).promise + } catch (err) { + reqPromise = Promise.reject(err) + } + // eslint-disable-next-line @typescript-eslint/promise-function-async + return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => { + return existTimeExp.test(lyricInfo.lyric) ? { + musicInfo, + lyricInfo, + isFromCache: false, + } : Promise.reject(new Error('failed')) + }).catch(async(err: any) => { + console.log(err) + if (!allowToggleSource) throw err + + onToggleSource() + // eslint-disable-next-line @typescript-eslint/promise-function-async + return getOtherSource(musicInfo).then(otherSource => { + console.log('find otherSource', otherSource) + if (otherSource.length) { + return getOnlineOtherSourceLyricInfo({ + musicInfos: [...otherSource], + onToggleSource, + isRefresh, + retryedSource: [musicInfo.source], + }) + } + throw err + }) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/action.ts new file mode 100644 index 000000000..88995d0a9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/action.ts @@ -0,0 +1,646 @@ +import { isEmpty, setPause, setPlay, setResource, setStop } from '@renderer/plugins/player' +import { isPlay, playedList, playInfo, playMusicInfo, tempPlayList, musicInfo as _musicInfo } from '@renderer/store/player/state' +import { + getList, + clearPlayedList, + clearTempPlayeList, + setPlayMusicInfo, + addPlayedList, + setMusicInfo, + setAllStatus, + removeTempPlayList, + setPlayListId, + removePlayedList, +} from '@renderer/store/player/action' +import { appSetting } from '@renderer/store/setting' +import { getMusicUrl, getPicPath, getLyricInfo } from '../music/index' +import { filterList } from './utils' +import { requestMsg } from '@renderer/utils/message' +import { getRandom } from '@renderer/utils/index' +import { addListMusics, removeListMusics } from '@renderer/store/list/action' +import { loveList } from '@renderer/store/list/state' +import { addDislikeInfo } from '@renderer/core/dislikeList' +// import { checkMusicFileAvailable } from '@renderer/utils/music' + +let gettingUrlId = '' +const createGettingUrlId = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem) => { + const tInfo = 'progress' in musicInfo ? musicInfo.metadata.musicInfo.meta.toggleMusicInfo : musicInfo.meta.toggleMusicInfo + return `${musicInfo.id}_${tInfo?.id ?? ''}` +} +const createDelayNextTimeout = (delay: number) => { + let timeout: NodeJS.Timeout | null + const clearDelayNextTimeout = () => { + // console.log(this.timeout) + if (timeout) { + clearTimeout(timeout) + timeout = null + } + } + + const addDelayNextTimeout = () => { + clearDelayNextTimeout() + timeout = setTimeout(() => { + timeout = null + if (window.lx.isPlayedStop) return + console.warn('delay next timeout timeout', delay) + void playNext(true) + }, delay) + } + + return { + clearDelayNextTimeout, + addDelayNextTimeout, + } +} +const { addDelayNextTimeout, clearDelayNextTimeout } = createDelayNextTimeout(5000) +const { addDelayNextTimeout: addLoadTimeout, clearDelayNextTimeout: clearLoadTimeout } = createDelayNextTimeout(100000) + +/** + * 检查音乐信息是否已更改 + */ +const diffCurrentMusicInfo = (curMusicInfo: LX.Music.MusicInfo | LX.Download.ListItem): boolean => { + // return curMusicInfo !== playMusicInfo.musicInfo || isPlay.value + return gettingUrlId != createGettingUrlId(curMusicInfo) || curMusicInfo.id != playMusicInfo.musicInfo?.id || isPlay.value +} + +let cancelDelayRetry: (() => void) | null = null +const delayRetry = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false): Promise => { + // if (cancelDelayRetry) cancelDelayRetry() + return new Promise((resolve, reject) => { + const time = getRandom(2, 6) + setAllStatus(window.i18n.t('player__getting_url_delay_retry', { time })) + const tiemout = setTimeout(() => { + getMusicPlayUrl(musicInfo, isRefresh, true).then((result) => { + cancelDelayRetry = null + resolve(result) + }).catch(async(err: any) => { + cancelDelayRetry = null + reject(err) + }) + }, time * 1000) + cancelDelayRetry = () => { + clearTimeout(tiemout) + cancelDelayRetry = null + resolve(null) + } + }) +} +const getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false, isRetryed = false): Promise => { + // this.musicInfo.url = await getMusicPlayUrl(targetSong, type) + setAllStatus(window.i18n.t('player__getting_url')) + if (appSetting['player.autoSkipOnError']) addLoadTimeout() + + // const type = getPlayType(appSetting['player.highQuality'], musicInfo) + let toggleMusicInfo = ('progress' in musicInfo ? musicInfo.metadata.musicInfo : musicInfo).meta.toggleMusicInfo + + return (toggleMusicInfo ? getMusicUrl({ + musicInfo: toggleMusicInfo, + isRefresh, + allowToggleSource: false, + }) : Promise.reject(new Error('not found'))).catch(async() => { + return getMusicUrl({ + musicInfo, + isRefresh, + onToggleSource(mInfo) { + if (diffCurrentMusicInfo(musicInfo)) return + setAllStatus(window.i18n.t('toggle_source_try')) + }, + }) + }).then(url => { + if (window.lx.isPlayedStop || diffCurrentMusicInfo(musicInfo)) return null + + return url + // eslint-disable-next-line @typescript-eslint/promise-function-async + }).catch(err => { + // console.log('err', err.message) + if (window.lx.isPlayedStop || + diffCurrentMusicInfo(musicInfo) || + err.message == requestMsg.cancelRequest) return null + + if (err.message == requestMsg.tooManyRequests) return delayRetry(musicInfo, isRefresh) + + if (!isRetryed) return getMusicPlayUrl(musicInfo, isRefresh, true) + + throw err + }) +} + +export const setMusicUrl = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh?: boolean) => { + // if (appSetting['player.autoSkipOnError']) addLoadTimeout() + if (!diffCurrentMusicInfo(musicInfo)) return + if (cancelDelayRetry) cancelDelayRetry() + gettingUrlId = createGettingUrlId(musicInfo) + void getMusicPlayUrl(musicInfo, isRefresh).then((url) => { + if (!url) return + setResource(url) + }).catch((err: any) => { + console.log(err) + setAllStatus(err.message) + window.app_event.error() + if (appSetting['player.autoSkipOnError']) addDelayNextTimeout() + }).finally(() => { + if (musicInfo === playMusicInfo.musicInfo) { + gettingUrlId = '' + clearLoadTimeout() + } + }) +} + +// 恢复上次播放的状态 +const handleRestorePlay = async(restorePlayInfo: LX.Player.SavedPlayInfo) => { + const musicInfo = playMusicInfo.musicInfo + if (!musicInfo) return + + setImmediate(() => { + if (musicInfo.id != playMusicInfo.musicInfo?.id) return + window.app_event.setProgress(appSetting['player.isSavePlayTime'] ? restorePlayInfo.time : 0, restorePlayInfo.maxTime) + window.app_event.pause() + }) + + + void getPicPath({ musicInfo, listId: playMusicInfo.listId }).then((url: string) => { + if (musicInfo.id != playMusicInfo.musicInfo?.id || url == _musicInfo.pic) return + setMusicInfo({ pic: url }) + window.app_event.picUpdated() + }).catch(_ => _) + + void getLyricInfo({ musicInfo }).then((lyricInfo) => { + if (musicInfo.id != playMusicInfo.musicInfo?.id) return + setMusicInfo({ + lrc: lyricInfo.lyric, + tlrc: lyricInfo.tlyric, + lxlrc: lyricInfo.lxlyric, + rlrc: lyricInfo.rlyric, + rawlrc: lyricInfo.rawlrcInfo.lyric, + }) + window.app_event.lyricUpdated() + }).catch((err) => { + console.log(err) + if (musicInfo.id != playMusicInfo.musicInfo?.id) return + setAllStatus(window.i18n.t('lyric__load_error')) + }) + + if (appSetting['player.togglePlayMethod'] == 'random' && !playMusicInfo.isTempPlay) addPlayedList({ ...playMusicInfo as LX.Player.PlayMusicInfo }) +} + + +// 处理音乐播放 +const handlePlay = () => { + window.lx.isPlayedStop &&= false + + resetRandomNextMusicInfo() + if (window.lx.restorePlayInfo) { + void handleRestorePlay(window.lx.restorePlayInfo) + window.lx.restorePlayInfo = null + return + } + const musicInfo = playMusicInfo.musicInfo + + if (!musicInfo) return + + setStop() + window.app_event.pause() + + clearDelayNextTimeout() + clearLoadTimeout() + + + if (appSetting['player.togglePlayMethod'] == 'random' && !playMusicInfo.isTempPlay) addPlayedList({ ...(playMusicInfo as LX.Player.PlayMusicInfo) }) + + setMusicUrl(musicInfo) + + void getPicPath({ musicInfo, listId: playMusicInfo.listId }).then((url: string) => { + if (musicInfo.id != playMusicInfo.musicInfo?.id || url == _musicInfo.pic) return + setMusicInfo({ pic: url }) + window.app_event.picUpdated() + }).catch(_ => _) + + void getLyricInfo({ musicInfo }).then((lyricInfo) => { + if (musicInfo.id != playMusicInfo.musicInfo?.id) return + setMusicInfo({ + lrc: lyricInfo.lyric, + tlrc: lyricInfo.tlyric, + lxlrc: lyricInfo.lxlyric, + rlrc: lyricInfo.rlyric, + rawlrc: lyricInfo.rawlrcInfo.lyric, + }) + window.app_event.lyricUpdated() + }).catch((err) => { + console.log(err) + if (musicInfo.id != playMusicInfo.musicInfo?.id) return + setAllStatus(window.i18n.t('lyric__load_error')) + }) +} + +/** + * 播放列表内歌曲 + * @param listId 列表id + * @param id 歌曲id + */ +export const playListById = (listId: string, id: string) => { + const prevListId = playInfo.playerListId + setPlayListId(listId) + // pause() + const musicInfo = getList(listId).find(m => m.id == id) + if (!musicInfo) return + setPlayMusicInfo(listId, musicInfo) + if (appSetting['player.isAutoCleanPlayedList'] || prevListId != listId) clearPlayedList() + clearTempPlayeList() + handlePlay() +} + +/** + * 播放列表内歌曲 + * @param listId 列表id + * @param index 播放的歌曲位置 + */ +export const playList = (listId: string, index: number) => { + const prevListId = playInfo.playerListId + setPlayListId(listId) + // pause() + setPlayMusicInfo(listId, getList(listId)[index]) + if (appSetting['player.isAutoCleanPlayedList'] || prevListId != listId) clearPlayedList() + clearTempPlayeList() + handlePlay() +} + +const handleToggleStop = () => { + stop() + setTimeout(() => { + setPlayMusicInfo(null, null) + }) +} + +const randomNextMusicInfo = { + info: null as LX.Player.PlayMusicInfo | null, + // index: -1, +} +export const resetRandomNextMusicInfo = () => { + if (randomNextMusicInfo.info) { + randomNextMusicInfo.info = null + // randomNextMusicInfo.index = -1 + } +} + +export const getNextPlayMusicInfo = async(): Promise => { + if (tempPlayList.length) { // 如果稍后播放列表存在歌曲则直接播放改列表的歌曲 + const playMusicInfo = tempPlayList[0] + return playMusicInfo + } + + if (playMusicInfo.musicInfo == null) return null + + if (randomNextMusicInfo.info) return randomNextMusicInfo.info + + // console.log(playInfo.playerListId) + const currentListId = playInfo.playerListId + if (!currentListId) return null + const currentList = getList(currentListId) + + if (playedList.length) { // 移除已播放列表内不存在原列表的歌曲 + let currentId: string + if (playMusicInfo.isTempPlay) { + const musicInfo = currentList[playInfo.playerPlayIndex] + if (musicInfo) currentId = musicInfo.id + } else { + currentId = playMusicInfo.musicInfo.id + } + // 从已播放列表移除播放列表已删除的歌曲 + let index + for (index = playedList.findIndex(m => m.musicInfo.id === currentId) + 1; index < playedList.length; index++) { + const playMusicInfo = playedList[index] + const currentId = playMusicInfo.musicInfo.id + if (playMusicInfo.listId == currentListId && !currentList.some(m => m.id === currentId)) { + removePlayedList(index) + continue + } + break + } + + if (index < playedList.length) return playedList[index] + } + // const isCheckFile = findNum > 2 // 针对下载列表,如果超过两次都碰到无效歌曲,则过滤整个列表内的无效歌曲 + let { filteredList, playerIndex } = await filterList({ // 过滤已播放歌曲 + listId: currentListId, + list: currentList, + playedList, + playerMusicInfo: currentList[playInfo.playerPlayIndex], + isNext: true, + }) + + if (!filteredList.length) return null + // let currentIndex: number = filteredList.indexOf(currentList[playInfo.playerPlayIndex]) + if (playerIndex == -1 && filteredList.length) playerIndex = 0 + let nextIndex = playerIndex + + let togglePlayMethod = appSetting['player.togglePlayMethod'] + switch (togglePlayMethod) { + case 'listLoop': + nextIndex = playerIndex === filteredList.length - 1 ? 0 : playerIndex + 1 + break + case 'random': + nextIndex = getRandom(0, filteredList.length) + break + case 'list': + nextIndex = playerIndex === filteredList.length - 1 ? -1 : playerIndex + 1 + break + case 'singleLoop': + break + default: + return null + } + if (nextIndex < 0) return null + + const nextPlayMusicInfo = { + musicInfo: filteredList[nextIndex], + listId: currentListId, + isTempPlay: false, + } + + if (togglePlayMethod == 'random') { + randomNextMusicInfo.info = nextPlayMusicInfo + // randomNextMusicInfo.index = nextIndex + } + return nextPlayMusicInfo +} + +const handlePlayNext = (playMusicInfo: LX.Player.PlayMusicInfo) => { + // pause() + setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay) + handlePlay() +} +/** + * 下一曲 + * @param isAutoToggle 是否自动切换 + * @returns + */ +export const playNext = async(isAutoToggle = false): Promise => { + console.log('skip next', isAutoToggle) + if (tempPlayList.length) { // 如果稍后播放列表存在歌曲则直接播放改列表的歌曲 + const playMusicInfo = tempPlayList[0] + removeTempPlayList(0) + handlePlayNext(playMusicInfo) + console.log('play temp list') + return + } + + if (playMusicInfo.musicInfo == null) { + handleToggleStop() + console.log('musicInfo empty') + return + } + + // console.log(playInfo.playerListId) + const currentListId = playInfo.playerListId + if (!currentListId) { + handleToggleStop() + console.log('currentListId empty') + return + } + const currentList = getList(currentListId) + + if (playedList.length) { // 移除已播放列表内不存在原列表的歌曲 + let currentId: string + if (playMusicInfo.isTempPlay) { + const musicInfo = currentList[playInfo.playerPlayIndex] + if (musicInfo) currentId = musicInfo.id + } else { + currentId = playMusicInfo.musicInfo.id + } + // 从已播放列表移除播放列表已删除的歌曲 + let index + for (index = playedList.findIndex(m => m.musicInfo.id === currentId) + 1; index < playedList.length; index++) { + const playMusicInfo = playedList[index] + const currentId = playMusicInfo.musicInfo.id + if (playMusicInfo.listId == currentListId && !currentList.some(m => m.id === currentId)) { + removePlayedList(index) + continue + } + break + } + + if (index < playedList.length) { + handlePlayNext(playedList[index]) + console.log('play played list') + return + } + } + if (randomNextMusicInfo.info) { + handlePlayNext(randomNextMusicInfo.info) + return + } + // const isCheckFile = findNum > 2 // 针对下载列表,如果超过两次都碰到无效歌曲,则过滤整个列表内的无效歌曲 + let { filteredList, playerIndex } = await filterList({ // 过滤已播放歌曲 + listId: currentListId, + list: currentList, + playedList, + playerMusicInfo: currentList[playInfo.playerPlayIndex], + isNext: true, + }) + + if (!filteredList.length) { + handleToggleStop() + console.log('filtered list empty') + return + } + // let currentIndex: number = filteredList.indexOf(currentList[playInfo.playerPlayIndex]) + if (playerIndex == -1 && filteredList.length) playerIndex = 0 + let nextIndex = playerIndex + + let togglePlayMethod = appSetting['player.togglePlayMethod'] + if (!isAutoToggle) { + switch (togglePlayMethod) { + case 'list': + case 'singleLoop': + case 'none': + togglePlayMethod = 'listLoop' + } + } + switch (togglePlayMethod) { + case 'listLoop': + nextIndex = playerIndex === filteredList.length - 1 ? 0 : playerIndex + 1 + break + case 'random': + nextIndex = getRandom(0, filteredList.length) + break + case 'list': + nextIndex = playerIndex === filteredList.length - 1 ? -1 : playerIndex + 1 + break + case 'singleLoop': + break + default: + nextIndex = -1 + console.log('stop toggle play', togglePlayMethod, isAutoToggle) + return + } + if (nextIndex < 0) { + console.log('next index empty') + return + } + + handlePlayNext({ + musicInfo: filteredList[nextIndex], + listId: currentListId, + isTempPlay: false, + }) +} + +/** + * 上一曲 + */ +export const playPrev = async(isAutoToggle = false): Promise => { + if (playMusicInfo.musicInfo == null) { + handleToggleStop() + return + } + + const currentListId = playInfo.playerListId + if (!currentListId) { + handleToggleStop() + return + } + const currentList = getList(currentListId) + + if (playedList.length) { + let currentId: string + if (playMusicInfo.isTempPlay) { + const musicInfo = currentList[playInfo.playerPlayIndex] + if (musicInfo) currentId = musicInfo.id + } else { + currentId = playMusicInfo.musicInfo.id + } + // 从已播放列表移除播放列表已删除的歌曲 + let index + for (index = playedList.findIndex(m => m.musicInfo.id === currentId) - 1; index > -1; index--) { + const playMusicInfo = playedList[index] + const currentId = playMusicInfo.musicInfo.id + if (playMusicInfo.listId == currentListId && !currentList.some(m => m.id === currentId)) { + removePlayedList(index) + continue + } + break + } + + if (index > -1) { + handlePlayNext(playedList[index]) + return + } + } + + // const isCheckFile = findNum > 2 + let { filteredList, playerIndex } = await filterList({ // 过滤已播放歌曲 + listId: currentListId, + list: currentList, + playedList, + playerMusicInfo: currentList[playInfo.playerPlayIndex], + isNext: false, + }) + if (!filteredList.length) { + handleToggleStop() + return + } + + // let currentIndex = filteredList.indexOf(currentList[playInfo.playerPlayIndex]) + if (playerIndex == -1 && filteredList.length) playerIndex = 0 + let nextIndex = playerIndex + if (!playMusicInfo.isTempPlay) { + let togglePlayMethod = appSetting['player.togglePlayMethod'] + if (!isAutoToggle) { + switch (togglePlayMethod) { + case 'list': + case 'singleLoop': + case 'none': + togglePlayMethod = 'listLoop' + } + } + switch (togglePlayMethod) { + case 'random': + nextIndex = getRandom(0, filteredList.length) + break + case 'listLoop': + case 'list': + nextIndex = playerIndex === 0 ? filteredList.length - 1 : playerIndex - 1 + break + case 'singleLoop': + break + default: + nextIndex = -1 + return + } + if (nextIndex < 0) return + } + + handlePlayNext({ + musicInfo: filteredList[nextIndex], + listId: currentListId, + isTempPlay: false, + }) +} + +/** + * 恢复播放 + */ +export const play = () => { + window.lx.isPlayedStop &&= false + if (playMusicInfo.musicInfo == null) return + if (isEmpty()) { + if (createGettingUrlId(playMusicInfo.musicInfo) != gettingUrlId) setMusicUrl(playMusicInfo.musicInfo) + return + } + setPlay() +} + +/** + * 暂停播放 + */ +export const pause = () => { + setPause() +} + +/** + * 停止播放 + */ +export const stop = () => { + setStop() + setTimeout(() => { + window.app_event.stop() + }) +} + +/** + * 播放、暂停播放切换 + */ +export const togglePlay = () => { + window.lx.isPlayedStop &&= false + if (isPlay.value) { + pause() + } else { + play() + } +} + +/** + * 收藏当前播放的歌曲 + */ +export const collectMusic = () => { + if (!playMusicInfo.musicInfo) return + void addListMusics(loveList.id, ['progress' in playMusicInfo.musicInfo ? playMusicInfo.musicInfo.metadata.musicInfo : playMusicInfo.musicInfo]) +} + +/** + * 取消收藏当前播放的歌曲 + */ +export const uncollectMusic = () => { + if (!playMusicInfo.musicInfo) return + void removeListMusics({ listId: loveList.id, ids: ['progress' in playMusicInfo.musicInfo ? playMusicInfo.musicInfo.metadata.musicInfo.id : playMusicInfo.musicInfo.id] }) +} + +/** + * 不喜欢当前播放的歌曲 + */ +export const dislikeMusic = async() => { + if (!playMusicInfo.musicInfo) return + const minfo = 'progress' in playMusicInfo.musicInfo ? playMusicInfo.musicInfo.metadata.musicInfo : playMusicInfo.musicInfo + await addDislikeInfo([{ name: minfo.name, singer: minfo.singer }]) + await playNext(true) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/index.ts new file mode 100644 index 000000000..a63dbe4d4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/index.ts @@ -0,0 +1,2 @@ +export * from './action' +export * from './timeoutStop' diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/timeoutStop.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/timeoutStop.ts new file mode 100644 index 000000000..3287ccb70 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/timeoutStop.ts @@ -0,0 +1,99 @@ +import { ref, computed, type ComputedRef } from '@common/utils/vueTools' +import { isPlay } from '@renderer/store/player/state' +import { appSetting } from '@renderer/store/setting' +// import { interval, intervalCancel } from '@renderer/utils/ipc' +import { pause } from './action' + +const time = ref(-1) + + +const timeoutTools: { + isRunning: boolean + // time: number + interval: null | number + timeout: NodeJS.Timeout | null + endTime: number + exit: () => void + clearTimeout: () => void + start: (_time: number) => void +} = { + isRunning: false, + timeout: null, + // time: -1, + endTime: 0, + interval: null, + exit() { + window.lx.isPlayedStop = true + if (!appSetting['player.waitPlayEndStop'] && isPlay.value) { + pause() + } + }, + clearTimeout() { + if (this.interval) { + window.clearInterval(this.interval) + this.interval = null + } + if (this.timeout) { + clearTimeout(this.timeout) + this.timeout = null + } + + if (!this.isRunning) return + // this.time = -1 + time.value = -1 + this.isRunning = false + }, + start(_time: number) { + this.clearTimeout() + // this.time = _time + time.value = _time + this.isRunning = true + this.endTime = performance.now() + _time * 1000 + + this.interval = window.setInterval(() => { + // this.endTime = performance.now() + // if (this.time > 0) { + // this.time-- + // } + time.value = Math.max(0, Math.round((this.endTime - performance.now()) / 1000)) + }, 1000) + this.timeout = setTimeout(() => { + this.timeout = null + time.value = -1 + this.clearTimeout() + this.exit() + }, _time * 1000) + }, +} + +export const startTimeoutStop = (time: number) => { + window.lx.isPlayedStop &&= false + timeoutTools.start(time) +} +export const stopTimeoutStop = () => { + console.warn('stopTimeoutStop') + window.lx.isPlayedStop &&= false + timeoutTools.clearTimeout() +} + +const formatTime = (time: number): string => { + // let d = parseInt(time / 86400) + // d = d ? d.toString() + ':' : '' + // time = time % 86400 + let h: number | string = Math.trunc(time / 3600) + h = h ? h.toString() + ':' : '' + time = time % 3600 + const m = Math.trunc(time / 60).toString().padStart(2, '0') + const s = Math.trunc(time % 60).toString().padStart(2, '0') + return `${h}${m}:${s}` +} +export const useTimeout = () => { + const timeLabel: ComputedRef = computed(() => { + return time.value > 0 ? formatTime(time.value) : '' + }) + + return { + time, + timeLabel, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/utils.ts new file mode 100644 index 000000000..3ec96db6c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/player/utils.ts @@ -0,0 +1,65 @@ +import { toRaw, markRawList } from '@common/utils/vueTools' +// import { qualityList } from '@renderer/store' +import { clearPlayedList } from '@renderer/store/player/action' +import { appSetting } from '@renderer/store/setting' +import { dislikeInfo } from '@renderer/store/dislikeList' +import { setPowerSaveBlocker as setPowerSaveBlockerRemote } from '@renderer/utils/ipc' + +// export const getPlayType = (highQuality: boolean, musicInfo: LX.Music.MusicInfo | LX.Download.ListItem): LX.Quality | null => { +// if ('progress' in musicInfo || musicInfo.source == 'local') return null +// let type: LX.Quality = '128k' +// let list = qualityList.value[musicInfo.source] +// if (highQuality && musicInfo.meta._qualitys['320k'] && list?.includes('320k')) type = '320k' +// return type +// } + +/** + * 过滤列表中已播放的歌曲 + */ +export const filterList = async({ playedList, listId, list, playerMusicInfo, isNext }: { + playedList: LX.Player.PlayMusicInfo[] + listId: string + list: Array + playerMusicInfo?: LX.Music.MusicInfo | LX.Download.ListItem + isNext: boolean +}) => { + // if (this.list.listName === null) return + // console.log(isCheckFile) + let { filteredList, canPlayList, playerIndex } = await window.lx.worker.main.filterMusicList({ + listId, + list: list.map(m => toRaw(m)), + playedList: toRaw(playedList), + // savePath: appSetting['download.savePath'], + playerMusicInfo: toRaw(playerMusicInfo), + dislikeInfo: { names: toRaw(dislikeInfo.names), musicNames: toRaw(dislikeInfo.musicNames), singerNames: toRaw(dislikeInfo.singerNames) }, + isNext, + }) + + if (!filteredList.length && playedList.length) { + clearPlayedList() + return { filteredList: markRawList(canPlayList), playerIndex } + } + return { filteredList: markRawList(filteredList), playerIndex } +} + +let timeout: NodeJS.Timeout | null = null +const clearTimer = () => { + if (!timeout) return + clearTimeout(timeout) + timeout = null +} +export const setPowerSaveBlocker = (enabled: boolean, force = false) => { + if (enabled) { + clearTimer() + if (!force && !appSetting['player.powerSaveBlocker']) return + setPowerSaveBlockerRemote(true) + } else if (force) { + clearTimer() + setPowerSaveBlockerRemote(false) + } else { + if (timeout) return + timeout = setTimeout(() => { + setPowerSaveBlockerRemote(false) + }, 60_000 * 1.5) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/compositions/usePlaySonglist.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/compositions/usePlaySonglist.ts new file mode 100644 index 000000000..4acd5048c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/compositions/usePlaySonglist.ts @@ -0,0 +1,46 @@ +import { playList } from '@renderer/core/player' +import { setTempList } from '@renderer/store/list/action' +import { tempList, tempListMeta } from '@renderer/store/list/state' +import { getListDetail, getListDetailAll } from '@renderer/store/songList/action' + +const getListPlayIndex = (list: LX.Music.MusicInfoOnline[], index?: number) => { + if (index == null) { + index = 1 + } else { + if (index < 1) index = 1 + else if (index > list.length) index = list.length + } + return index - 1 +} + +export default () => { + const playSongListDetail = async(source: LX.OnlineSource, link: string, playIndex?: number) => { + // console.log(source, link, playIndex) + if (link == null) return + let isPlayingList = false + const id = decodeURIComponent(link) + const playListId = `${source}__${decodeURIComponent(link)}` + let list = (await getListDetail(id, source, 1)).list + if (playIndex == null || list.length > playIndex) { + isPlayingList = true + await setTempList(playListId, list) + playList(tempList.id, getListPlayIndex(list, playIndex)) + } + list = await getListDetailAll(id, source) + if (isPlayingList) { + if (tempListMeta.id == id) await setTempList(playListId, list) + } else { + await setTempList(playListId, list) + playList(tempList.id, getListPlayIndex(list, playIndex)) + } + } + + return async(source: LX.OnlineSource, link: string, playIndex?: number) => { + try { + await playSongListDetail(source, link, playIndex) + } catch (err) { + console.error(err) + throw new Error('Get play list failed.') + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/index.ts new file mode 100644 index 000000000..795361411 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/index.ts @@ -0,0 +1,77 @@ +import { checkUpdate, getEnvParams, getViewPrevState, sendInited } from '@renderer/utils/ipc' + +import { proxy, isFullscreen, themeId } from '@renderer/store' +import { appSetting } from '@renderer/store/setting' + +import useSync from './useSync' +import useOpenAPI from './useOpenAPI' +import useStatusbarLyric from './useStatusbarLyric' +import useUpdate from './useUpdate' +import useDataInit from './useDataInit' +import useHandleEnvParams from './useHandleEnvParams' +import useEventListener from './useEventListener' +import useDeeplink from './useDeeplink' +import usePlayer from './usePlayer' +import useSettingSync from './useSettingSync' +import { useRouter } from '@common/utils/vueRouter' +import handleListAutoUpdate from './listAutoUpdate' + + +export default () => { + // apiSource.value = appSetting['common.apiSource'] + proxy.enable = appSetting['network.proxy.enable'] + proxy.host = appSetting['network.proxy.host'] + proxy.port = appSetting['network.proxy.port'] + isFullscreen.value = appSetting['common.startInFullscreen'] + themeId.value = appSetting['theme.id'] + + const router = useRouter() + const initSyncService = useSync() + const initOpenAPI = useOpenAPI() + const initStatusbarLyric = useStatusbarLyric() + useEventListener() + const initPlayer = usePlayer() + const handleEnvParams = useHandleEnvParams() + const initData = useDataInit() + const initDeeplink = useDeeplink() + // const handleListAutoUpdate = useListAutoUpdate() + + useUpdate() + useSettingSync() + + void getEnvParams().then(envParams => { + // 移除代理相关的环境变量设置,防止请求库自动应用它们 + // eslint-disable-next-line no-undef + // const processEnv = ENVIRONMENT + // for (const key of Object.keys(processEnv)) { + // // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + // if (/^(?:http_proxy|https_proxy|NO_PROXY)$/i.test(key)) delete processEnv[key] + // } + const envProxy = envParams.cmdParams['proxy-server'] + if (envProxy && typeof envProxy == 'string') { + const [host, port = ''] = envProxy.split(':') + proxy.envProxy = { + host, + port, + } + } + + void getViewPrevState().then(state => { + void router.push({ path: state.url, query: state.query }) + }) + + // 初始化我的列表、下载列表等数据 + void initData().then(() => { + initPlayer() + handleEnvParams(envParams) // 处理传入的启动参数 + void initDeeplink(envParams) + void initSyncService() + void initOpenAPI() + void initStatusbarLyric() + sendInited() + + handleListAutoUpdate() + if (window.lx.isProd && appSetting['common.isAgreePact']) checkUpdate() + }) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/listAutoUpdate.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/listAutoUpdate.ts new file mode 100644 index 000000000..7e10e0378 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/listAutoUpdate.ts @@ -0,0 +1,25 @@ +import { getListUpdateInfo } from '@renderer/utils/data' +import { userLists } from '@renderer/store/list/state' +import syncSourceList from '@renderer/store/list/syncSourceList' + +const handleSyncSourceList = async(waitUpdateLists: LX.List.UserListInfo[]) => { + if (!waitUpdateLists.length) return + const targetListInfo = waitUpdateLists.shift()! + // console.log(targetListInfo) + try { + await syncSourceList(targetListInfo) + } catch {} + void handleSyncSourceList(waitUpdateLists) +} + +export default () => { + void getListUpdateInfo().then(listUpdateInfo => { + const waitUpdateLists = Object.entries(listUpdateInfo) + .map(([id, info]) => info.isAutoUpdate && userLists.find(l => l.id == id)) + .filter(_ => _) as LX.List.UserListInfo[] + // for (let i = 2; i > 0; i--) { + // void handleSyncSourceList(waitUpdateLists) + void handleSyncSourceList(waitUpdateLists) + // } + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDataInit.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDataInit.ts new file mode 100644 index 000000000..98ce29121 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDataInit.ts @@ -0,0 +1,60 @@ +import { getPlayInfo } from '@renderer/utils/ipc' +import music from '@renderer/utils/musicSdk' +import { log } from '@common/utils' +import { getListMusics, getUserLists, registerAction } from '@renderer/store/list/action' + + +import useInitUserApi from './useInitUserApi' +import { play, playList } from '@renderer/core/player' +import { onBeforeUnmount } from '@common/utils/vueTools' +import { appSetting } from '@renderer/store/setting' +import { playMusicInfo } from '@renderer/store/player/state' +import { initDislikeInfo, registerRemoteDislikeAction } from '@renderer/core/dislikeList' + +const initPrevPlayInfo = async() => { + const info = await getPlayInfo() + window.lx.restorePlayInfo = null + if (!info?.listId || info.index < 0) return + const list = await getListMusics(info.listId) + if (!list[info.index]) return + window.lx.restorePlayInfo = info + playList(info.listId, info.index) + + if (appSetting['player.startupAutoPlay']) { + const musicInfo = playMusicInfo.musicInfo + if (!musicInfo) return + setTimeout(() => { + if (musicInfo.id == playMusicInfo.musicInfo?.id) play() + }) + } +} + +export default () => { + const initUserApi = useInitUserApi() + + let unregister: null | (() => void) = null + let unregisterDislikeEvent: null | (() => void) = null + + onBeforeUnmount(() => { + if (unregister) unregister() + if (unregisterDislikeEvent) unregisterDislikeEvent() + }) + + return async() => { + await Promise.all([ + initUserApi(), // 自定义API + ]).catch(err => { + log.error(err) + }) + void music.init() // 初始化音乐sdk + unregister = registerAction((ids) => { + window.app_event.myListUpdate(ids) + }) + window.lxData.userLists = await getUserLists() // 获取用户列表 + unregisterDislikeEvent = registerRemoteDislikeAction() + await initDislikeInfo() // 获取不喜欢列表 + await initPrevPlayInfo().catch(err => { + log.error(err) + }) // 初始化上次的歌曲播放信息 + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/index.ts new file mode 100644 index 000000000..e0b842902 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/index.ts @@ -0,0 +1,81 @@ +import { onBeforeUnmount } from '@common/utils/vueTools' +import { clearEnvParamsDeeplink, focusWindow, onDeeplink } from '@renderer/utils/ipc' + +import { useDialog } from './utils' +import useMusicAction from './useMusicAction' +import useSonglistAction from './useSonglistAction' +import usePlayerAction from './usePlayerAction' + +export default () => { + let isInited = false + + const showErrorDialog = useDialog() + + const handleMusicAction = useMusicAction() + const handleSonglistAction = useSonglistAction() + const handlePlayerAction = usePlayerAction() + + + const handleLinkAction = async(link: string) => { + // console.log(link) + const [url, search] = link.split('?') + const [type, action, ...paths] = url.replace('lxmusic://', '').split('/') + const params: { + paths: string[] + data?: any + [key: string]: any + } = { + paths: [], + } + if (search) { + for (const param of search.split('&')) { + const [key, value] = param.split('=') + params[key] = value + } + if (params.data) params.data = JSON.parse(decodeURIComponent(params.data)) + } + params.paths = paths.map(p => decodeURIComponent(p)) + console.log(params) + switch (type) { + case 'music': + await handleMusicAction(action, params) + break + case 'songlist': + await handleSonglistAction(action, params) + break + case 'player': + await handlePlayerAction(action as any) + break + default: throw new Error('Unknown type: ' + type) + } + } + + const rDeeplink = onDeeplink(async({ params: link }) => { + console.log(link) + if (!isInited) return + clearEnvParamsDeeplink() + try { + await handleLinkAction(link) + } catch (err: any) { + showErrorDialog(err.message) + focusWindow() + } + }) + + onBeforeUnmount(() => { + rDeeplink() + }) + + return async(envParams: LX.EnvParams) => { + if (envParams.deeplink) { + clearEnvParamsDeeplink() + try { + await handleLinkAction(envParams.deeplink) + } catch (err: any) { + showErrorDialog(err.message) + focusWindow() + } + } + isInited = true + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/useMusicAction.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/useMusicAction.js new file mode 100644 index 000000000..8247fb11b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/useMusicAction.js @@ -0,0 +1,239 @@ +import { markRaw } from '@common/utils/vueTools' +import { useRouter } from '@common/utils/vueRouter' +import { decodeName } from '@renderer/utils' +// import { allList, defaultList, loveList, userLists } from '@renderer/store/list' +import { playMusicInfo, isShowPlayerDetail } from '@renderer/store/player/state' +import { setShowPlayerDetail, addTempPlayList } from '@renderer/store/player/action' + +import { dataVerify, qualityFilter, sources } from './utils' +import { focusWindow } from '@renderer/utils/ipc' +import { playNext } from '@renderer/core/player/action' +import { toNewMusicInfo } from '@common/utils/tools' +import { LIST_IDS } from '@common/constants' +import { getOtherSource } from '@renderer/core/music/utils' + +const useSearchMusic = () => { + const router = useRouter() + + return ({ paths, data: params }) => { + let text + let source + if (params) { + text = dataVerify([ + { key: 'keywords', types: ['string', 'number'], max: 128, required: true }, + ], params).keywords + source = params.source + } else { + if (!paths.length) throw new Error('Keyword missing') + + if (paths.length > 1) { + text = paths[1] + source = paths[0] + } else { + text = paths[0] + } + + if (text.length > 128) text = text.substring(0, 128) + } + + if (isShowPlayerDetail.value) setShowPlayerDetail(false) + const sourceList = [...sources, 'all'] + source = sourceList.includes(source) ? source : null + setTimeout(() => { + router.replace({ + path: '/search', + query: { + text, + source, + }, + }) + }, 500) + focusWindow() + } +} + +const usePlayMusic = () => { + const filterInfoByPlayMusic = musicInfo => { + switch (musicInfo.source) { + case 'kw': + musicInfo = dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], required: true, max: 200 }, + { key: 'source', types: ['string'], required: true }, + { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, + { key: 'img', types: ['string'], max: 1024 }, + { key: 'albumId', types: ['string', 'number'], max: 64 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: 'albumName', types: ['string'], max: 200 }, + { key: 'types', types: ['object'], required: true }, + ], musicInfo) + break + case 'kg': + musicInfo = dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], required: true, max: 200 }, + { key: 'source', types: ['string'], required: true }, + { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, + { key: 'img', types: ['string'], max: 1024 }, + { key: 'albumId', types: ['string', 'number'], max: 64 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: '_interval', types: ['number'], max: 64 }, + { key: 'albumName', types: ['string'], max: 200 }, + { key: 'types', types: ['object'], required: true }, + + { key: 'hash', types: ['string'], required: true, max: 64 }, + ], musicInfo) + break + case 'tx': + musicInfo = dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], required: true, max: 200 }, + { key: 'source', types: ['string'], required: true }, + { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, + { key: 'img', types: ['string'], max: 1024 }, + { key: 'albumId', types: ['string', 'number'], max: 64 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: 'albumName', types: ['string'], max: 200 }, + { key: 'types', types: ['object'], required: true }, + + { key: 'strMediaMid', types: ['string'], required: true, max: 64 }, + { key: 'albumMid', types: ['string'], max: 64 }, + ], musicInfo) + break + case 'wy': + musicInfo = dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], required: true, max: 200 }, + { key: 'source', types: ['string'], required: true }, + { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, + { key: 'img', types: ['string'], max: 1024 }, + { key: 'albumId', types: ['string', 'number'], max: 64 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: 'albumName', types: ['string'], max: 200 }, + { key: 'types', types: ['object'], required: true }, + ], musicInfo) + break + case 'mg': + musicInfo = dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], required: true, max: 200 }, + { key: 'source', types: ['string'], required: true }, + { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, + { key: 'img', types: ['string'], max: 1024 }, + { key: 'albumId', types: ['string', 'number'], max: 64 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: 'albumName', types: ['string'], max: 200 }, + { key: 'types', types: ['object'], required: true }, + + { key: 'copyrightId', types: ['string', 'number'], required: true, max: 64 }, + { key: 'lrcUrl', types: ['string'], max: 1024 }, + { key: 'trcUrl', types: ['string'], max: 1024 }, + { key: 'mrcUrl', types: ['string'], max: 1024 }, + ], musicInfo) + break + default: throw new Error('Unknown source: ' + musicInfo.source) + } + musicInfo.types = qualityFilter(musicInfo.source, musicInfo.types) + return musicInfo + } + + return ({ data: _musicInfo }) => { + _musicInfo = filterInfoByPlayMusic(_musicInfo) + + let musicInfo = { + ..._musicInfo, + singer: decodeName(_musicInfo.singer), + name: decodeName(_musicInfo.name), + albumName: decodeName(_musicInfo.albumName), + otherSource: null, + _types: {}, + typeUrl: {}, + } + for (const type of musicInfo.types) { + musicInfo._types[type.type] = { size: type.size } + } + musicInfo = toNewMusicInfo(musicInfo) + markRaw(musicInfo) + const isPlaying = !!playMusicInfo.musicInfo + addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo, isTop: true }]) + if (isPlaying) playNext() + } +} + + +const useSearchPlayMusic = () => { + const verifyInfo = (info) => { + return dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], max: 200 }, + { key: 'albumName', types: ['string'], max: 200 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: 'playLater', types: ['boolean'] }, + ], info) + } + + const searchMusic = async(name, singer, albumName, interval) => { + return getOtherSource({ + name, + singer, + interval, + meta: { + albumName, + }, + source: 'local', + id: `sp_${name}_s${singer}_a${albumName}_i${interval ?? ''}`, + }) + } + return async({ paths, data }) => { + // console.log(paths, data) + let info + if (paths.length) { + let name = paths[0].trim() + let singer = '' + if (name.includes('-')) [name, singer] = name.split('-').map(val => val.trim()) + info = { + name, + singer, + } + } else info = data + info = verifyInfo(info) + if (!info.name) return + const musicList = await searchMusic(info.name, info.singer || '', info.albumName || '', info.interval || null) + if (musicList.length) { + console.log('find music:', musicList) + const musicInfo = musicList[0] + markRaw(musicInfo) + const isPlaying = !!playMusicInfo.musicInfo + if (info.playLater) { + addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo }]) + } else { + addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo, isTop: true }]) + if (isPlaying) playNext() + } + } else { + console.log('msuic not found:', info) + } + } +} + +export default () => { + const handleSearchMusic = useSearchMusic() + const handlePlayMusic = usePlayMusic() + const handleSearchPlayMusic = useSearchPlayMusic() + + + return async(action, info) => { + switch (action) { + case 'search': + handleSearchMusic(info) + break + case 'play': + handlePlayMusic(info) + break + case 'searchPlay': + await handleSearchPlayMusic(info) + break + default: throw new Error('Unknown action: ' + action) + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/usePlayerAction.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/usePlayerAction.ts new file mode 100644 index 000000000..fca662579 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/usePlayerAction.ts @@ -0,0 +1,35 @@ +import { collectMusic, dislikeMusic, pause, play, playNext, playPrev, togglePlay, uncollectMusic } from '@renderer/core/player' + +type Action = 'play' | 'pause' | 'skipNext' | 'skipPrev' | 'togglePlay' | 'collect' | 'uncollect' | 'dislike' + +export default () => { + return async(action: Action) => { + switch (action) { + case 'play': + play() + break + case 'pause': + pause() + break + case 'skipNext': + playNext() + break + case 'skipPrev': + playPrev() + break + case 'togglePlay': + togglePlay() + break + case 'collect': + collectMusic() + break + case 'uncollect': + uncollectMusic() + break + case 'dislike': + dislikeMusic() + break + default: throw new Error('Unknown action: ' + (action as any ?? '')) + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/useSonglistAction.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/useSonglistAction.js new file mode 100644 index 000000000..5fd9304e5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/useSonglistAction.js @@ -0,0 +1,113 @@ +import { useRouter, useRoute } from '@common/utils/vueRouter' +import { isShowPlayerDetail } from '@renderer/store/player/state' +import { setShowPlayerDetail } from '@renderer/store/player/action' +import usePlaySonglist from '../compositions/usePlaySonglist' +import { focusWindow } from '@renderer/utils/ipc' + +import { dataVerify, sourceVerify } from './utils' + +const useOpenSonglist = () => { + const router = useRouter() + const route = useRoute() + + const handleOpenSonglist = params => { + if (params.id) { + router[route.path == '/songList/detail' ? 'replace' : 'push']({ + path: '/songList/detail', + query: { + source: params.source, + id: params.id, + }, + }) + } else if (params.url) { + router[route.path == '/songList/detail' ? 'replace' : 'push']({ + path: '/songList/detail', + query: { + source: params.source, + id: params.url, + }, + }) + } + } + + return ({ paths, data }) => { + let songlistInfo = { + source: null, + id: null, + url: null, + } + if (data) { + songlistInfo = data + } else { + songlistInfo.source = paths[0] + songlistInfo.url = paths[1] + } + + sourceVerify(songlistInfo.source) + + songlistInfo = dataVerify([ + { key: 'source', types: ['string'] }, + { key: 'id', types: ['string', 'number'], max: 64 }, + { key: 'url', types: ['string'], max: 500 }, + ], songlistInfo) + + if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing') + if (isShowPlayerDetail.value) setShowPlayerDetail(false) + handleOpenSonglist(songlistInfo) + focusWindow() + } +} +const usePlaySonglistDetail = () => { + const playSongListDetail = usePlaySonglist() + + return async({ paths, data }) => { + let songlistInfo = { + source: null, + id: null, + url: null, + index: null, + } + if (data) { + songlistInfo = data + } else { + songlistInfo.source = paths[0] + songlistInfo.url = paths[1] + songlistInfo.index = paths[2] + if (songlistInfo.index != null) { + songlistInfo.index = parseInt(songlistInfo.index) + if (Number.isNaN(songlistInfo.index)) delete songlistInfo.index + } + } + + sourceVerify(songlistInfo.source) + + songlistInfo = dataVerify([ + { key: 'source', types: ['string'] }, + { key: 'id', types: ['string', 'number'], max: 64 }, + { key: 'url', types: ['string'], max: 500 }, + { key: 'index', types: ['number'], max: 1000000 }, + ], songlistInfo) + + if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing') + + await playSongListDetail(songlistInfo.source, songlistInfo.id ?? songlistInfo.url, songlistInfo.index) + } +} + +export default () => { + const handleOpenSonglist = useOpenSonglist() + const handlePlaySonglist = usePlaySonglistDetail() + + + return async(action, info) => { + switch (action) { + case 'open': + handleOpenSonglist(info) + break + case 'play': + await handlePlaySonglist(info) + break + default: throw new Error('Unknown action: ' + action) + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/utils.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/utils.js new file mode 100644 index 000000000..c59fcc398 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useDeeplink/utils.js @@ -0,0 +1,45 @@ +import { useI18n } from '@renderer/plugins/i18n' +import { dialog } from '@renderer/plugins/Dialog' + +export const useDialog = () => { + const t = useI18n() + const errorDialog = message => { + dialog({ + message: `${t('deep_link__handle_error_tip', { message })}`, + confirmButtonText: t('ok'), + }) + } + + return errorDialog +} + +export const sources = ['kw', 'kg', 'tx', 'wy', 'mg'] +export const sourceVerify = source => { + if (!sources.includes(source)) throw new Error('Source no match') +} + +export const qualitys = ['128k', '320k', 'flac', 'flac24bit'] +export const qualityFilter = (source, types) => { + types = types.filter(({ type }) => qualitys.includes(type)).map(({ type, size, hash }) => { + if (size != null && typeof size != 'string') throw new Error(type + ' size type no match') + if (source == 'kg' && typeof hash != 'string') throw new Error(type + ' hash type no match') + return hash == null ? { type, size } : { type, size, hash } + }) + if (!types.length) throw new Error('quality no match') + return types +} + +export const dataVerify = (rules, data) => { + const newData = {} + for (const rule of rules) { + const val = data[rule.key] + if (rule.required && val == null) throw new Error(rule.key + ' missing') + if (val != null) { + if (rule.types && !rule.types.includes(typeof val)) throw new Error(rule.key + ' type no match') + if (rule.max && String(val).length > rule.max) throw new Error(rule.key + ' max length no match') + if (rule.min && String(val).length > rule.min) throw new Error(rule.key + ' min length no match') + } + newData[rule.key] = val + } + return newData +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useEventListener.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useEventListener.ts new file mode 100644 index 000000000..2ecc1ee11 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useEventListener.ts @@ -0,0 +1,149 @@ +import { getFontSizeWithScreen } from '@renderer/utils' +import { + minWindow, + onFocus, + onSettingChanged, + onThemeChange, + openDevTools, + quitApp, + setFullScreen, + showHideWindowToggle, +} from '@renderer/utils/ipc' +import { + isFullscreen, + themeId, + themeShouldUseDarkColors, +} from '@renderer/store' +import { + appSetting, + isShowAnimation, + mergeSetting, +} from '@renderer/store/setting' + +import { + onBeforeUnmount, + watch, +} from '@common/utils/vueTools' +// import { isLinux, isProd } from '@common/utils' +import { openUrl } from '@common/utils/electron' +import { HOTKEY_COMMON } from '@common/hotKey' +import { applyTheme, getThemes } from '@renderer/store/utils' +import { clearDownKeys } from '@renderer/event' + +const handle_key_down = ({ event, type, key }: LX.KeyDownEevent) => { + // console.log(key) + if (key != 'escape' || !event || event.repeat || type == 'up' || window.lx.isEditingHotKey || (event.target as HTMLElement)?.classList.contains('ignore-esc') || event.lx_handled) return + if ((event.target as HTMLElement).tagName != 'INPUT') { + if (isFullscreen.value) { + event.lx_handled = true + void setFullScreen(false).then(fullscreen => { + isFullscreen.value = fullscreen + }) + } + return + } + (event.target as HTMLInputElement).value = '' + ;(event.target as HTMLInputElement).blur() + event.lx_handled = true +} + +const handleBodyClick = (event: MouseEvent) => { + if ((event?.target as HTMLElement)?.tagName != 'A') return + if ((event?.target as HTMLAnchorElement).host == window.location.host) return + event.preventDefault() + if (/^https?:\/\//.test((event?.target as HTMLAnchorElement).href)) void openUrl((event?.target as HTMLAnchorElement).href) +} +const handle_open_devtools = () => { + openDevTools() +} +const handle_fullscreen = (event: LX.KeyDownEevent) => { + let fullscreen = !isFullscreen.value + if (typeof event == 'boolean') { + fullscreen = event + } else if (event.event?.repeat) return + void setFullScreen(fullscreen).then(fullscreen => { + isFullscreen.value = fullscreen + }) +} +const handle_selection = (event: LX.KeyDownEevent) => { + event.event?.preventDefault() +} + +export default () => { + watch(isFullscreen, val => { + if (val) { + document.documentElement.classList.remove(window.dt ? 'disableTransparent' : 'transparent') + document.documentElement.classList.add('fullscreen') + document.documentElement.style.fontSize = `${getFontSizeWithScreen(window.screen.width)}px` + } else { + document.documentElement.classList.remove('fullscreen') + document.documentElement.classList.add(window.dt ? 'disableTransparent' : 'transparent') + document.documentElement.style.fontSize = `${appSetting['common.fontSize']}px` + } + }, { + immediate: true, + }) + + watch(isShowAnimation, val => { + if (val) { + if (document.documentElement.classList.contains('disableAnimation')) { + document.documentElement.classList.remove('disableAnimation') + } + } else { + if (!document.documentElement.classList.contains('disableAnimation')) { + document.documentElement.classList.add('disableAnimation') + } + } + }, { + immediate: true, + }) + + const rSetConfig = onSettingChanged(({ params: setting }) => { + // console.log(config) + mergeSetting(setting) + window.app_event.configUpdate(setting) + }) + + const rFocus = onFocus(() => { + clearDownKeys() + }) + + const rThemeChange = onThemeChange(({ params: setting }) => { + // console.log(setting) + if (themeShouldUseDarkColors.value == setting.shouldUseDarkColors) { + if (themeId.value == setting.theme.id) return + themeId.value = setting.theme.id + } else { + themeShouldUseDarkColors.value = setting.shouldUseDarkColors + if (themeId.value != 'auto') return + } + getThemes(({ dataPath }) => { + applyTheme('auto', appSetting['theme.lightId'], appSetting['theme.darkId'], dataPath) + }) + }) + + window.key_event.on(HOTKEY_COMMON.min.action, minWindow) + window.key_event.on(HOTKEY_COMMON.hide_toggle.action, showHideWindowToggle) + window.key_event.on(HOTKEY_COMMON.close.action, quitApp) + + window.app_event.on('keyDown', handle_key_down) + window.key_event.on('key_mod+f12_down', handle_open_devtools) + window.key_event.on('key_f11_down', handle_fullscreen) + window.key_event.on('key_mod+a_down', handle_selection) + document.body.addEventListener('click', handleBodyClick, true) + + onBeforeUnmount(() => { + window.key_event.off(HOTKEY_COMMON.min.action, minWindow) + window.key_event.off(HOTKEY_COMMON.hide_toggle.action, showHideWindowToggle) + window.key_event.off(HOTKEY_COMMON.close.action, quitApp) + + window.app_event.off('keyDown', handle_key_down) + window.key_event.off('key_mod+f12_down', handle_open_devtools) + window.key_event.off('key_f11_down', handle_fullscreen) + window.key_event.off('key_mod+a_down', handle_selection) + document.body.removeEventListener('click', handleBodyClick) + rSetConfig() + rFocus() + rThemeChange() + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useHandleEnvParams.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useHandleEnvParams.ts new file mode 100644 index 000000000..ff94e7427 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useHandleEnvParams.ts @@ -0,0 +1,89 @@ +import { useRouter } from '@common/utils/vueRouter' +import { parseUrlParams } from '@common/utils/common' +import { defaultList, loveList, userLists } from '@renderer/store/list/state' +import { getListMusics } from '@renderer/store/list/action' +import usePlaySonglist from './compositions/usePlaySonglist' +import { playList } from '@renderer/core/player' + +const getListPlayIndex = (list: LX.Music.MusicInfo[], indexStr?: string): number => { + let index: number + if (indexStr == null) { + index = 1 + } else { + index = parseInt(indexStr) + if (Number.isNaN(index)) { + index = 1 + } else { + if (index < 1) index = 1 + else if (index > list.length) index = list.length + } + } + return index - 1 +} + +const useInitEnvParamSearch = () => { + const router = useRouter() + + return (search?: string) => { + if (search == null) return + setTimeout(() => { + void router.replace({ + path: '/search', + query: { + text: search, + }, + }) + }, 1000) + } +} +const useInitEnvParamPlay = () => { + // const setPlayList = useCommit('player', 'setList') + + const playSongListDetail = usePlaySonglist() + + return async(playStr?: string) => { + if (playStr == null || typeof playStr != 'string') return + // -play="source=kw&link=链接、ID" + // -play="source=myList&name=名字" + // -play="source=myList&name=名字&index=位置" + const params = parseUrlParams(playStr) + if (params.type != 'songList') return + switch (params.source) { + case 'myList': + if (params.name != null) { + let targetList + const lists = [defaultList, loveList, ...userLists] + for (const list of lists) { + if (list.name === params.name) { + targetList = list + break + } + } + if (!targetList) return + + playList(targetList.id, getListPlayIndex(await getListMusics(targetList.id), params.index)) + } + break + case 'kw': + case 'kg': + case 'tx': + case 'mg': + case 'wy': + void playSongListDetail(params.source, params.link, parseInt(params.index)) + break + } + } +} + +export default () => { + // 处理启动参数 search + const initEnvParamSearch = useInitEnvParamSearch() + + // 处理启动参数 play + const initEnvParamPlay = useInitEnvParamPlay() + + return (envParams: LX.EnvParams) => { + initEnvParamSearch(envParams.cmdParams.search) + void initEnvParamPlay(envParams.cmdParams.play) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useInitUserApi.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useInitUserApi.ts new file mode 100644 index 000000000..4042667a4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useInitUserApi.ts @@ -0,0 +1,187 @@ +import { onBeforeUnmount, watch } from '@common/utils/vueTools' +import { useI18n } from '@renderer/plugins/i18n' +import { onUserApiStatus, getUserApiList, sendUserApiRequest as sendUserApiRequestRemote, userApiRequestCancel, onShowUserApiUpdateAlert } from '@renderer/utils/ipc' +import { openUrl } from '@common/utils/electron' +import { qualityList, userApi } from '@renderer/store' +import { appSetting } from '@renderer/store/setting' +import { dialog } from '@renderer/plugins/Dialog' +import { setUserApi } from '@renderer/core/apiSource' + +const sendUserApiRequest: typeof sendUserApiRequestRemote = async(data) => { + let stop: () => void + return new Promise((resolve, reject) => { + stop = watch(() => appSetting['common.apiSource'], () => { + reject(new Error('source changed')) + }) + void sendUserApiRequestRemote(data).then(resolve).catch(reject) + }).finally(() => { + stop() + }) +} + +export default () => { + const t = useI18n() + + const rUserApiStatus = onUserApiStatus(({ params: { status, message, apiInfo } }) => { + // console.log({ status, message, apiInfo }) + userApi.status = status + userApi.message = message + + if (!apiInfo || apiInfo.id !== appSetting['common.apiSource']) return + if (status) { + if (apiInfo.sources) { + let apis: any = {} + let qualitys: LX.QualityList = {} + for (const [source, { actions, type, qualitys: sourceQualitys }] of Object.entries(apiInfo.sources)) { + if (type != 'music') continue + apis[source as LX.Source] = {} + for (const action of actions) { + switch (action) { + case 'musicUrl': + apis[source].getMusicUrl = (songInfo: LX.Music.MusicInfo, type: LX.Quality) => { + const requestKey = `request__${Math.random().toString().substring(2)}` + return { + canceleFn() { + userApiRequestCancel(requestKey) + }, + promise: sendUserApiRequest({ + requestKey, + data: { + source, + action: 'musicUrl', + info: { + type, + musicInfo: songInfo, + }, + }, + // eslint-disable-next-line @typescript-eslint/promise-function-async + }).then(res => { + // console.log(res) + return { type, url: res.data.url } + }).catch(async err => { + console.log(err.message) + return Promise.reject(err) + }), + } + } + break + case 'lyric': + apis[source].getLyric = (songInfo: LX.Music.MusicInfo) => { + const requestKey = `request__${Math.random().toString().substring(2)}` + return { + canceleFn() { + userApiRequestCancel(requestKey) + }, + promise: sendUserApiRequest({ + requestKey, + data: { + source, + action: 'lyric', + info: { + type, + musicInfo: songInfo, + }, + }, + // eslint-disable-next-line @typescript-eslint/promise-function-async + }).then(res => { + // console.log(res) + return res.data + }).catch(async err => { + console.log(err.message) + return Promise.reject(err) + }), + } + } + break + case 'pic': + apis[source].getPic = (songInfo: LX.Music.MusicInfo) => { + const requestKey = `request__${Math.random().toString().substring(2)}` + return { + canceleFn() { + userApiRequestCancel(requestKey) + }, + promise: sendUserApiRequest({ + requestKey, + data: { + source, + action: 'pic', + info: { + type, + musicInfo: songInfo, + }, + }, + // eslint-disable-next-line @typescript-eslint/promise-function-async + }).then(res => { + // console.log(res) + return res.data + }).catch(async err => { + console.log(err.message) + return Promise.reject(err) + }), + } + } + break + default: + break + } + } + qualitys[source as LX.Source] = sourceQualitys + } + qualityList.value = qualitys + userApi.apis = apis + } + } else { + if (message) { + void dialog({ + message: `${t('user_api__init_failed_alert', { name: apiInfo.name })}\n${message}`, + selection: true, + confirmButtonText: t('ok'), + }) + } + } + if (!window.lx.apiInitPromise[1]) window.lx.apiInitPromise[2](status) + }) + + const rUserApiShowUpdateAlert = onShowUserApiUpdateAlert(({ params: { name, log, updateUrl } }) => { + if (updateUrl) { + void dialog({ + message: `${t('user_api__update_alert', { name })}\n${log}`, + selection: true, + showCancel: true, + confirmButtonText: t('user_api__update_alert_open_url'), + cancelButtonText: t('close'), + }).then(confirm => { + if (!confirm) return + window.setTimeout(() => { + void openUrl(updateUrl) + }, 300) + }) + } else { + void dialog({ + message: `${t('user_api__update_alert', { name })}\n${log}`, + selection: true, + confirmButtonText: t('ok'), + }) + } + }) + + onBeforeUnmount(() => { + rUserApiStatus() + rUserApiShowUpdateAlert() + }) + + return async() => { + await setUserApi(appSetting['common.apiSource']) + void getUserApiList().then(list => { + // console.log(list) + // if (![...apiSourceInfo.map(s => s.id), ...list.map(s => s.id)].includes(appSetting['common.apiSource'])) { + // console.warn('reset api') + // let api = apiSourceInfo.find(api => !api.disabled) + // if (api) apiSource.value = api.id + // } + userApi.list = list + }).catch(err => { + console.log(err) + }) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useOpenAPI.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useOpenAPI.ts new file mode 100644 index 000000000..0c971fc82 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useOpenAPI.ts @@ -0,0 +1,45 @@ +import { watch } from '@common/utils/vueTools' +import { appSetting } from '@renderer/store/setting' +import { sendOpenAPIAction } from '@renderer/utils/ipc' +import { openAPI } from '@renderer/store' +import { setDisableAutoPauseBySource } from '@renderer/core/lyric' + +export default () => { + const handleEnable = async(enable: boolean, port: string, bindLan: boolean) => { + await sendOpenAPIAction({ + action: 'enable', + data: { + enable, + port, + bindLan, + }, + }).then((status) => { + openAPI.address = status.address + openAPI.message = status.message + }).catch((error) => { + openAPI.address = '' + openAPI.message = error.message + }).finally(() => { + setDisableAutoPauseBySource(!!openAPI.address, 'openAPI') + }) + } + watch(() => appSetting['openAPI.enable'], enable => { + void handleEnable(enable, appSetting['openAPI.port'], appSetting['openAPI.bindLan']) + }) + + watch(() => appSetting['openAPI.port'], port => { + if (!appSetting['openAPI.enable']) return + void handleEnable(appSetting['openAPI.enable'], port, appSetting['openAPI.bindLan']) + }) + + watch(() => appSetting['openAPI.bindLan'], bindLan => { + if (!appSetting['openAPI.enable']) return + void handleEnable(appSetting['openAPI.enable'], appSetting['openAPI.port'], bindLan) + }) + + return async() => { + if (appSetting['openAPI.enable']) { + void handleEnable(true, appSetting['openAPI.port'], appSetting['openAPI.bindLan']) + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/index.ts new file mode 100644 index 000000000..6c3cc4167 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/index.ts @@ -0,0 +1,21 @@ +import { + createAudio, +} from '@renderer/plugins/player' +import useMediaDevice from './useMediaDevice' +import usePlayerEvent from './usePlayerEvent' +import usePlayer from './usePlayer' +import usePlayStatus from './usePlayStatus' + +export default () => { + createAudio() + + usePlayerEvent() + useMediaDevice() // 初始化音频驱动输出设置 + usePlayer() + const initPlayStatus = usePlayStatus() + + return () => { + void initPlayStatus() + } +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useLyric.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useLyric.ts new file mode 100644 index 000000000..9c04d5cb7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useLyric.ts @@ -0,0 +1,48 @@ +import { onBeforeUnmount, watch } from '@common/utils/vueTools' +import { debounce } from '@common/utils/common' +// import { setDesktopLyricInfo, onGetDesktopLyricInfo } from '@renderer/utils/ipc' +// import { musicInfo } from '@renderer/store/player/state' +import { + pause, + play, + setLyric, + stop, + init, + sendInfo, + setPlaybackRate, +} from '@renderer/core/lyric' +import { appSetting } from '@renderer/store/setting' + +const handleApplyPlaybackRate = debounce(setPlaybackRate, 300) + +export default () => { + init() + + const setPlayInfo = () => { + stop() + sendInfo() + } + + watch(() => appSetting['player.isShowLyricTranslation'], setLyric) + watch(() => appSetting['player.isShowLyricRoma'], setLyric) + watch(() => appSetting['player.isSwapLyricTranslationAndRoma'], setLyric) + watch(() => appSetting['player.isPlayLxlrc'], setLyric) + + window.app_event.on('play', play) + window.app_event.on('pause', pause) + window.app_event.on('stop', stop) + window.app_event.on('error', pause) + window.app_event.on('musicToggled', setPlayInfo) + window.app_event.on('lyricUpdated', setLyric) + window.app_event.on('setPlaybackRate', handleApplyPlaybackRate) + + onBeforeUnmount(() => { + window.app_event.off('play', play) + window.app_event.off('pause', pause) + window.app_event.off('stop', stop) + window.app_event.off('error', pause) + window.app_event.off('musicToggled', setPlayInfo) + window.app_event.off('lyricUpdated', setLyric) + window.app_event.off('setPlaybackRate', handleApplyPlaybackRate) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useMaxOutputChannelCount.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useMaxOutputChannelCount.ts new file mode 100644 index 000000000..4034752b4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useMaxOutputChannelCount.ts @@ -0,0 +1,14 @@ +import { watch } from '@common/utils/vueTools' +import { setMaxOutputChannelCount } from '@renderer/plugins/player' + +import { appSetting } from '@renderer/store/setting' + +export default () => { + // console.log(appSetting['player.soundEffect.panner.enable']) + setMaxOutputChannelCount(appSetting['player.isMaxOutputChannelCount']) + + watch(() => appSetting['player.isMaxOutputChannelCount'], (val) => { + setMaxOutputChannelCount(val) + }) +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useMediaDevice.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useMediaDevice.ts new file mode 100644 index 000000000..686dedb10 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useMediaDevice.ts @@ -0,0 +1,93 @@ +import { + onBeforeUnmount, + watch, +} from '@common/utils/vueTools' +import { pause } from '@renderer/core/player/action' +import { dialog } from '@renderer/plugins/Dialog' +import { setMediaDeviceId } from '@renderer/plugins/player' +import { isPlay } from '@renderer/store/player/state' +import { appSetting, saveMediaDeviceId } from '@renderer/store/setting' + +const getDevices = async() => { + const devices = await navigator.mediaDevices.enumerateDevices() + return devices.filter(({ kind }) => kind == 'audiooutput') +} + +let isShowingTipAlert = false + +export default () => { + let prevDeviceLabel: string | null = null + let prevDeviceId = '' + + const getMediaDevice = async(deviceId: string) => { + const devices = await getDevices() + let device = devices.find(device => device.deviceId === deviceId) + if (!device) { + deviceId = 'default' + device = devices.find(device => device.deviceId === deviceId) + } + + if (!device && !devices.length && !isShowingTipAlert) { + isShowingTipAlert = true + void dialog({ + message: window.i18n.t('media_device__empty_device_tip'), + confirmButtonText: window.i18n.t('ok'), + }).finally(() => { + isShowingTipAlert = false + }) + } + return device ? { label: device.label, deviceId: device.deviceId } : { label: '', deviceId: '' } + } + const setMediaDevice = async(deviceId: string, label: string) => { + prevDeviceLabel = label + // console.log(device) + setMediaDeviceId(deviceId).then(() => { + prevDeviceId = deviceId + saveMediaDeviceId(deviceId) + }).catch((err: any) => { + console.log(err) + setMediaDeviceId('default').finally(() => { + prevDeviceId = 'default' + saveMediaDeviceId('default') + }) + }) + } + + const handleDeviceChange = (label: string) => { + // console.log(device) + // console.log(appSetting['player.isMediaDeviceRemovedStopPlay'], isPlay.value, label, prevDeviceLabel) + if (label != prevDeviceLabel) { + window.app_event.playerDeviceChanged() + + if (appSetting['player.isMediaDeviceRemovedStopPlay'] && isPlay.value) { + window.lx.isPlayedStop = true + pause() + } + } + } + + const handleMediaListChange = async() => { + const mediaDeviceId = appSetting['player.mediaDeviceId'] + const device = await getMediaDevice(mediaDeviceId) + + handleDeviceChange(device.label) + + if (device.deviceId == mediaDeviceId) prevDeviceLabel = device.label + else void setMediaDevice(device.deviceId, device.label) + } + + watch(() => appSetting['player.mediaDeviceId'], (id) => { + if (prevDeviceId == id) return + void getMediaDevice(id).then(async({ deviceId, label }) => setMediaDevice(deviceId, label)) + }) + + void getMediaDevice(appSetting['player.mediaDeviceId']).then(async({ deviceId, label }) => setMediaDevice(deviceId, label)) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + navigator.mediaDevices.addEventListener('devicechange', handleMediaListChange) + + onBeforeUnmount(() => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + navigator.mediaDevices.removeEventListener('devicechange', handleMediaListChange) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useMediaSessionInfo.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useMediaSessionInfo.ts new file mode 100644 index 000000000..1c78c5a70 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useMediaSessionInfo.ts @@ -0,0 +1,154 @@ +import { onBeforeUnmount } from '@common/utils/vueTools' +import { getDuration, getPlaybackRate, getCurrentTime } from '@renderer/plugins/player' +import { isPlay, musicInfo, playMusicInfo } from '@renderer/store/player/state' +import { playProgress } from '@renderer/store/player/playProgress' +import { pause, play, playNext, playPrev, stop } from '@renderer/core/player' + +export default () => { + // 创建一个空白音频以保持对 Media Session 的注册 + const emptyAudio = new Audio() + emptyAudio.autoplay = false + emptyAudio.src = require('@renderer/assets/medias/Silence02s.mp3') + emptyAudio.controls = false + emptyAudio.preload = 'auto' + emptyAudio.onplaying = () => { + emptyAudio.pause() + } + void emptyAudio.play() + let prevPicUrl = '' + + const updateMediaSessionInfo = () => { + if (musicInfo.id == null) { + navigator.mediaSession.metadata = null + return + } + const mediaMetadata: MediaMetadata = { + title: musicInfo.name, + artist: musicInfo.singer, + album: musicInfo.album, + artwork: [], + } + if (musicInfo.pic) { + const pic = new Image() + pic.src = prevPicUrl = musicInfo.pic + pic.onload = () => { + if (prevPicUrl == pic.src) { + mediaMetadata.artwork = [{ src: pic.src }] + // @ts-expect-error + navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata) + } + } + } else prevPicUrl = '' + + // @ts-expect-error + navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata) + } + + const updatePositionState = (state: { + duration?: number + position?: number + playbackRate?: number + } = {}) => { + navigator.mediaSession.setPositionState({ + duration: state.duration ?? getDuration(), + playbackRate: state.playbackRate ?? getPlaybackRate(), + position: state.position ?? getCurrentTime(), + }) + } + + const setProgress = (time: number) => { + window.app_event.setProgress(time) + } + + const setStop = () => { + stop() + } + const handlePlay = () => { + navigator.mediaSession.playbackState = 'playing' + } + const handlePause = () => { + navigator.mediaSession.playbackState = 'paused' + } + const handleStop = () => { + navigator.mediaSession.playbackState = 'none' + } + const handleSetPlayInfo = () => { + void emptyAudio.play().finally(() => { + updateMediaSessionInfo() + updatePositionState({ + position: playProgress.nowPlayTime, + duration: playProgress.maxPlayTime, + }) + handlePause() + }) + } + + // const registerMediaSessionHandler = () => { + navigator.mediaSession.setActionHandler('play', () => { + if (isPlay.value || !playMusicInfo) return + console.log('play') + play() + }) + navigator.mediaSession.setActionHandler('pause', () => { + if (!isPlay.value || !playMusicInfo) return + console.log('pause') + pause() + }) + navigator.mediaSession.setActionHandler('stop', () => { + console.log('stop') + setStop() + }) + navigator.mediaSession.setActionHandler('seekbackward', details => { + console.log('seekbackward') + const seekOffset = details.seekOffset ?? 5 + setProgress(Math.max(getCurrentTime() - seekOffset, 0)) + }) + navigator.mediaSession.setActionHandler('seekforward', details => { + console.log('seekforward') + const seekOffset = details.seekOffset ?? 5 + setProgress(Math.min(getCurrentTime() + seekOffset, getDuration())) + }) + navigator.mediaSession.setActionHandler('seekto', details => { + console.log('seekto', details.seekTime) + if (details.seekTime == null) return + let time = Math.min(details.seekTime, getDuration()) + time = Math.max(time, 0) + setProgress(time) + }) + navigator.mediaSession.setActionHandler('previoustrack', () => { + console.log('previoustrack') + void playPrev() + }) + navigator.mediaSession.setActionHandler('nexttrack', () => { + console.log('nexttrack') + void playNext() + }) + // navigator.mediaSession.setActionHandler('skipad', () => { + // console.log('') + // }) + // } + + window.app_event.on('playerLoadeddata', updatePositionState) + window.app_event.on('playerPlaying', updatePositionState) + window.app_event.on('play', handlePlay) + window.app_event.on('pause', handlePause) + window.app_event.on('stop', handleStop) + window.app_event.on('error', handlePause) + window.app_event.on('playerEmptied', handleSetPlayInfo) + // window.app_event.on('playerLoadstart', handleSetPlayInfo) + window.app_event.on('musicToggled', handleSetPlayInfo) + window.app_event.on('picUpdated', updateMediaSessionInfo) + + onBeforeUnmount(() => { + window.app_event.off('playerLoadeddata', updatePositionState) + window.app_event.off('playerPlaying', updatePositionState) + window.app_event.off('play', handlePlay) + window.app_event.off('pause', handlePause) + window.app_event.off('stop', handleStop) + window.app_event.off('error', handlePause) + window.app_event.off('playerEmptied', handleSetPlayInfo) + // window.app_event.off('playerLoadstart', handleSetPlayInfo) + window.app_event.off('musicToggled', handleSetPlayInfo) + window.app_event.off('picUpdated', updateMediaSessionInfo) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayEvent.ts new file mode 100644 index 000000000..5401f45fb --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayEvent.ts @@ -0,0 +1,138 @@ +import { onBeforeUnmount } from '@common/utils/vueTools' +import { useI18n } from '@renderer/plugins/i18n' +import { musicInfo, playMusicInfo } from '@renderer/store/player/state' +import { setStop, isEmpty } from '@renderer/plugins/player' +import { playNext, setMusicUrl } from '@renderer/core/player' +import { setAllStatus } from '@renderer/store/player/action' +import { appSetting } from '@renderer/store/setting' + +export default () => { + const t = useI18n() + let retryNum = 0 + let prevTimeoutId: string | null = null + + let loadingTimeout: NodeJS.Timeout | null = null + let delayNextTimeout: NodeJS.Timeout | null = null + const startLoadingTimeout = () => { + // console.log('start load timeout') + clearLoadingTimeout() + loadingTimeout = setTimeout(() => { + if (window.lx.isPlayedStop) { + prevTimeoutId = null + setAllStatus('') + return + } + + // 如果加载超时,则尝试刷新URL + if (prevTimeoutId == musicInfo.id) { + prevTimeoutId = null + void playNext(true) + } else { + prevTimeoutId = musicInfo.id + if (playMusicInfo.musicInfo) setMusicUrl(playMusicInfo.musicInfo, true) + } + }, 25000) + } + const clearLoadingTimeout = () => { + if (!loadingTimeout) return + // console.log('clear load timeout') + clearTimeout(loadingTimeout) + loadingTimeout = null + } + + const clearDelayNextTimeout = () => { + // console.log(this.delayNextTimeout) + if (!delayNextTimeout) return + clearTimeout(delayNextTimeout) + delayNextTimeout = null + } + const addDelayNextTimeout = () => { + clearDelayNextTimeout() + delayNextTimeout = setTimeout(() => { + if (window.lx.isPlayedStop) { + setAllStatus('') + return + } + void playNext(true) + }, 5000) + } + + const handleLoadstart = () => { + if (window.lx.isPlayedStop) return + if (appSetting['player.autoSkipOnError']) startLoadingTimeout() + setAllStatus(t('player__loading')) + } + + const handleLoadeddata = () => { + setAllStatus(t('player__loading')) + } + + const handlePlaying = () => { + setAllStatus('') + clearLoadingTimeout() + } + + const handleEmpied = () => { + clearDelayNextTimeout() + clearLoadingTimeout() + } + + const handleWating = () => { + setAllStatus(t('player__buffering')) + } + + const handleError = (errCode?: number) => { + if (!musicInfo.id) return + clearLoadingTimeout() + if (window.lx.isPlayedStop) return + if (!isEmpty()) setStop() + if (playMusicInfo.musicInfo && errCode !== 1 && retryNum < 2) { // 若音频URL无效则尝试刷新2次URL + // console.log(this.retryNum) + retryNum++ + setMusicUrl(playMusicInfo.musicInfo, true) + setAllStatus(t('player__refresh_url')) + return + } + + if (appSetting['player.autoSkipOnError']) { + if (document.hidden) { + console.warn('error skip to next') + void playNext(true) + } else { + setAllStatus(t('player__error')) + setTimeout(addDelayNextTimeout) + } + } + } + + const handleSetPlayInfo = () => { + retryNum = 0 + prevTimeoutId = null + clearDelayNextTimeout() + clearLoadingTimeout() + } + + // const handlePlayedStop = () => { + // clearDelayNextTimeout() + // clearLoadingTimeout() + // } + + + window.app_event.on('playerLoadstart', handleLoadstart) + window.app_event.on('playerLoadeddata', handleLoadeddata) + window.app_event.on('playerPlaying', handlePlaying) + window.app_event.on('playerWaiting', handleWating) + window.app_event.on('playerEmptied', handleEmpied) + window.app_event.on('playerError', handleError) + window.app_event.on('musicToggled', handleSetPlayInfo) + + onBeforeUnmount(() => { + window.app_event.off('playerLoadstart', handleLoadstart) + window.app_event.off('playerLoadeddata', handleLoadeddata) + window.app_event.off('playerPlaying', handlePlaying) + window.app_event.off('playerWaiting', handleWating) + window.app_event.off('playerEmptied', handleEmpied) + window.app_event.off('playerError', handleError) + window.app_event.off('musicToggled', handleSetPlayInfo) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayProgress.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayProgress.ts new file mode 100644 index 000000000..a7d1abd49 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayProgress.ts @@ -0,0 +1,208 @@ +import { onBeforeUnmount, watch } from '@common/utils/vueTools' +import { formatPlayTime2, getRandom } from '@common/utils/common' +import { throttle } from '@common/utils' +import { savePlayInfo } from '@renderer/utils/ipc' +import { onTimeupdate, getCurrentTime, getDuration, setCurrentTime, onVisibilityChange } from '@renderer/plugins/player' +import { playProgress, setNowPlayTime, setMaxplayTime } from '@renderer/store/player/playProgress' +import { musicInfo, playMusicInfo, playInfo } from '@renderer/store/player/state' +// import { getList } from '@renderer/store/utils' +import { appSetting } from '@renderer/store/setting' +import { playNext } from '@renderer/core/player' +import { updateListMusics } from '@renderer/store/list/action' + +const delaySavePlayInfo = throttle(savePlayInfo, 2000) + +export default () => { + let restorePlayTime = 0 + const mediaBuffer: { + timeout: NodeJS.Timeout | null + playTime: number + } = { + timeout: null, + playTime: 0, + } + + // const updateMusicInfo = useCommit('list', 'updateMusicInfo') + + const startBuffering = () => { + console.log('start t') + if (mediaBuffer.timeout) return + mediaBuffer.timeout = setTimeout(() => { + mediaBuffer.timeout = null + if (window.lx.isPlayedStop) return + const currentTime = getCurrentTime() + + mediaBuffer.playTime ||= currentTime + let skipTime = currentTime + getRandom(3, 6) + if (skipTime > playProgress.maxPlayTime) skipTime = (playProgress.maxPlayTime - currentTime) / 2 + if (skipTime - mediaBuffer.playTime < 1 || playProgress.maxPlayTime - skipTime < 1) { + mediaBuffer.playTime = 0 + if (appSetting['player.autoSkipOnError']) { + console.warn('buffering end') + void playNext(true) + } + return + } + startBuffering() + setCurrentTime(skipTime) + console.log(mediaBuffer.playTime) + console.log(currentTime) + }, 3000) + } + const clearBufferTimeout = () => { + console.log('clear t') + if (!mediaBuffer.timeout) return + clearTimeout(mediaBuffer.timeout) + mediaBuffer.timeout = null + mediaBuffer.playTime = 0 + } + + const setProgress = (time: number, maxTime?: number) => { + if (!musicInfo.id) return + if (maxTime != null) setMaxplayTime(maxTime) + console.log('setProgress', time, maxTime) + if (time > 0) restorePlayTime = time + if (mediaBuffer.playTime) { + clearBufferTimeout() + mediaBuffer.playTime = time + startBuffering() + } + setNowPlayTime(time) + setCurrentTime(time) + + // if (!isPlay) audio.play() + } + + const handlePause = () => { + clearBufferTimeout() + } + + const handleStop = () => { + setNowPlayTime(0) + setMaxplayTime(0) + } + + const handleError = () => { + restorePlayTime ||= getCurrentTime() // 记录出错的播放时间 + console.log('handleError') + } + + const handleLoadeddata = () => { + setMaxplayTime(getDuration()) + + if (playMusicInfo.musicInfo && 'source' in playMusicInfo.musicInfo && !playMusicInfo.musicInfo.interval) { + // console.log(formatPlayTime2(playProgress.maxPlayTime)) + + if (playMusicInfo.listId) { + void updateListMusics([{ + id: playMusicInfo.listId, + musicInfo: { + ...playMusicInfo.musicInfo, + interval: formatPlayTime2(playProgress.maxPlayTime), + }, + }]) + } + } + } + + const handlePlaying = () => { + console.log('handlePlaying', mediaBuffer.playTime, restorePlayTime) + clearBufferTimeout() + if (mediaBuffer.playTime) { + let playTime = mediaBuffer.playTime + mediaBuffer.playTime = 0 + setCurrentTime(playTime) + } else if (restorePlayTime) { + setCurrentTime(restorePlayTime) + restorePlayTime = 0 + } + } + const handleWating = () => { + startBuffering() + } + + const handleEmpied = () => { + mediaBuffer.playTime = 0 + clearBufferTimeout() + } + + const handleSetPlayInfo = () => { + // restorePlayTime = playProgress.nowPlayTime + setCurrentTime(restorePlayTime = playProgress.nowPlayTime) + // setMaxplayTime(playProgress.maxPlayTime) + handlePause() + if (!playMusicInfo.isTempPlay && playMusicInfo.listId) { + delaySavePlayInfo({ + time: playProgress.nowPlayTime, + maxTime: playProgress.maxPlayTime, + listId: playMusicInfo.listId, + index: playInfo.playIndex, + }) + } + } + + watch(() => playProgress.nowPlayTime, (newValue, oldValue) => { + if (Math.abs(newValue - oldValue) > 2) window.app_event.activePlayProgressTransition() + if (appSetting['player.isSavePlayTime'] && !playMusicInfo.isTempPlay) { + delaySavePlayInfo({ + time: newValue, + maxTime: playProgress.maxPlayTime, + listId: playMusicInfo.listId as string, + index: playInfo.playIndex, + }) + } + }) + watch(() => playProgress.maxPlayTime, maxPlayTime => { + if (!playMusicInfo.isTempPlay) { + delaySavePlayInfo({ + time: playProgress.nowPlayTime, + maxTime: maxPlayTime, + listId: playMusicInfo.listId as string, + index: playInfo.playIndex, + }) + } + }) + + // window.app_event.on('play', handlePlay) + window.app_event.on('pause', handlePause) + window.app_event.on('stop', handleStop) + window.app_event.on('error', handleError) + window.app_event.on('setProgress', setProgress) + // window.app_event.on(eventPlayerNames.restorePlay, handleRestorePlay) + window.app_event.on('playerLoadeddata', handleLoadeddata) + window.app_event.on('playerPlaying', handlePlaying) + window.app_event.on('playerWaiting', handleWating) + window.app_event.on('playerEmptied', handleEmpied) + window.app_event.on('musicToggled', handleSetPlayInfo) + + const rOnTimeupdate = onTimeupdate(() => { + setNowPlayTime(getCurrentTime()) + }) + + let currentPlayTime = 0 + const rVisibilityChange = onVisibilityChange(() => { + if (document.hidden) { + currentPlayTime = playProgress.nowPlayTime + } else { + if (Math.abs(playProgress.nowPlayTime - currentPlayTime) > 2) { + window.app_event.activePlayProgressTransition() + } + } + }) + + onBeforeUnmount(() => { + rOnTimeupdate() + rVisibilityChange() + // window.app_event.off('play', handlePlay) + window.app_event.off('pause', handlePause) + window.app_event.off('stop', handleStop) + window.app_event.off('error', handleError) + window.app_event.off('setProgress', setProgress) + // window.app_event.off(eventPlayerNames.restorePlay, handleRestorePlay) + window.app_event.off('playerLoadeddata', handleLoadeddata) + window.app_event.off('playerPlaying', handlePlaying) + window.app_event.off('playerWaiting', handleWating) + window.app_event.off('playerEmptied', handleEmpied) + window.app_event.off('musicToggled', handleSetPlayInfo) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayStatus.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayStatus.ts new file mode 100644 index 000000000..73b9d250d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayStatus.ts @@ -0,0 +1,199 @@ +import { onBeforeUnmount, watch } from '@common/utils/vueTools' +import { sendPlayerStatus, onPlayerAction } from '@renderer/utils/ipc' +// import store from '@renderer/store' + +import { loveList } from '@renderer/store/list/state' +import { addListMusics, removeListMusics, checkListExistMusic } from '@renderer/store/list/action' +import { playMusicInfo, musicInfo } from '@renderer/store/player/state' +import { throttle } from '@common/utils' +import { pause, play, playNext, playPrev } from '@renderer/core/player' +import { playProgress } from '@renderer/store/player/playProgress' +import { appSetting } from '@renderer/store/setting' +import { lyric } from '@renderer/store/player/lyric' + +export default () => { + // const setVisibleDesktopLyric = useCommit('setVisibleDesktopLyric') + // const setLockDesktopLyric = useCommit('setLockDesktopLyric') + let collect = false + + const updateCollectStatus = async() => { + let status = !!playMusicInfo.musicInfo && await checkListExistMusic(loveList.id, playMusicInfo.musicInfo.id) + if (collect == status) return false + collect = status + return true + } + + const handlePlay = () => { + sendPlayerStatus({ status: 'playing' }) + } + const handlePause = () => { + sendPlayerStatus({ status: 'paused' }) + } + const handleStop = () => { + if (playMusicInfo.musicInfo != null) return + sendPlayerStatus({ status: 'stoped' }) + } + const handleError = () => { + sendPlayerStatus({ status: 'error' }) + } + const handleSetPlayInfo = async() => { + await updateCollectStatus() + sendPlayerStatus({ + collect, + name: musicInfo.name, + singer: musicInfo.singer, + albumName: musicInfo.album, + picUrl: musicInfo.pic ?? '', + lyric: musicInfo.lrc ?? '', + lyricLineText: '', + lyricLineAllText: '', + }) + } + const handleSetLyric = () => { + sendPlayerStatus({ + lyric: musicInfo.lrc ?? '', + tlyric: musicInfo.tlrc ?? '', + rlyric: musicInfo.rlrc ?? '', + lxlyric: musicInfo.lxlrc ?? '', + lyricLineText: '', + lyricLineAllText: '', + }) + } + const handleSetPic = () => { + sendPlayerStatus({ + picUrl: musicInfo.pic ?? '', + }) + } + const handleSetLyricLine = (text: string, line: number) => { + let curLine = lyric.lines[line]?.extendedLyrics.join('\n') ?? '' + sendPlayerStatus({ + lyricLineText: text, + lyricLineAllText: curLine ? text + '\n' + curLine : text, + }) + } + // const handleSetTaskbarThumbnailClip = (clip) => { + // setTaskbarThumbnailClip(clip) + // } + const throttleListChange = throttle(async listIds => { + if (!listIds.includes(loveList.id)) return + if (await updateCollectStatus()) sendPlayerStatus({ collect }) + }) + // const updateSetting = () => { + // const setting = store.getters.setting + // buttons.lrc = setting.desktopLyric.enable + // buttons.lockLrc = setting.desktopLyric.isLock + // setButtons() + // } + const rTaskbarThumbarClick = onPlayerAction(async({ params: { action, data } }) => { + switch (action) { + case 'play': + play() + break + case 'pause': + pause() + break + case 'prev': + void playPrev() + break + case 'next': + void playNext() + break + case 'collect': + if (!playMusicInfo.musicInfo) return + void addListMusics(loveList.id, ['progress' in playMusicInfo.musicInfo ? playMusicInfo.musicInfo.metadata.musicInfo : playMusicInfo.musicInfo]) + if (await updateCollectStatus()) sendPlayerStatus({ collect }) + break + case 'unCollect': + if (!playMusicInfo.musicInfo) return + void removeListMusics({ listId: loveList.id, ids: ['progress' in playMusicInfo.musicInfo ? playMusicInfo.musicInfo.metadata.musicInfo.id : playMusicInfo.musicInfo.id] }) + if (await updateCollectStatus()) sendPlayerStatus({ collect }) + break + case 'seek': { + let progress = data as number + if (progress < 0) progress = 0 + else if (progress > playProgress.maxPlayTime) progress = playProgress.maxPlayTime + window.app_event.setProgress(progress) + break + } + case 'mute': + window.app_event.setVolumeIsMute(data as boolean) + break + case 'volume': + window.app_event.setVolume(data as number) + break + // case 'lrc': + // setVisibleDesktopLyric(true) + // updateSetting() + // break + // case 'unLrc': + // setVisibleDesktopLyric(false) + // updateSetting() + // break + // case 'lockLrc': + // setLockDesktopLyric(true) + // updateSetting() + // break + // case 'unlockLrc': + // setLockDesktopLyric(false) + // updateSetting() + // break + } + }) + watch(() => playProgress.nowPlayTime, (newValue, oldValue) => { + // console.log(playProgress.nowPlayTime, newValue, oldValue) + // if (newValue.toFixed(2) === oldValue.toFixed(2)) return + // console.log(playProgress.nowPlayTime) + sendPlayerStatus({ progress: newValue }) + }) + watch(() => playProgress.maxPlayTime, (newValue) => { + sendPlayerStatus({ duration: newValue }) + }) + watch(() => appSetting['player.playbackRate'], rate => { + sendPlayerStatus({ playbackRate: rate }) + }) + + window.app_event.on('play', handlePlay) + window.app_event.on('pause', handlePause) + window.app_event.on('stop', handleStop) + window.app_event.on('error', handleError) + window.app_event.on('musicToggled', handleSetPlayInfo) + window.app_event.on('lyricUpdated', handleSetLyric) + window.app_event.on('picUpdated', handleSetPic) + window.app_event.on('lyricLinePlay', handleSetLyricLine) + // window.app_event.on(eventTaskbarNames.setTaskbarThumbnailClip, handleSetTaskbarThumbnailClip) + window.app_event.on('myListUpdate', throttleListChange) + + onBeforeUnmount(() => { + rTaskbarThumbarClick() + window.app_event.off('play', handlePlay) + window.app_event.off('pause', handlePause) + window.app_event.off('stop', handleStop) + window.app_event.off('error', handleError) + window.app_event.off('musicToggled', handleSetPlayInfo) + window.app_event.off('lyricUpdated', handleSetLyric) + window.app_event.off('picUpdated', handleSetPic) + window.app_event.off('lyricLinePlay', handleSetLyricLine) + // window.app_event.off(eventTaskbarNames.setTaskbarThumbnailClip, handleSetTaskbarThumbnailClip) + window.app_event.off('myListUpdate', throttleListChange) + }) + + return async() => { + // const setting = store.getters.setting + // buttons.lrc = setting.desktopLyric.enable + // buttons.lockLrc = setting.desktopLyric.isLock + await updateCollectStatus() + if (playMusicInfo.musicInfo == null) return + sendPlayerStatus({ + collect, + name: musicInfo.name, + singer: musicInfo.singer, + albumName: musicInfo.album, + playbackRate: appSetting['player.playbackRate'], + picUrl: musicInfo.pic ?? '', + lyric: musicInfo.lrc ?? '', + tlyric: musicInfo.tlrc ?? '', + rlyric: musicInfo.rlrc ?? '', + lxlyric: musicInfo.lxlrc ?? '', + }) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlaybackRate.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlaybackRate.ts new file mode 100644 index 000000000..8f2236639 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlaybackRate.ts @@ -0,0 +1,59 @@ +import { onBeforeUnmount, watch } from '@common/utils/vueTools' +import { setPlaybackRate as setPlayerPlaybackRate, setPreservesPitch } from '@renderer/plugins/player' + +import { debounce } from '@common/utils' +// import { HOTKEY_PLAYER } from '@common/hotKey' +import { playbackRate, setPlaybackRate } from '@renderer/store/player/playbackRate' +import { appSetting, savePlaybackRate } from '@renderer/store/setting' + +export default () => { + const handleSavePlaybackRate = debounce(savePlaybackRate, 300) + + setPlaybackRate(appSetting['player.playbackRate']) + setPlayerPlaybackRate(appSetting['player.playbackRate']) + setPreservesPitch(appSetting['player.preservesPitch']) + + + const handleSetPlaybackRate = (num: number) => { + const rate = num < 0.5 ? 0.5 : num > 2 ? 2 : num + setPlaybackRate(rate) + } + + // const handleSetPlaybackRateUp = (step = 0.02) => { + // handleSetPlaybackRate(volume.value + step) + // } + // const handleSetPlaybackRateDown = (step = 0.02) => { + // handleSetPlaybackRate(volume.value - step) + // } + + // const hotkeyVolumeUp = () => { + // handleSetPlaybackRateUp() + // } + // const hotkeyVolumeDown = () => { + // handleSetPlaybackRateDown() + // } + + watch(playbackRate, rate => { + handleSavePlaybackRate(rate) + setPlayerPlaybackRate(rate) + }) + watch(() => appSetting['player.playbackRate'], rate => { + setPlaybackRate(rate) + }) + + + watch(() => appSetting['player.preservesPitch'], preservesPitch => { + setPreservesPitch(preservesPitch) + }) + + + // window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp) + // window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown) + window.app_event.on('setPlaybackRate', handleSetPlaybackRate) + + onBeforeUnmount(() => { + // window.key_event.off(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp) + // window.key_event.off(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown) + window.app_event.off('setPlaybackRate', handleSetPlaybackRate) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayer.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayer.ts new file mode 100644 index 000000000..af617738e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayer.ts @@ -0,0 +1,181 @@ +import { onBeforeUnmount, watch } from '@common/utils/vueTools' +import { useI18n } from '@renderer/plugins/i18n' +import { setTitle } from '@renderer/utils' + +import { + getCurrentTime, + getDuration, + setPause, setStop, +} from '@renderer/plugins/player' + +import useMediaSessionInfo from './useMediaSessionInfo' +import usePlayProgress from './usePlayProgress' +import usePlayEvent from './usePlayEvent' + +import { + musicInfo, + playMusicInfo, + playedList, +} from '@renderer/store/player/state' +import { + setPlay, + setAllStatus, + addPlayedList, + clearPlayedList, + // resetPlayerMusicInfo, +} from '@renderer/store/player/action' + +import { appSetting } from '@renderer/store/setting' + +import useLyric from './useLyric' +import useVolume from './useVolume' +import useWatchList from './useWatchList' +import { HOTKEY_PLAYER } from '@common/hotKey' +import { playNext, pause, playPrev, togglePlay, collectMusic, uncollectMusic, dislikeMusic } from '@renderer/core/player' +import usePlaybackRate from './usePlaybackRate' +import useSoundEffect from './useSoundEffect' +import useMaxOutputChannelCount from './useMaxOutputChannelCount' +import { setPowerSaveBlocker } from '@renderer/core/player/utils' +import usePreloadNextMusic from './usePreloadNextMusic' + + +export default () => { + const t = useI18n() + + usePlayProgress() + useMediaSessionInfo() + usePlayEvent() + useLyric() + useVolume() + useMaxOutputChannelCount() + useSoundEffect() + usePlaybackRate() + useWatchList() + usePreloadNextMusic() + + const handlePlayNext = () => { + void playNext() + } + const handlePlayPrev = () => { + void playPrev() + } + + const addPowerSaveBlocker = () => { + setPowerSaveBlocker(true) + } + const removePowerSaveBlocker = () => { + setPowerSaveBlocker(false) + } + + const setPlayStatus = () => { + setPlay(true) + } + const setPauseStatus = () => { + setPlay(false) + if (window.lx.isPlayedStop) pause() + removePowerSaveBlocker() + } + + const handleUpdatePlayInfo = () => { + setTitle(musicInfo.id ? `${musicInfo.name} - ${musicInfo.singer}` : null) + } + + const handleCanplay = () => { + if (window.lx.isPlayedStop) { + setPause() + } + } + const handleEnded = () => { + // setTimeout(() => { + setAllStatus(t('player__end')) + if (window.lx.isPlayedStop) { + console.log('played stop') + return + } + // resetPlayerMusicInfo() + // window.app_event.stop() + void playNext(true) + // }) + } + + const setProgress = (time: number) => { + window.app_event.setProgress(time) + } + const handleSeekforward = () => { + const seekOffset = 5 + const curTime = getCurrentTime() + const time = Math.min(getCurrentTime() + seekOffset, getDuration()) + if (Math.trunc(curTime) == Math.trunc(time)) return + setProgress(time) + } + const handleSeekbackward = () => { + const seekOffset = 5 + const curTime = getCurrentTime() + const time = Math.max(getCurrentTime() - seekOffset, 0) + if (Math.trunc(curTime) == Math.trunc(time)) return + setProgress(time) + } + + const setStopStatus = () => { + setPlay(false) + setTitle(null) + setAllStatus('') + setStop() + removePowerSaveBlocker() + } + + watch(() => appSetting['player.togglePlayMethod'], newValue => { + // setLoopPlay(newValue == 'singleLoop') + if (playedList.length) clearPlayedList() + if (newValue == 'random' && playMusicInfo.musicInfo && !playMusicInfo.isTempPlay) addPlayedList({ ...(playMusicInfo as LX.Player.PlayMusicInfo) }) + }) + + // setLoopPlay(appSetting['player.togglePlayMethod'] == 'singleLoop') + + + window.key_event.on(HOTKEY_PLAYER.next.action, handlePlayNext) + window.key_event.on(HOTKEY_PLAYER.prev.action, handlePlayPrev) + window.key_event.on(HOTKEY_PLAYER.toggle_play.action, togglePlay) + window.key_event.on(HOTKEY_PLAYER.music_love.action, collectMusic) + window.key_event.on(HOTKEY_PLAYER.music_unlove.action, uncollectMusic) + window.key_event.on(HOTKEY_PLAYER.music_dislike.action, dislikeMusic) + window.key_event.on(HOTKEY_PLAYER.seekbackward.action, handleSeekbackward) + window.key_event.on(HOTKEY_PLAYER.seekforward.action, handleSeekforward) + + window.app_event.on('play', setPlayStatus) + window.app_event.on('pause', setPauseStatus) + window.app_event.on('error', setPauseStatus) + window.app_event.on('stop', setStopStatus) + window.app_event.on('musicToggled', handleUpdatePlayInfo) + window.app_event.on('playerCanplay', handleCanplay) + window.app_event.on('playerPlaying', addPowerSaveBlocker) + window.app_event.on('playerEmptied', removePowerSaveBlocker) + + window.app_event.on('playerEnded', handleEnded) + + + onBeforeUnmount(() => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + window.key_event.off(HOTKEY_PLAYER.next.action, handlePlayNext) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + window.key_event.off(HOTKEY_PLAYER.prev.action, handlePlayPrev) + window.key_event.off(HOTKEY_PLAYER.toggle_play.action, togglePlay) + window.key_event.off(HOTKEY_PLAYER.music_love.action, collectMusic) + window.key_event.off(HOTKEY_PLAYER.music_unlove.action, uncollectMusic) + window.key_event.off(HOTKEY_PLAYER.music_dislike.action, dislikeMusic) + window.key_event.off(HOTKEY_PLAYER.seekbackward.action, handleSeekbackward) + window.key_event.off(HOTKEY_PLAYER.seekforward.action, handleSeekforward) + + + window.app_event.off('play', setPlayStatus) + window.app_event.off('pause', setPauseStatus) + window.app_event.off('error', setPauseStatus) + window.app_event.off('stop', setStopStatus) + window.app_event.off('musicToggled', handleUpdatePlayInfo) + window.app_event.off('playerPlaying', addPowerSaveBlocker) + window.app_event.off('playerEmptied', removePowerSaveBlocker) + window.app_event.off('playerCanplay', handleCanplay) + + window.app_event.off('playerEnded', handleEnded) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayerEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayerEvent.ts new file mode 100644 index 000000000..e22357008 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePlayerEvent.ts @@ -0,0 +1,73 @@ +import { onBeforeUnmount } from '@common/utils/vueTools' +import { + onPlaying, + onPause, + onEnded, + onError, + onLoadeddata, + onLoadstart, + onCanplay, + onEmptied, + onWaiting, + getErrorCode, +} from '@renderer/plugins/player' + + +export default () => { + const rOnPlaying = onPlaying(() => { + console.log('onPlaying') + window.app_event.playerPlaying() + window.app_event.play() + }) + const rOnPause = onPause(() => { + console.log('onPause') + window.app_event.playerPause() + window.app_event.pause() + }) + const rOnEnded = onEnded(() => { + console.log('onEnded') + window.app_event.playerEnded() + // window.app_event.pause() + }) + const rOnError = onError(() => { + console.log('onError') + const errorCode = getErrorCode() + window.app_event.error(errorCode) + window.app_event.playerError(errorCode) + }) + const rOnLoadeddata = onLoadeddata(() => { + console.log('onLoadeddata') + window.app_event.playerLoadeddata() + }) + const rOnLoadstart = onLoadstart(() => { + console.log('onLoadstart') + window.app_event.playerLoadstart() + }) + const rOnCanplay = onCanplay(() => { + console.log('onCanplay') + window.app_event.playerCanplay() + }) + const rOnEmptied = onEmptied(() => { + console.log('onEmptied') + window.app_event.playerEmptied() + // window.app_event.stop() + }) + const rOnWaiting = onWaiting(() => { + console.log('onWaiting') + window.app_event.pause() + window.app_event.playerWaiting() + }) + + + onBeforeUnmount(() => { + rOnPlaying() + rOnPause() + rOnEnded() + rOnError() + rOnLoadeddata() + rOnLoadstart() + rOnCanplay() + rOnEmptied() + rOnWaiting() + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePreloadNextMusic.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePreloadNextMusic.ts new file mode 100644 index 000000000..650a865e6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/usePreloadNextMusic.ts @@ -0,0 +1,114 @@ +import { onBeforeUnmount, watch } from '@common/utils/vueTools' +import { onTimeupdate, getCurrentTime } from '@renderer/plugins/player' +import { playProgress } from '@renderer/store/player/playProgress' +import { musicInfo } from '@renderer/store/player/state' +// import { getList } from '@renderer/store/utils' +import { getNextPlayMusicInfo, resetRandomNextMusicInfo } from '@renderer/core/player' +import { getMusicUrl } from '@renderer/core/music' +import { appSetting } from '@renderer/store/setting' + +let audio: HTMLAudioElement +const initAudio = () => { + if (audio) return + audio = new Audio() + audio.controls = false + audio.preload = 'auto' + audio.crossOrigin = 'anonymous' + audio.muted = true + audio.volume = 0 + audio.autoplay = true + audio.addEventListener('playing', () => { + audio.pause() + }) +} +const checkMusicUrl = async(url: string): Promise => { + initAudio() + return new Promise((resolve) => { + const clear = () => { + audio.removeEventListener('error', handleErr) + audio.removeEventListener('canplay', handlePlay) + } + const handleErr = () => { + clear() + if (audio?.error?.code !== 1) { + resolve(false) + } else { + resolve(true) + } + } + const handlePlay = () => { + clear() + resolve(true) + } + audio.addEventListener('error', handleErr) + audio.addEventListener('canplay', handlePlay) + audio.src = url + }) +} + +const preloadMusicInfo = { + isLoading: false, + preProgress: 0, + info: null as LX.Player.PlayMusicInfo | null, +} +const resetPreloadInfo = () => { + preloadMusicInfo.preProgress = 0 + preloadMusicInfo.info = null + preloadMusicInfo.isLoading = false +} +const preloadNextMusicUrl = async(curTime: number) => { + if (preloadMusicInfo.isLoading || curTime - preloadMusicInfo.preProgress < 3) return + preloadMusicInfo.isLoading = true + console.log('preload next music url') + const info = await getNextPlayMusicInfo() + if (info) { + preloadMusicInfo.info = info + const url = await getMusicUrl({ musicInfo: info.musicInfo }).catch(() => '') + if (url) { + console.log('preload url', url) + const result = await checkMusicUrl(url) + if (!result) { + const url = await getMusicUrl({ musicInfo: info.musicInfo, isRefresh: true }).catch(() => '') + void checkMusicUrl(url) + console.log('preload url refresh', url) + } + } + } + preloadMusicInfo.isLoading = false +} + +export default () => { + const setProgress = (time: number) => { + if (!musicInfo.id) return + preloadMusicInfo.preProgress = time + } + + const handleSetPlayInfo = () => { + resetPreloadInfo() + } + + watch(() => appSetting['player.togglePlayMethod'], () => { + if (!preloadMusicInfo.info || preloadMusicInfo.info.isTempPlay) return + resetRandomNextMusicInfo() + preloadMusicInfo.info = null + preloadMusicInfo.preProgress = playProgress.nowPlayTime + }) + + window.app_event.on('setProgress', setProgress) + window.app_event.on('musicToggled', handleSetPlayInfo) + + const rOnTimeupdate = onTimeupdate(() => { + const time = getCurrentTime() + const duration = playProgress.maxPlayTime + if (duration > 10 && duration - time < 10 && !preloadMusicInfo.info) { + void preloadNextMusicUrl(time) + } + }) + + + onBeforeUnmount(() => { + rOnTimeupdate() + window.app_event.off('setProgress', setProgress) + window.app_event.off('musicToggled', handleSetPlayInfo) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useSoundEffect.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useSoundEffect.ts new file mode 100644 index 000000000..efc028e66 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useSoundEffect.ts @@ -0,0 +1,162 @@ +import { watch } from '@common/utils/vueTools' +import { + freqs, + getAudioContext, + getBiquadFilter, + setConvolver, + setPannerSoundR, + setPannerSpeed, + startPanner, + stopPanner, + setConvolverMainGain, + setConvolverSendGain, + setPitchShifter, +} from '@renderer/plugins/player' + +import { appSetting } from '@renderer/store/setting' + +const cache = new Map() +const loadBuffer = async(name: string) => new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const path = require('@renderer/assets/medias/filters/' + name) as string + if (cache.has(path)) { + resolve(cache.get(path)!) + return + } + // Load buffer asynchronously + let request = new XMLHttpRequest() + request.open('GET', path, true) + request.responseType = 'arraybuffer' + + request.onload = function() { + // Asynchronously decode the audio file data in request.response + void getAudioContext().decodeAudioData(request.response, (buffer) => { + if (!buffer) { + reject(new Error('error decoding file data: ' + path)) + return + } + cache.set(path, buffer) + resolve(buffer) + }, + function(error) { + reject(error) + console.error('decodeAudioData error', error) + }) + } + + request.onerror = function() { + reject(new Error('XHR error')) + } + + request.send() +}) + +export default () => { + // console.log(appSetting['player.soundEffect.panner.enable']) + if (appSetting['player.soundEffect.panner.enable']) startPanner() + setPannerSoundR(appSetting['player.soundEffect.panner.soundR'] / 10) + setPannerSpeed(2 * (appSetting['player.soundEffect.panner.speed'] / 10)) + if (freqs.some(v => appSetting[`player.soundEffect.biquadFilter.hz${v}`] != 0)) { + const bfs = getBiquadFilter() + for (const item of freqs) { + bfs.get(`hz${item}`)!.gain.value = appSetting[`player.soundEffect.biquadFilter.hz${item}`] + } + } + if (appSetting['player.soundEffect.convolution.fileName']) { + void loadBuffer(appSetting['player.soundEffect.convolution.fileName']).then((buffer) => { + setConvolver(buffer, appSetting['player.soundEffect.convolution.mainGain'] / 10, appSetting['player.soundEffect.convolution.sendGain'] / 10) + }) + } + if (appSetting['player.soundEffect.pitchShifter.playbackRate'] != 1) { + setPitchShifter(appSetting['player.soundEffect.pitchShifter.playbackRate']) + } + + + watch(() => appSetting['player.soundEffect.panner.enable'], (enable) => { + if (enable) { + startPanner() + } else { + stopPanner() + } + }) + watch(() => appSetting['player.soundEffect.panner.soundR'], (soundR) => { + setPannerSoundR(soundR / 10) + }) + watch(() => appSetting['player.soundEffect.panner.speed'], (speed) => { + setPannerSpeed(2 * (speed / 10)) + }) + watch(() => appSetting['player.soundEffect.convolution.fileName'], (fileName) => { + setTimeout(() => { + if (fileName) { + void loadBuffer(fileName).then((buffer) => { + setConvolver(buffer, appSetting['player.soundEffect.convolution.mainGain'] / 10, appSetting['player.soundEffect.convolution.sendGain'] / 10) + }) + } else { + setConvolver(null, 0, 0) + } + }) + }) + watch(() => appSetting['player.soundEffect.convolution.mainGain'], (mainGain) => { + if (!appSetting['player.soundEffect.convolution.fileName']) return + setConvolverMainGain(mainGain / 10) + }) + watch(() => appSetting['player.soundEffect.convolution.sendGain'], (sendGain) => { + if (!appSetting['player.soundEffect.convolution.fileName']) return + setConvolverSendGain(sendGain / 10) + }) + watch(() => appSetting['player.soundEffect.biquadFilter.hz31'], (hz31) => { + const bfs = getBiquadFilter() + bfs.get('hz31')!.gain.value = hz31 + }) + watch(() => appSetting['player.soundEffect.biquadFilter.hz62'], (hz62) => { + const bfs = getBiquadFilter() + bfs.get('hz62')!.gain.value = hz62 + }) + watch(() => appSetting['player.soundEffect.biquadFilter.hz125'], (hz125) => { + const bfs = getBiquadFilter() + bfs.get('hz125')!.gain.value = hz125 + }) + watch(() => appSetting['player.soundEffect.biquadFilter.hz250'], (hz250) => { + const bfs = getBiquadFilter() + bfs.get('hz250')!.gain.value = hz250 + }) + watch(() => appSetting['player.soundEffect.biquadFilter.hz500'], (hz500) => { + const bfs = getBiquadFilter() + bfs.get('hz500')!.gain.value = hz500 + }) + watch(() => appSetting['player.soundEffect.biquadFilter.hz1000'], (hz1000) => { + const bfs = getBiquadFilter() + bfs.get('hz1000')!.gain.value = hz1000 + }) + watch(() => appSetting['player.soundEffect.biquadFilter.hz2000'], (hz2000) => { + const bfs = getBiquadFilter() + bfs.get('hz2000')!.gain.value = hz2000 + }) + watch(() => appSetting['player.soundEffect.biquadFilter.hz4000'], (hz4000) => { + const bfs = getBiquadFilter() + bfs.get('hz4000')!.gain.value = hz4000 + }) + watch(() => appSetting['player.soundEffect.biquadFilter.hz8000'], (hz8000) => { + const bfs = getBiquadFilter() + bfs.get('hz8000')!.gain.value = hz8000 + }) + watch(() => appSetting['player.soundEffect.biquadFilter.hz16000'], (hz16000) => { + const bfs = getBiquadFilter() + bfs.get('hz16000')!.gain.value = hz16000 + }) + + watch(() => appSetting['player.soundEffect.pitchShifter.playbackRate'], (playbackRate) => { + setPitchShifter(playbackRate) + }) + + + // window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp) + // window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown) + // window.app_event.on('setPlaybackRate', handleSetPlaybackRate) + + // onBeforeUnmount(() => { + // // window.key_event.off(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp) + // // window.key_event.off(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown) + // window.app_event.off('setPlaybackRate', handleSetPlaybackRate) + // }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useVolume.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useVolume.ts new file mode 100644 index 000000000..0eb49abb1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useVolume.ts @@ -0,0 +1,75 @@ +import { onBeforeUnmount, watch } from '@common/utils/vueTools' +import { setVolume as setPlayerVolume, setMute as setPlayerMute } from '@renderer/plugins/player' + +import { debounce } from '@common/utils' +import { HOTKEY_PLAYER } from '@common/hotKey' +// import { player as eventPlayerNames } from '@renderer/event/names' +import { volume, isMute, setMute, setVolume } from '@renderer/store/player/volume' +import { appSetting, saveVolume, saveVolumeIsMute } from '@renderer/store/setting' + +export default () => { + const handleSaveVolume = debounce(saveVolume, 300) + + setVolume(appSetting['player.volume']) + setMute(appSetting['player.isMute']) + setPlayerVolume(appSetting['player.volume']) + setPlayerMute(appSetting['player.isMute']) + + const handleToggleVolumeMute = (_isMute?: boolean) => { + let muteStatus = _isMute ?? !isMute.value + saveVolumeIsMute(muteStatus) + setMute(muteStatus) + } + + const handleSetVolume = (num: number) => { + const _volume = num < 0 ? 0 : num > 1 ? 1 : num + setVolume(_volume) + } + + const handleSetVolumeUp = (step = 0.04) => { + handleSetVolume(volume.value + step) + } + const handleSetVolumeDown = (step = 0.04) => { + handleSetVolume(volume.value - step) + } + + const hotkeyVolumeUp = () => { + handleSetVolumeUp() + } + const hotkeyVolumeDown = () => { + handleSetVolumeDown() + } + const hotkeyVolumeMute = () => { + handleToggleVolumeMute() + } + + watch(volume, _volume => { + handleSaveVolume(_volume) + setPlayerVolume(_volume) + }) + watch(isMute, mute => { + saveVolumeIsMute(mute) + setPlayerMute(mute) + }) + watch(() => appSetting['player.volume'], _volume => { + setVolume(_volume) + }) + watch(() => appSetting['player.isMute'], muteStatus => { + setMute(muteStatus) + }) + + + window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp) + window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown) + window.key_event.on(HOTKEY_PLAYER.volume_mute.action, hotkeyVolumeMute) + window.app_event.on('setVolume', handleSetVolume) + window.app_event.on('setVolumeIsMute', handleToggleVolumeMute) + + onBeforeUnmount(() => { + window.key_event.off(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp) + window.key_event.off(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown) + window.key_event.off(HOTKEY_PLAYER.volume_mute.action, hotkeyVolumeMute) + window.app_event.off('setVolume', handleSetVolume) + window.app_event.off('setVolumeIsMute', handleToggleVolumeMute) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useWatchList.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useWatchList.ts new file mode 100644 index 000000000..d6dc61605 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/usePlayer/useWatchList.ts @@ -0,0 +1,48 @@ +import { onBeforeUnmount } from '@common/utils/vueTools' + +import { playInfo, playMusicInfo } from '@renderer/store/player/state' +import { setPlayMusicInfo, updatePlayIndex } from '@renderer/store/player/action' +import { throttle } from '@common/utils' +import { playNext, stop } from '@renderer/core/player' + +const changedListIds = new Set() + +export default () => { + const throttleListChange = throttle(() => { + const isSkip = playMusicInfo.listId && !changedListIds.has(playInfo.playerListId) && !changedListIds.has(playMusicInfo.listId) + changedListIds.clear() + if (isSkip) return + + const { playIndex } = updatePlayIndex() + if (playIndex < 0) { // 歌曲被移除 + if (window.lx.isPlayedStop) { + stop() + setTimeout(() => { + setPlayMusicInfo(null, null) + }) + } else if (!playMusicInfo.isTempPlay) { + console.log('current music removed') + void playNext(true) + } + } + }) + + const handleListChange = (listIds: string[]) => { + for (const id of listIds) { + changedListIds.add(id) + } + throttleListChange() + } + + const handleDownloadListChange = () => { + handleListChange(['download']) + } + + window.app_event.on('myListUpdate', handleListChange) + window.app_event.on('downloadListUpdate', handleDownloadListChange) + + onBeforeUnmount(() => { + window.app_event.off('myListUpdate', handleListChange) + window.app_event.off('downloadListUpdate', handleDownloadListChange) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useSettingSync.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useSettingSync.ts new file mode 100644 index 000000000..665b97bad --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useSettingSync.ts @@ -0,0 +1,107 @@ +import { watch } from '@common/utils/vueTools' +import { isFullscreen, proxy, sync, windowSizeList } from '@renderer/store' +import { appSetting } from '@renderer/store/setting' +import { sendSyncAction, setWindowSize } from '@renderer/utils/ipc' +import { setLanguage } from '@root/lang' +import { setUserApi } from '../apiSource' +// import { applyTheme, getThemes } from '@renderer/store/utils' + + +export default () => { + watch(() => appSetting['common.windowSizeId'], (index) => { + const info = index == null ? windowSizeList[2] : windowSizeList[index] + setWindowSize(info.width, info.height) + }) + watch(() => appSetting['common.fontSize'], (fontSize) => { + if (isFullscreen.value) return + document.documentElement.style.fontSize = `${fontSize}px` + }) + + watch(() => appSetting['common.langId'], (id) => { + if (!id) return + setLanguage(id) + window.setLang(id) + }) + + watch(() => appSetting['common.apiSource'], apiSource => { + void setUserApi(apiSource) + }) + + watch(() => appSetting['common.font'], (val) => { + document.documentElement.style.fontFamily = val + }, { + immediate: true, + }) + + watch(() => appSetting['sync.mode'], (mode) => { + sync.mode = mode + }) + + watch(() => appSetting['sync.enable'], enable => { + switch (appSetting['sync.mode']) { + case 'server': + if (appSetting['sync.server.port']) { + void sendSyncAction({ + action: 'enable_server', + data: { + enable: appSetting['sync.enable'], + port: appSetting['sync.server.port'], + }, + }).catch(err => { + console.log(err) + }) + } + break + case 'client': + if (appSetting['sync.client.host']) { + void sendSyncAction({ + action: 'enable_client', + data: { + enable: appSetting['sync.enable'], + host: appSetting['sync.client.host'], + }, + }).catch(err => { + console.log(err) + }) + } + break + default: + break + } + sync.enable = enable + }) + watch(() => appSetting['sync.server.port'], port => { + if (appSetting['sync.mode'] == 'server') { + void sendSyncAction({ + action: 'enable_server', + data: { + enable: appSetting['sync.enable'], + port: appSetting['sync.server.port'], + }, + }) + } + sync.server.port = port + }) + watch(() => appSetting['sync.client.host'], host => { + if (appSetting['sync.mode'] == 'client') { + void sendSyncAction({ + action: 'enable_client', + data: { + enable: appSetting['sync.enable'], + host: appSetting['sync.client.host'], + }, + }) + } + sync.client.host = host + }) + + watch(() => appSetting['network.proxy.enable'], enable => { + proxy.enable = enable + }) + watch(() => appSetting['network.proxy.host'], host => { + proxy.host = host + }) + watch(() => appSetting['network.proxy.port'], port => { + proxy.port = port + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useStatusbarLyric.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useStatusbarLyric.ts new file mode 100644 index 000000000..c27bbb977 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useStatusbarLyric.ts @@ -0,0 +1,20 @@ +import { appSetting } from '@renderer/store/setting' +import { setDisableAutoPauseBySource } from '@renderer/core/lyric' + +export default () => { + const handleEnable = (enable: boolean) => { + setDisableAutoPauseBySource(enable, 'statusBarLyric') + } + + window.app_event.on('configUpdate', (setting) => { + if (setting['player.isShowStatusBarLyric'] != null) { + handleEnable(setting['player.isShowStatusBarLyric']) + } + }) + + return async() => { + if (appSetting['player.isShowStatusBarLyric']) { + handleEnable(true) + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useSync.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useSync.ts new file mode 100644 index 000000000..af9dcf2cb --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useSync.ts @@ -0,0 +1,83 @@ +import { markRaw, onBeforeUnmount } from '@common/utils/vueTools' +import { onSyncAction, sendSyncAction } from '@renderer/utils/ipc' +import { sync } from '@renderer/store' +import { appSetting } from '@renderer/store/setting' +import { SYNC_CODE } from '@common/constants_sync' + +export default () => { + const handleSyncList = (event: LX.Sync.SyncMainWindowActions) => { + // console.log(event) + switch (event.action) { + case 'select_mode': + sync.deviceName = event.data.deviceName + sync.type = event.data.type + sync.isShowSyncMode = true + break + case 'close_select_mode': + sync.isShowSyncMode = false + break + case 'server_status': + sync.server.status.status = event.data.status + sync.server.status.message = event.data.message + sync.server.status.address = markRaw(event.data.address) + sync.server.status.code = event.data.code + sync.server.status.devices = markRaw(event.data.devices) + break + case 'client_status': + sync.client.status.status = event.data.status + sync.client.status.message = event.data.message + sync.client.status.address = markRaw(event.data.address) + if (event.data.message == SYNC_CODE.missingAuthCode || event.data.message == SYNC_CODE.authFailed) { + if (!sync.isShowAuthCodeModal) sync.isShowAuthCodeModal = true + } else if (sync.isShowAuthCodeModal) sync.isShowAuthCodeModal = false + break + } + } + + const rSyncAction = onSyncAction(({ params }) => { + handleSyncList(params) + }) + + onBeforeUnmount(() => { + rSyncAction() + }) + + return async() => { + sync.enable = appSetting['sync.enable'] + sync.mode = appSetting['sync.mode'] + sync.server.port = appSetting['sync.server.port'] + sync.client.host = appSetting['sync.client.host'] + if (appSetting['sync.enable']) { + switch (appSetting['sync.mode']) { + case 'server': + if (appSetting['sync.server.port']) { + void sendSyncAction({ + action: 'enable_server', + data: { + enable: appSetting['sync.enable'], + port: appSetting['sync.server.port'], + }, + }).catch(err => { + console.log(err) + }) + } + break + case 'client': + if (appSetting['sync.client.host']) { + void sendSyncAction({ + action: 'enable_client', + data: { + enable: appSetting['sync.enable'], + host: appSetting['sync.client.host'], + }, + }).catch(err => { + console.log(err) + }) + } + break + default: + break + } + } + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useUpdate.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useUpdate.ts new file mode 100644 index 000000000..0d2be3cd5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/core/useApp/useUpdate.ts @@ -0,0 +1,208 @@ +import { nextTick, onBeforeUnmount, watch } from '@common/utils/vueTools' +import { + onUpdateAvailable, + onUpdateDownloaded, + onUpdateError, + onUpdateNotAvailable, + onUpdateProgress, + getIgnoreVersion, + getLastStartInfo, + saveLastStartInfo, +} from '@renderer/utils/ipc' +import { compareVer, isWin } from '@common/utils' +import { isShowChangeLog, versionInfo } from '@renderer/store' +import { getVersionInfo } from '@renderer/utils/update' +import { dialog } from '@renderer/plugins/Dialog' +import { appSetting } from '@renderer/store/setting' + +export default () => { + let isShowedChangeLog = false + + // 更新超时定时器 + let updateTimeout: number | null = null + const startUpdateTimeout = () => { + if (window.lx.isProd && !(isWin && process.arch.includes('arm'))) { + updateTimeout = window.setTimeout(() => { + updateTimeout = null + void nextTick(() => { + showUpdateModal() + setTimeout(() => { + void dialog({ + message: window.i18n.t('update__timeout_top'), + confirmButtonText: window.i18n.t('alert_button_text'), + }) + }, 500) + }) + }, 60 * 60 * 1000) + } + } + + const clearUpdateTimeout = () => { + if (!updateTimeout) return + clearTimeout(updateTimeout) + updateTimeout = null + } + + const handleShowChangeLog = () => { + isShowedChangeLog = true + void getLastStartInfo().then((version) => { + if (version == process.versions.app) return + saveLastStartInfo(process.versions.app) + if (!appSetting['common.showChangeLog']) return + if (version) { + if (compareVer(process.versions.app, version) < 0) { + void dialog({ + message: window.i18n.t('update__downgrade_tip', { ver: `${version} → ${process.versions.app}` }), + confirmButtonText: window.i18n.t('update__ignore_confirm_tip_confirm'), + }) + return + } + + if (compareVer(version, versionInfo.newVersion!.version) >= 0) return + } else if ( + // 如果当前版本不在已发布的版本中,则不需要显示更新日志 + ![{ version: versionInfo.newVersion!.version, desc: '' }, ...(versionInfo.newVersion!.history ?? [])] + .some(i => i.version == process.versions.app) + ) return + isShowChangeLog.value = true + }) + } + + const handleGetVersionInfo = async(): Promise> => { + return (versionInfo.newVersion?.history && !versionInfo.reCheck + ? Promise.resolve(versionInfo.newVersion) + : getVersionInfo().then((body: any) => { + versionInfo.newVersion = body + return body + }) + ).catch(() => { + if (versionInfo.newVersion) return versionInfo.newVersion + let result = { + version: '0.0.0', + desc: '', + } + versionInfo.newVersion = result + return result + }) + } + + let versionInfoPromise: null | ReturnType = null + + const showUpdateModal = (status?: LX.UpdateStatus) => { + if (versionInfoPromise) { + if ( + // @ts-expect-error + versionInfoPromise.resolved && + versionInfo.reCheck) { + versionInfoPromise = handleGetVersionInfo() + } + } else versionInfoPromise = handleGetVersionInfo() + // eslint-disable-next-line @typescript-eslint/promise-function-async + void versionInfoPromise.then((result) => { + versionInfo.reCheck = false + + if (result.version == '0.0.0') { + versionInfo.isUnknown = true + versionInfo.status = 'error' + let ignoreFailTipTime = parseInt(localStorage.getItem('update__check_failed_tip') ?? '0') + if (Date.now() - ignoreFailTipTime > 7 * 86400000) { + versionInfo.showModal = true + } + return + } + versionInfo.isUnknown = false + if (compareVer(versionInfo.version, result.version) != -1) { + versionInfo.status = 'idle' + versionInfo.isLatest = true + handleShowChangeLog() + return + } + + return getIgnoreVersion().then((ignoreVersion) => { + versionInfo.isLatest = false + let preStatus = versionInfo.status + if (status) versionInfo.status = status + if (result.version === ignoreVersion) return + void nextTick(() => { + versionInfo.showModal = true + if (status == 'error' && preStatus == 'downloading' && !localStorage.getItem('update__download_failed_tip')) { + setTimeout(() => { + void dialog({ + message: window.i18n.t('update__error_top'), + confirmButtonText: window.i18n.t('alert_button_text'), + }).finally(() => { + localStorage.setItem('update__download_failed_tip', '1') + }) + }, 500) + } + }) + }) + }).finally(() => { + // @ts-expect-error + versionInfoPromise!.resolved = true + }) + } + + const rUpdateAvailable = onUpdateAvailable(({ params: info }) => { + // versionInfo.isDownloading = true + // console.log(info) + versionInfo.newVersion = { + version: info.version, + desc: info.releaseNotes as string, + } + versionInfo.isLatest = false + if (appSetting['common.tryAutoUpdate']) { + versionInfo.status = 'downloading' + startUpdateTimeout() + } + void nextTick(() => { + showUpdateModal() + }) + }) + const rUpdateNotAvailable = onUpdateNotAvailable(({ params: info }) => { + clearUpdateTimeout() + // versionInfo.newVersion = { + // version: info.version, + // desc: info.releaseNotes as string, + // } + void handleGetVersionInfo().finally(() => { + versionInfo.isLatest = true + versionInfo.isUnknown = false + versionInfo.status = 'idle' + handleShowChangeLog() + }) + }) + const rUpdateError = onUpdateError((params) => { + clearUpdateTimeout() + // versionInfo.status = 'error' + void nextTick(() => { + showUpdateModal('error') + }) + }) + const rUpdateProgress = onUpdateProgress(({ params: progress }) => { + versionInfo.downloadProgress = progress + }) + const rUpdateDownloaded = onUpdateDownloaded(({ params: info }) => { + clearUpdateTimeout() + // versionInfo.status = 'downloaded' + void nextTick(() => { + showUpdateModal('downloaded') + }) + }) + + watch(() => versionInfo.showModal, (visible) => { + if (visible || isShowedChangeLog || versionInfo.status == 'downloaded') return + setTimeout(() => { + handleShowChangeLog() + }, 1000) + }) + + onBeforeUnmount(() => { + clearUpdateTimeout() + rUpdateAvailable() + rUpdateNotAvailable() + rUpdateError() + rUpdateProgress() + rUpdateDownloaded() + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/event/Event.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/event/Event.ts new file mode 100644 index 000000000..4552c34c5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/event/Event.ts @@ -0,0 +1,55 @@ +// import mitt from 'mitt' +// import type { Emitter } from 'mitt' + +export default class Event { + listeners: Map any>> + constructor() { + this.listeners = new Map() + } + + on(eventName: string, listener: (...args: any[]) => any) { + let targetListeners = this.listeners.get(eventName) + if (!targetListeners) this.listeners.set(eventName, targetListeners = []) + targetListeners.push(listener) + } + + off(eventName: string, listener: (...args: any[]) => any) { + let targetListeners = this.listeners.get(eventName) + if (!targetListeners) return + const index = targetListeners.indexOf(listener) + if (index < 0) return + targetListeners.splice(index, 1) + } + + emit(eventName: string, ...args: any[]) { + let targetListeners = this.listeners.get(eventName) + if (!targetListeners) return + for (const listener of targetListeners) { + listener(...args) + } + } + + offAll(eventName: string) { + let targetListeners = this.listeners.get(eventName) + if (!targetListeners) return + this.listeners.delete(eventName) + } +} + +// export class App_EVENT { +// listeners: Map void>> +// constructor() { +// this.listeners = new Map() +// } + +// on(eventName: string, listener: () => void) { +// let targetListeners = this.listeners.get(eventName) +// if (targetListeners) this.listeners.set(eventName, targetListeners = []) +// targetListeners!.push(listener) +// } + +// off(eventName: string, listener: () => void) { + +// } +// } + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/event/appEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/event/appEvent.ts new file mode 100644 index 000000000..c7a7e21c7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/event/appEvent.ts @@ -0,0 +1,188 @@ +import Event from './Event' + + +// { +// // sync: { +// // send_action_list: 'send_action_list', +// // handle_action_list: 'handle_action_list', +// // send_sync_list: 'send_sync_list', +// // handle_sync_list: 'handle_sync_list', +// // }, +// } + +export class AppEvent extends Event { + configUpdate(setting: Partial) { + this.emit('configUpdate', setting) + } + + focus() { + this.emit('focus') + } + + dragStart() { + this.emit('dragStart') + } + + dragEnd() { + this.emit('dragEnd') + } + + /** + * 音乐信息切换 + */ + musicToggled() { + this.emit('musicToggled') + } + + /** + * 手动改变进度 + * @param progress 进度 + */ + setProgress(progress: number, maxPlayTime?: number) { + this.emit('setProgress', progress, maxPlayTime) + } + + /** + * 设置音量大小 + * @param volume 音量大小 + */ + setVolume(volume: number) { + this.emit('setVolume', volume) + } + + /** + * 设置播放速率大小 + * @param rate 播放速率 + */ + setPlaybackRate(rate: number) { + this.emit('setPlaybackRate', rate) + } + + /** + * 设置是否静音 + * @param isMute 是否静音 + */ + setVolumeIsMute(isMute: boolean) { + this.emit('setVolumeIsMute', isMute) + } + + // 播放器事件 + play() { + this.emit('play') + } + + pause() { + this.emit('pause') + } + + stop() { + this.emit('stop') + } + + error(code?: number) { + this.emit('error', code) + } + + // 播放器原始事件 + playerPlaying() { + this.emit('playerPlaying') + } + + playerPause() { + this.emit('playerPause') + } + + playerStop() { + this.emit('playerStop') + } + + playerEnded() { + this.emit('playerEnded') + } + + playerError(code?: number) { + this.emit('playerError', code) + } + + playerLoadeddata() { + this.emit('playerLoadeddata') + } + + playerLoadstart() { + this.emit('playerLoadstart') + } + + playerCanplay() { + this.emit('playerCanplay') + } + + playerEmptied() { + this.emit('playerEmptied') + } + + playerWaiting() { + this.emit('playerWaiting') + } + + playerDeviceChanged() { + this.emit('playerDeviceChanged') + } + + // 激活进度条动画事件 + activePlayProgressTransition() { + this.emit('activePlayProgressTransition') + } + + // 更新图片事件 + picUpdated() { + this.emit('picUpdated') + } + + // 更新歌词事件 + lyricUpdated() { + this.emit('lyricUpdated') + } + + // 更新歌词偏移 + lyricOffsetUpdate() { + this.emit('lyricOffsetUpdate') + } + + // 歌词行播放 + lyricLinePlay(text: string, line: number) { + this.emit('lyricLinePlay', text, line) + } + + // 我的列表改变事件 + myListUpdate(ids: string[]) { + this.emit('myListUpdate', ids) + } + + // 下载列表改变事件 + downloadListUpdate() { + this.emit('downloadListUpdate') + } + + // 列表里的音乐信息改变事件 + // musicInfoUpdate(musicInfo: LX.Music.MusicInfo) { + // this.emit('musicInfoUpdate', musicInfo) + // } + + keyDown(event: LX.KeyDownEevent) { + this.emit('keyDown', event) + } +} + + +type EventMethods = Omit + + +declare class EventType extends AppEvent { + on(event: K, listener: EventMethods[K]): any + off(event: K, listener: EventMethods[K]): any +} + +export type AppEventTypes = Omit> +export const createAppEventHub = (): AppEventTypes => { + return new AppEvent() +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/event/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/event/index.ts new file mode 100644 index 000000000..472ccb820 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/event/index.ts @@ -0,0 +1,47 @@ +import { getHotKeyConfig, onFocus, onKeyDown, onUpdateHotkey } from '@renderer/utils/ipc' +import { registerKeyEvent, createKeyEventHub } from './keyEvent' +// import { registerRendererEvents, unregisterRendererEvents } from './rendererEvent' +import { createAppEventHub } from './appEvent' + +export const registerEvents = () => { + window.lx.isEditingHotKey = false + window.app_event = createAppEventHub() + window.key_event = createKeyEventHub() + + const setHotkeyConfig = ({ local, global }: LX.HotKeyConfigAll) => { + window.lx.appHotKeyConfig = { + local, + global, + } + } + + void getHotKeyConfig().then(setHotkeyConfig) + + onUpdateHotkey(({ params }) => { + setHotkeyConfig(params) + }) + + onKeyDown(({ params: { key } }) => { + const keyInfo = window.lx.appHotKeyConfig.global.keys[key] + if (keyInfo) window.key_event.emit(keyInfo.action) + }) + + onFocus(() => { + window.app_event.focus() + }) + + registerKeyEvent() + // registerRendererEvents() +} + +// export const unregisterEvents = () => { +// unregisterKeyEvent() +// // unregisterRendererEvents() +// } + +export { clearDownKeys } from './keyEvent' + +export type { AppEventTypes } from './appEvent' +export type { KeyEventTypes } from './keyEvent' + +registerEvents() diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/event/keyEvent.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/event/keyEvent.ts new file mode 100644 index 000000000..48779f24d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/event/keyEvent.ts @@ -0,0 +1,54 @@ +import keyBind from '../utils/keyBind' +import { HOTKEY_COMMON } from '@common/hotKey' +import Event from './Event' +import { appSetting } from '@renderer/store/setting' + +declare class keyEventTypes extends Event { + on(event: string, listener: (event: LX.KeyDownEevent) => any): void + off(event: string, listener: (event: LX.KeyDownEevent) => any): void +} + +export type KeyEventTypes = keyEventTypes + +export const createKeyEventHub = (): keyEventTypes => { + return new Event() +} + +window.lx.isEditingHotKey = false +// let appHotKeyConfig: LX.HotKeyConfigAll = window.lx.appHotKeyConfig + +export const registerKeyEvent = () => { + keyBind.bindKey((key, eventKey, type, event, keys, isEditing) => { + // console.log(`key_${key}_${type}`) + window.app_event.keyDown({ event, keys, key, eventKey, type }) + // console.log(event, key) + // console.log(key, eventKey, type, event, keys) + if (window.lx.isEditingHotKey || (isEditing && type == 'down') || event?.lx_handled) return + if (event && window.lx.appHotKeyConfig.local.enable && window.lx.appHotKeyConfig.local.keys[key] && (key != 'escape' || !((event.target as HTMLElement).classList.contains('ignore-esc')))) { + // console.log(key, eventKey, type, keys, isEditing) + event.preventDefault() + if (type == 'up') return + + // 软件内快捷键的最小化触发时 + // 如果已启用托盘,则隐藏程序,否则最小化程序 https://github.com/lyswhut/lx-music-desktop/issues/603 + if (window.lx.appHotKeyConfig.local.keys[key].action == HOTKEY_COMMON.min.action && appSetting['tray.enable']) { + window.key_event.emit(HOTKEY_COMMON.hide_toggle.action) + return + } + + window.key_event.emit(window.lx.appHotKeyConfig.local.keys[key].action) + return + } + // console.log(`key_${key}_${type}`) + window.key_event.emit(`key_${key}_${type}`, { event, keys, key, eventKey, type }) + if (key != eventKey) window.key_event.emit(`key_${eventKey}_${type}`, { event, keys, key, eventKey, type }) + }) +} + +export const unregisterKeyEvent = () => { + keyBind.unbindKey() +} + +export const clearDownKeys = () => { + keyBind.clearDownKeys() +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/index.html b/plugins/lx-music-ztools/vendor/lx/src/renderer/index.html new file mode 100644 index 000000000..ea0b85e1a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/index.html @@ -0,0 +1,49 @@ + + + + + + LX Music + + + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/main.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/main.ts new file mode 100644 index 000000000..dccaa19e1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/main.ts @@ -0,0 +1,82 @@ +import '@common/error' +import { createApp } from 'vue' + +import './core/globalData' + +import '@renderer/event' + +// Components +import mountComponents from './components' + +// Plugins +import initPlugins from './plugins' +import { i18nPlugin } from './plugins/i18n' + +import App from './App.vue' +import router from './router' +// import store from './store' + + +import { getSetting, updateSetting } from './utils/ipc' +import { langList } from '@root/lang' +import type { I18n } from '@root/lang/i18n' + +import { initSetting } from './store/setting' +// import { bubbleCursor } from './utils/cursor-effects/bubbleCursor' + +import './worker' +import { saveViewPrevState } from './utils/data' + +// sync(store, router) + +router.afterEach((to) => { + if (to.path != '/songList/detail') { + saveViewPrevState({ + url: to.path, + query: { ...to.query }, + }) + } +}) + +void getSetting().then(setting => { + // window.lx.appSetting = setting + // Set language automatically + if (!setting['common.langId'] || !window.i18n.availableLocales.includes(setting['common.langId'])) { + let langId: I18n['locale'] | null = null + const locale = window.navigator.language.toLocaleLowerCase() as I18n['locale'] + if (window.i18n.availableLocales.includes(locale)) { + langId = locale + } else { + for (const lang of langList) { + if (lang.alternate == locale) { + langId = lang.locale + break + } + } + langId ??= 'en-us' + } + setting['common.langId'] = langId + void updateSetting({ 'common.langId': langId }) + console.log('Set lang', setting['common.langId']) + } + window.setLang(setting['common.langId']) + window.i18n.setLanguage(setting['common.langId']) + + if (!setting['common.startInFullscreen'] && (document.body.clientHeight > window.screen.availHeight || document.body.clientWidth > window.screen.availWidth) && setting['common.windowSizeId'] > 1) { + void updateSetting({ 'common.windowSizeId': 1 }) + } + + // store.commit('setSetting', setting) + initSetting(setting) + + const app = createApp(App) + app + .use(router) + // .use(store) + .use(i18nPlugin) + initPlugins(app) + mountComponents(app) + app.mount('#root') +}) + +// bubbleCursor() diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Dialog/Dialog.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Dialog/Dialog.vue new file mode 100644 index 000000000..7234fc70f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Dialog/Dialog.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Dialog/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Dialog/index.js new file mode 100644 index 000000000..d1a54300d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Dialog/index.js @@ -0,0 +1,63 @@ +import Dialog from './Dialog.vue' +import { createApp } from 'vue' + + +const defaultOptions = { + message: '', + teleport: '#root', + showCancel: false, + cancelButtonText: '', + confirmButtonText: '', + selection: false, +} + +export const dialog = function(options) { + const { message, showCancel, cancelButtonText, confirmButtonText, teleport, selection } = + Object.assign({}, defaultOptions, typeof options == 'string' ? { message: options } : options || {}) + return new Promise((resolve, reject) => { + let app = createApp(Dialog, { + afterLeave() { + app?.unmount() + app = null + }, + }) + + let instance = app.mount(document.createElement('div')) + + // 属性设置 + instance.visible = true + instance.message = message + instance.showCancel = showCancel + instance.cancelButtonText = cancelButtonText + instance.confirmButtonText = confirmButtonText + instance.teleport = teleport + instance.selection = selection + + // 挂载 + document.getElementById('container').appendChild(instance.$el) + + instance.handleCancel = () => { + instance.visible = false + resolve(false) + } + + instance.handleComfirm = () => { + instance.visible = false + resolve(true) + } + }) +} + +dialog.confirm = options => dialog( + typeof options == 'string' + ? { message: options, showCancel: true } + : { ...options, showCancel: true }, +) + +const dialogPlugin = { + install(Vue, options) { + Vue.config.globalProperties.$dialog = dialog + }, +} + +export default dialogPlugin diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/SvgIcon/SvgIcon.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/SvgIcon/SvgIcon.vue new file mode 100644 index 000000000..4f4f7c3ea --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/SvgIcon/SvgIcon.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/SvgIcon/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/SvgIcon/index.js new file mode 100644 index 000000000..b77cb733f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/SvgIcon/index.js @@ -0,0 +1,9 @@ +import SvgIcon from './SvgIcon.vue' + +const req = require.context('@renderer/assets/svgs', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys().map(requireContext) +requireAll(req) + +export default app => { + app.component('svg-icon', SvgIcon) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Tips/Tips.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Tips/Tips.js new file mode 100644 index 000000000..e19cf3186 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Tips/Tips.js @@ -0,0 +1,54 @@ +import Tips from './Tips.vue' +import { createApp } from 'vue' + +const addAutoCloseTimer = (instance, time) => { + if (!time) return + if (instance.autoCloseTimer) clearTimeout(instance.autoCloseTimer) + instance.autoCloseTimer = setTimeout(() => { + instance.cancel() + }, time) +} +const clearAutoCloseTimer = instance => { + if (!instance.autoCloseTimer) return + clearTimeout(instance.autoCloseTimer) + instance.autoCloseTimer = null +} + +export default ({ position, message, autoCloseTime } = {}, props) => { + if (!position) return + let app = createApp(Tips, { + afterLeave() { + app.unmount() + app = null + }, + }) + + let instance = app.mount(document.createElement('div')) + + // Tips实例挂载到刚创建的div + // 属性设置 + instance.visible = true + instance.message = message + instance.position.top = position.top + instance.position.left = position.left + + // 将Tips的DOM挂载到body上 + document.body.appendChild(instance.$el) + + instance.cancel = () => { + props.beforeClose(instance) + clearAutoCloseTimer(instance) + instance.visible = false + instance = null + } + + instance.setTips = tips => { + addAutoCloseTimer(instance, autoCloseTime) + instance.message = tips + } + + addAutoCloseTimer(instance, autoCloseTime) + + return instance +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Tips/Tips.vue b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Tips/Tips.vue new file mode 100644 index 000000000..4e1fcfa5c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Tips/Tips.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Tips/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Tips/index.js new file mode 100644 index 000000000..cf4708e1a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/Tips/index.js @@ -0,0 +1,88 @@ +import tips from './Tips' +import { debounce } from '@common/utils' + +let instance +let prevTips +let prevX = 0 +let prevY = 0 +let isDraging = false + +const getTipText = el => { + return el.getAttribute('aria-label') && el.getAttribute('ignore-tip') == null ? el.getAttribute('aria-label') : null +} + +const getTips = el => + el + ? getTipText(el) + ? getTipText(el) + : el.parentNode === document.documentElement + ? null + : getTips(el.parentNode) + : null + +const showTips = debounce(event => { + if (isDraging) return + let msg = getTips(event.target)?.trim() + if (!msg) return + prevTips = msg + instance = tips({ + message: msg, + autoCloseTime: 10000, + position: { + top: event.y + 12, + left: event.x + 8, + }, + }, { + beforeClose(closeInstance) { + if (instance !== closeInstance) return + prevTips = null + instance = null + }, + }) +}, 400) + +const hideTips = () => { + if (!instance) return + instance.cancel() +} + +const setTips = tips => { + if (!instance) return + instance.setTips(tips) +} + +const updateTips = event => { + if (isDraging) return + if (!instance) return showTips(event) + setTimeout(() => { + let msg = getTips(event.target) + if (!msg || prevTips === msg) return + setTips(msg) + prevTips = msg + }) +} + +setTimeout(() => { + document.body.addEventListener('mousemove', event => { + if ((event.x == prevX && event.y == prevY) || isDraging) return + prevX = event.x + prevY = event.y + hideTips() + showTips(event) + }) + + document.body.addEventListener('click', updateTips) + + document.body.addEventListener('contextmenu', updateTips) + + window.app_event.on('focus', () => { + hideTips() + }) + window.app_event.on('dragStart', () => { + isDraging = true + hideTips() + }) + window.app_event.on('dragEnd', () => { + isDraging = false + }) +}) diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/i18n.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/i18n.ts new file mode 100644 index 000000000..5ab892196 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/i18n.ts @@ -0,0 +1,11 @@ +import type { I18n } from '@root/lang' +import { createI18n, i18nPlugin, useI18n } from '@root/lang' + +window.i18n = createI18n() + +export { + i18nPlugin, + useI18n, +} + +export type { I18n } diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/index.ts new file mode 100644 index 000000000..8eab5ec7e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/index.ts @@ -0,0 +1,11 @@ +// import './axios' +import { type App } from 'vue' +import dialog from './Dialog' +import './Tips' +import svgIcon from './SvgIcon' + +export default (app: App) => { + app.use(dialog) + + svgIcon(app) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/index.ts new file mode 100644 index 000000000..058999cd3 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/index.ts @@ -0,0 +1,574 @@ +interface HTMLAudioElementChrome extends HTMLAudioElement { + setSinkId: (id: string) => Promise +} +let audio: HTMLAudioElementChrome | null = null +let audioContext: AudioContext +let mediaSource: MediaElementAudioSourceNode +let analyser: AnalyserNode +// https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext +// https://benzleung.gitbooks.io/web-audio-api-mini-guide/content/chapter5-1.html +export const freqs = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000] as const +type Freqs = (typeof freqs)[number] +let biquads: Map<`hz${Freqs}`, BiquadFilterNode> +export const freqsPreset = [ + { name: 'pop', hz31: 6, hz62: 5, hz125: -3, hz250: -2, hz500: 5, hz1000: 4, hz2000: -4, hz4000: -3, hz8000: 6, hz16000: 4 }, + { name: 'dance', hz31: 4, hz62: 3, hz125: -4, hz250: -6, hz500: 0, hz1000: 0, hz2000: 3, hz4000: 4, hz8000: 4, hz16000: 5 }, + { name: 'rock', hz31: 7, hz62: 6, hz125: 2, hz250: 1, hz500: -3, hz1000: -4, hz2000: 2, hz4000: 1, hz8000: 4, hz16000: 5 }, + { name: 'classical', hz31: 6, hz62: 7, hz125: 1, hz250: 2, hz500: -1, hz1000: 1, hz2000: -4, hz4000: -6, hz8000: -7, hz16000: -8 }, + { name: 'vocal', hz31: -5, hz62: -6, hz125: -4, hz250: -3, hz500: 3, hz1000: 4, hz2000: 5, hz4000: 4, hz8000: -3, hz16000: -3 }, + { name: 'slow', hz31: 5, hz62: 4, hz125: 2, hz250: 0, hz500: -2, hz1000: 0, hz2000: 3, hz4000: 6, hz8000: 7, hz16000: 8 }, + { name: 'electronic', hz31: 6, hz62: 5, hz125: 0, hz250: -5, hz500: -4, hz1000: 0, hz2000: 6, hz4000: 8, hz8000: 8, hz16000: 7 }, + { name: 'subwoofer', hz31: 8, hz62: 7, hz125: 5, hz250: 4, hz500: 0, hz1000: 0, hz2000: 0, hz4000: 0, hz8000: 0, hz16000: 0 }, + { name: 'soft', hz31: -5, hz62: -5, hz125: -4, hz250: -4, hz500: 3, hz1000: 2, hz2000: 4, hz4000: 4, hz8000: 0, hz16000: 0 }, +] as const +export const convolutions = [ + { name: 'telephone', mainGain: 0.0, sendGain: 3.0, source: 'filter-telephone.wav' }, // 电话 + { name: 's2_r4_bd', mainGain: 1.8, sendGain: 0.9, source: 's2_r4_bd.wav' }, // 教堂 + { name: 'bright_hall', mainGain: 0.8, sendGain: 2.4, source: 'bright-hall.wav' }, + { name: 'cinema_diningroom', mainGain: 0.6, sendGain: 2.3, source: 'cinema-diningroom.wav' }, + { name: 'dining_living_true_stereo', mainGain: 0.6, sendGain: 1.8, source: 'dining-living-true-stereo.wav' }, + { name: 'living_bedroom_leveled', mainGain: 0.6, sendGain: 2.1, source: 'living-bedroom-leveled.wav' }, + { name: 'spreader50_65ms', mainGain: 1, sendGain: 2.5, source: 'spreader50-65ms.wav' }, + // { name: 'spreader25_125ms', mainGain: 1, sendGain: 2.5, source: 'spreader25-125ms.wav' }, + // { name: 'backslap', mainGain: 1.8, sendGain: 0.8, source: 'backslap1.wav' }, + { name: 's3_r1_bd', mainGain: 1.8, sendGain: 0.8, source: 's3_r1_bd.wav' }, + { name: 'matrix_1', mainGain: 1.5, sendGain: 0.9, source: 'matrix-reverb1.wav' }, + { name: 'matrix_2', mainGain: 1.3, sendGain: 1, source: 'matrix-reverb2.wav' }, + { name: 'cardiod_35_10_spread', mainGain: 1.8, sendGain: 0.6, source: 'cardiod-35-10-spread.wav' }, + { name: 'tim_omni_35_10_magnetic', mainGain: 1, sendGain: 0.2, source: 'tim-omni-35-10-magnetic.wav' }, + // { name: 'spatialized', mainGain: 1.8, sendGain: 0.8, source: 'spatialized8.wav' }, + // { name: 'zing_long_stereo', mainGain: 0.8, sendGain: 1.8, source: 'zing-long-stereo.wav' }, + { name: 'feedback_spring', mainGain: 1.8, sendGain: 0.8, source: 'feedback-spring.wav' }, + // { name: 'tim_omni_rear_blend', mainGain: 1.8, sendGain: 0.8, source: 'tim-omni-rear-blend.wav' }, +] as const +// 半音 +// export const semitones = [-1.5, -1, -0.5, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] as const + +let convolver: ConvolverNode +let convolverSourceGainNode: GainNode +let convolverOutputGainNode: GainNode +let convolverDynamicsCompressor: DynamicsCompressorNode +let gainNode: GainNode +let panner: PannerNode +let pitchShifterNode: AudioWorkletNode +let pitchShifterNodePitchFactor: AudioParam +let pitchShifterNodeLoadStatus: 'none' | 'loading' | 'unconnect' | 'connected' = 'none' +let pitchShifterNodeTempValue = 1 +let defaultChannelCount = 2 +export const soundR = 0.5 + + +export const createAudio = () => { + if (audio) return + audio = new window.Audio() as HTMLAudioElementChrome + audio.controls = false + audio.autoplay = true + audio.preload = 'auto' + audio.crossOrigin = 'anonymous' + + // https://developer.chrome.com/blog/autoplay + audio.addEventListener('playing', () => { + if (audioContext?.state == 'suspended') { + void audioContext.resume().catch((err) => { + console.error('Resume audio context failed:', err) + throw err + }) + } + }) +} + +const initAnalyser = () => { + analyser = audioContext.createAnalyser() + analyser.fftSize = 256 +} + +const initBiquadFilter = () => { + biquads = new Map() + let i + + for (const item of freqs) { + const filter = audioContext.createBiquadFilter() + biquads.set(`hz${item}`, filter) + filter.type = 'peaking' + filter.frequency.value = item + filter.Q.value = 1.4 + filter.gain.value = 0 + } + + for (i = 1; i < freqs.length; i++) { + (biquads.get(`hz${freqs[i - 1]}`)!).connect(biquads.get(`hz${freqs[i]}`)!) + } +} + +const initConvolver = () => { + convolverSourceGainNode = audioContext.createGain() + convolverOutputGainNode = audioContext.createGain() + convolverDynamicsCompressor = audioContext.createDynamicsCompressor() + convolver = audioContext.createConvolver() + convolver.connect(convolverOutputGainNode) + convolverSourceGainNode.connect(convolverDynamicsCompressor) + convolverOutputGainNode.connect(convolverDynamicsCompressor) +} + +const initPanner = () => { + panner = audioContext.createPanner() +} + +const initGain = () => { + gainNode = audioContext.createGain() +} + +const initAdvancedAudioFeatures = () => { + if (audioContext) return + if (!audio) throw new Error('audio not defined') + audioContext = new window.AudioContext({ latencyHint: 'playback' }) + defaultChannelCount = audioContext.destination.channelCount + + initAnalyser() + initBiquadFilter() + initConvolver() + initPanner() + initGain() + // source -> analyser -> biquadFilter -> pitchShifter -> [(convolver & convolverSource)->convolverDynamicsCompressor] -> panner -> gain + mediaSource = audioContext.createMediaElementSource(audio) + mediaSource.connect(analyser) + analyser.connect(biquads.get(`hz${freqs[0]}`)!) + const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1)!}`)!) + lastBiquadFilter.connect(convolverSourceGainNode) + lastBiquadFilter.connect(convolver) + convolverDynamicsCompressor.connect(panner) + panner.connect(gainNode) + gainNode.connect(audioContext.destination) + + // 音频输出设备改变时刷新 audio node 连接 + window.app_event.on('playerDeviceChanged', handleMediaListChange) + + // audio.addEventListener('playing', connectAudioNode) + // audio.addEventListener('pause', disconnectAudioNode) + // audio.addEventListener('waiting', disconnectAudioNode) + // audio.addEventListener('emptied', disconnectAudioNode) + // if (!audio.paused) connectAudioNode() +} + +const handleMediaListChange = () => { + mediaSource.disconnect() + mediaSource.connect(analyser) +} + +// let isConnected = true +// const connectAudioNode = () => { +// if (isConnected) return +// console.log('connect Node') +// mediaSource.connect(analyser) +// isConnected = true +// if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') { +// disconnectPitchShifterNode() +// } +// } + +// const disconnectAudioNode = () => { +// if (!isConnected) return +// console.log('disconnect Node') +// mediaSource.disconnect() +// isConnected = false +// if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') { +// disconnectPitchShifterNode() +// } +// } + +export const getAudioContext = () => { + initAdvancedAudioFeatures() + return audioContext +} + +let unsubMediaListChangeEvent: (() => void) | null = null +export const setMaxOutputChannelCount = (enable: boolean) => { + if (enable) { + initAdvancedAudioFeatures() + audioContext.destination.channelCountMode = 'max' + audioContext.destination.channelCount = audioContext.destination.maxChannelCount + // navigator.mediaDevices.addEventListener('devicechange', handleMediaListChange) + if (!unsubMediaListChangeEvent) { + let handleMediaListChange = () => { + setMaxOutputChannelCount(true) + } + window.app_event.on('playerDeviceChanged', handleMediaListChange) + unsubMediaListChangeEvent = () => { + window.app_event.off('playerDeviceChanged', handleMediaListChange) + unsubMediaListChangeEvent = null + } + } + } else { + unsubMediaListChangeEvent?.() + if (audioContext && audioContext.destination.channelCountMode != 'explicit') { + audioContext.destination.channelCount = defaultChannelCount + // audioContext.destination.channelInterpretation + audioContext.destination.channelCountMode = 'explicit' + } + } +} + +export const getAnalyser = (): AnalyserNode | null => { + initAdvancedAudioFeatures() + return analyser +} + +export const getBiquadFilter = () => { + initAdvancedAudioFeatures() + return biquads +} + +// let isConvolverConnected = false +export const setConvolver = (buffer: AudioBuffer | null, mainGain: number, sendGain: number) => { + initAdvancedAudioFeatures() + convolver.buffer = buffer + // console.log(mainGain, sendGain) + if (buffer) { + convolverSourceGainNode.gain.value = mainGain + convolverOutputGainNode.gain.value = sendGain + } else { + convolverSourceGainNode.gain.value = 1 + convolverOutputGainNode.gain.value = 0 + } +} + +export const setConvolverMainGain = (gain: number) => { + if (convolverSourceGainNode.gain.value == gain) return + // console.log(gain) + convolverSourceGainNode.gain.value = gain +} + +export const setConvolverSendGain = (gain: number) => { + if (convolverOutputGainNode.gain.value == gain) return + // console.log(gain) + convolverOutputGainNode.gain.value = gain +} + +let pannerInfo = { + x: 0, + y: 0, + z: 0, + soundR: 0.5, + rad: 0, + speed: 1, + intv: null as NodeJS.Timeout | null, +} +const setPannerXYZ = (nx: number, ny: number, nz: number) => { + pannerInfo.x = nx + pannerInfo.y = ny + pannerInfo.z = nz + // console.log(pannerInfo) + panner.positionX.value = nx * pannerInfo.soundR + panner.positionY.value = ny * pannerInfo.soundR + panner.positionZ.value = nz * pannerInfo.soundR +} +export const setPannerSoundR = (r: number) => { + pannerInfo.soundR = r +} + +export const setPannerSpeed = (speed: number) => { + pannerInfo.speed = speed + if (pannerInfo.intv) startPanner() +} +export const stopPanner = () => { + if (pannerInfo.intv) { + clearInterval(pannerInfo.intv) + pannerInfo.intv = null + pannerInfo.rad = 0 + } + panner.positionX.value = 0 + panner.positionY.value = 0 + panner.positionZ.value = 0 +} + +export const startPanner = () => { + initAdvancedAudioFeatures() + if (pannerInfo.intv) { + clearInterval(pannerInfo.intv) + pannerInfo.intv = null + pannerInfo.rad = 0 + } + pannerInfo.intv = setInterval(() => { + pannerInfo.rad += 1 + if (pannerInfo.rad > 360) pannerInfo.rad -= 360 + setPannerXYZ(Math.sin(pannerInfo.rad * Math.PI / 180), Math.cos(pannerInfo.rad * Math.PI / 180), Math.cos(pannerInfo.rad * Math.PI / 180)) + }, pannerInfo.speed * 10) +} + +let isConnected = true +const connectNode = () => { + if (isConnected) return + console.log('connect Node') + analyser?.connect(biquads.get(`hz${freqs[0]}`)!) + isConnected = true + if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') { + disconnectPitchShifterNode() + } +} +const disconnectNode = () => { + if (!isConnected) return + console.log('disconnect Node') + analyser?.disconnect() + isConnected = false + if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') { + disconnectPitchShifterNode() + } +} +const connectPitchShifterNode = () => { + console.log('connect Pitch Shifter Node') + audio!.addEventListener('playing', connectNode) + audio!.addEventListener('pause', disconnectNode) + audio!.addEventListener('waiting', disconnectNode) + audio!.addEventListener('emptied', disconnectNode) + if (audio!.paused) disconnectNode() + + const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1)!}`)!) + lastBiquadFilter.disconnect() + lastBiquadFilter.connect(pitchShifterNode) + + pitchShifterNode.connect(convolver) + pitchShifterNode.connect(convolverSourceGainNode) + // convolverDynamicsCompressor.disconnect(panner) + // convolverDynamicsCompressor.connect(pitchShifterNode) + // pitchShifterNode.connect(panner) + pitchShifterNodeLoadStatus = 'connected' + pitchShifterNodePitchFactor.value = pitchShifterNodeTempValue +} +const disconnectPitchShifterNode = () => { + console.log('disconnect Pitch Shifter Node') + const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1)!}`)!) + lastBiquadFilter.disconnect() + lastBiquadFilter.connect(convolver) + lastBiquadFilter.connect(convolverSourceGainNode) + pitchShifterNodeLoadStatus = 'unconnect' + + audio!.removeEventListener('playing', connectNode) + audio!.removeEventListener('pause', disconnectNode) + audio!.removeEventListener('waiting', disconnectNode) + audio!.removeEventListener('emptied', disconnectNode) + connectNode() +} +const loadPitchShifterNode = () => { + pitchShifterNodeLoadStatus = 'loading' + initAdvancedAudioFeatures() + // source -> analyser -> biquadFilter -> audioWorklet(pitch shifter) -> [(convolver & convolverSource)->convolverDynamicsCompressor] -> panner -> gain + void audioContext.audioWorklet.addModule(new URL( + /* webpackChunkName: 'pitch_shifter.audioWorklet' */ + './pitch-shifter/phase-vocoder.js', + import.meta.url, + )).then(() => { + console.log('pitch shifter audio worklet loaded') + // https://github.com/olvb/phaze/issues/26#issuecomment-1574629971 + pitchShifterNode = new AudioWorkletNode(audioContext, 'phase-vocoder-processor', { outputChannelCount: [2] }) + let pitchFactorParam = pitchShifterNode.parameters.get('pitchFactor') + if (!pitchFactorParam) return + pitchShifterNodePitchFactor = pitchFactorParam + pitchShifterNodeLoadStatus = 'unconnect' + if (pitchShifterNodeTempValue == 1) return + + connectPitchShifterNode() + }) +} + +export const setPitchShifter = (val: number) => { + // console.log('setPitchShifter', val) + pitchShifterNodeTempValue = val + switch (pitchShifterNodeLoadStatus) { + case 'loading': + break + case 'none': + loadPitchShifterNode() + break + case 'connected': + // a: 1 = 半音 + // value = 2 ** (a / 12) + pitchShifterNodePitchFactor.value = val + break + case 'unconnect': + connectPitchShifterNode() + break + } +} + +export const hasInitedAdvancedAudioFeatures = (): boolean => audioContext != null + +export const setResource = (src: string) => { + if (audio) audio.src = src +} + +export const setPlay = () => { + void audio?.play() +} + +export const setPause = () => { + audio?.pause() +} + +export const setStop = () => { + if (audio) { + audio.src = '' + audio.removeAttribute('src') + } +} + +export const isEmpty = (): boolean => !audio?.src + +export const setLoopPlay = (isLoop: boolean) => { + if (audio) audio.loop = isLoop +} + +export const getPlaybackRate = (): number => { + return audio?.defaultPlaybackRate ?? 1 +} + +export const setPlaybackRate = (rate: number) => { + if (!audio) return + audio.defaultPlaybackRate = rate + audio.playbackRate = rate +} + +export const setPreservesPitch = (preservesPitch: boolean) => { + if (!audio) return + audio.preservesPitch = preservesPitch +} + +export const getMute = (): boolean => { + return audio?.muted ?? false +} + +export const setMute = (isMute: boolean) => { + if (audio) audio.muted = isMute +} + +export const getCurrentTime = () => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return audio?.currentTime || 0 +} + +export const setCurrentTime = (time: number) => { + if (audio) audio.currentTime = time +} + +export const setMediaDeviceId = async(mediaDeviceId: string): Promise => { + if (!audio) return + return audio.setSinkId(mediaDeviceId) +} + +export const setVolume = (volume: number) => { + if (audio) audio.volume = volume +} + +export const getDuration = () => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return audio?.duration || 0 +} + +// export const getPlaybackRate = () => { +// return audio?.playbackRate ?? 1 +// } + +type Noop = () => void + +export const onPlaying = (callback: Noop) => { + if (!audio) throw new Error('audio not defined') + + audio.addEventListener('playing', callback) + return () => { + audio?.removeEventListener('playing', callback) + } +} + +export const onPause = (callback: Noop) => { + if (!audio) throw new Error('audio not defined') + + audio?.addEventListener('pause', callback) + return () => { + audio?.removeEventListener('pause', callback) + } +} + +export const onEnded = (callback: Noop) => { + if (!audio) throw new Error('audio not defined') + + audio.addEventListener('ended', callback) + return () => { + audio?.removeEventListener('ended', callback) + } +} + +export const onError = (callback: Noop) => { + if (!audio) throw new Error('audio not defined') + + audio.addEventListener('error', callback) + return () => { + audio?.removeEventListener('error', callback) + } +} + +export const onLoadeddata = (callback: Noop) => { + if (!audio) throw new Error('audio not defined') + + audio.addEventListener('loadeddata', callback) + return () => { + audio?.removeEventListener('loadeddata', callback) + } +} + +export const onLoadstart = (callback: Noop) => { + if (!audio) throw new Error('audio not defined') + + audio.addEventListener('loadstart', callback) + return () => { + audio?.removeEventListener('loadstart', callback) + } +} + +export const onCanplay = (callback: Noop) => { + if (!audio) throw new Error('audio not defined') + + audio.addEventListener('canplay', callback) + return () => { + audio?.removeEventListener('canplay', callback) + } +} + +export const onEmptied = (callback: Noop) => { + if (!audio) throw new Error('audio not defined') + + audio.addEventListener('emptied', callback) + return () => { + audio?.removeEventListener('emptied', callback) + } +} + +export const onTimeupdate = (callback: Noop) => { + if (!audio) throw new Error('audio not defined') + + audio.addEventListener('timeupdate', callback) + return () => { + audio?.removeEventListener('timeupdate', callback) + } +} + +// 缓冲中 +export const onWaiting = (callback: Noop) => { + if (!audio) throw new Error('audio not defined') + + audio.addEventListener('waiting', callback) + return () => { + audio?.removeEventListener('waiting', callback) + } +} + +// 可见性改变 +export const onVisibilityChange = (callback: Noop) => { + document.addEventListener('visibilitychange', callback) + return () => { + document.removeEventListener('visibilitychange', callback) + } +} + + +export const getErrorCode = () => { + return audio?.error?.code +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/pitch-shifter/fft.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/pitch-shifter/fft.js new file mode 100644 index 000000000..db3be21bf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/pitch-shifter/fft.js @@ -0,0 +1,500 @@ +// https://github.com/indutny/fft.js + + +function FFT(size) { + this.size = size | 0 + if (this.size <= 1 || (this.size & (this.size - 1)) !== 0) { throw new Error('FFT size must be a power of two and bigger than 1') } + + this._csize = size << 1 + + // NOTE: Use of `var` is intentional for old V8 versions + let table = new Array(this.size * 2) + for (let i = 0; i < table.length; i += 2) { + const angle = Math.PI * i / this.size + table[i] = Math.cos(angle) + table[i + 1] = -Math.sin(angle) + } + this.table = table + + // Find size's power of two + let power = 0 + for (let t = 1; this.size > t; t <<= 1) { power++ } + + // Calculate initial step's width: + // * If we are full radix-4 - it is 2x smaller to give inital len=8 + // * Otherwise it is the same as `power` to give len=4 + this._width = power % 2 === 0 ? power - 1 : power + + // Pre-compute bit-reversal patterns + this._bitrev = new Array(1 << this._width) + for (let j = 0; j < this._bitrev.length; j++) { + this._bitrev[j] = 0 + for (let shift = 0; shift < this._width; shift += 2) { + let revShift = this._width - shift - 2 + this._bitrev[j] |= ((j >>> shift) & 3) << revShift + } + } + + this._out = null + this._data = null + this._inv = 0 +} + +FFT.prototype.fromComplexArray = function fromComplexArray(complex, storage) { + let res = storage || new Array(complex.length >>> 1) + for (let i = 0; i < complex.length; i += 2) { res[i >>> 1] = complex[i] } + return res +} + +FFT.prototype.createComplexArray = function createComplexArray() { + const res = new Array(this._csize) + for (let i = 0; i < res.length; i++) { res[i] = 0 } + return res +} + +FFT.prototype.toComplexArray = function toComplexArray(input, storage) { + let res = storage || this.createComplexArray() + for (let i = 0; i < res.length; i += 2) { + res[i] = input[i >>> 1] + res[i + 1] = 0 + } + return res +} + +FFT.prototype.completeSpectrum = function completeSpectrum(spectrum) { + let size = this._csize + let half = size >>> 1 + for (let i = 2; i < half; i += 2) { + spectrum[size - i] = spectrum[i] + spectrum[size - i + 1] = -spectrum[i + 1] + } +} + +FFT.prototype.transform = function transform(out, data) { + if (out === data) { throw new Error('Input and output buffers must be different') } + + this._out = out + this._data = data + this._inv = 0 + this._transform4() + this._out = null + this._data = null +} + +FFT.prototype.realTransform = function realTransform(out, data) { + if (out === data) { throw new Error('Input and output buffers must be different') } + + this._out = out + this._data = data + this._inv = 0 + this._realTransform4() + this._out = null + this._data = null +} + +FFT.prototype.inverseTransform = function inverseTransform(out, data) { + if (out === data) { throw new Error('Input and output buffers must be different') } + + this._out = out + this._data = data + this._inv = 1 + this._transform4() + for (let i = 0; i < out.length; i++) { out[i] /= this.size } + this._out = null + this._data = null +} + +// radix-4 implementation +// +// NOTE: Uses of `var` are intentional for older V8 version that do not +// support both `let compound assignments` and `const phi` +FFT.prototype._transform4 = function _transform4() { + let out = this._out + let size = this._csize + + // Initial step (permute and transform) + let width = this._width + let step = 1 << width + let len = (size / step) << 1 + + let outOff + let t + let bitrev = this._bitrev + if (len === 4) { + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t] + this._singleTransform2(outOff, off, step) + } + } else { + // len === 8 + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t] + this._singleTransform4(outOff, off, step) + } + } + + // Loop through steps in decreasing order + let inv = this._inv ? -1 : 1 + let table = this.table + for (step >>= 2; step >= 2; step >>= 2) { + len = (size / step) << 1 + let quarterLen = len >>> 2 + + // Loop through offsets in the data + for (outOff = 0; outOff < size; outOff += len) { + // Full case + let limit = outOff + quarterLen + for (let i = outOff, k = 0; i < limit; i += 2, k += step) { + const A = i + const B = A + quarterLen + const C = B + quarterLen + const D = C + quarterLen + + // Original values + const Ar = out[A] + const Ai = out[A + 1] + const Br = out[B] + const Bi = out[B + 1] + const Cr = out[C] + const Ci = out[C + 1] + const Dr = out[D] + const Di = out[D + 1] + + // Middle values + const MAr = Ar + const MAi = Ai + + const tableBr = table[k] + const tableBi = inv * table[k + 1] + const MBr = Br * tableBr - Bi * tableBi + const MBi = Br * tableBi + Bi * tableBr + + const tableCr = table[2 * k] + const tableCi = inv * table[2 * k + 1] + const MCr = Cr * tableCr - Ci * tableCi + const MCi = Cr * tableCi + Ci * tableCr + + const tableDr = table[3 * k] + const tableDi = inv * table[3 * k + 1] + const MDr = Dr * tableDr - Di * tableDi + const MDi = Dr * tableDi + Di * tableDr + + // Pre-Final values + const T0r = MAr + MCr + const T0i = MAi + MCi + const T1r = MAr - MCr + const T1i = MAi - MCi + const T2r = MBr + MDr + const T2i = MBi + MDi + const T3r = inv * (MBr - MDr) + const T3i = inv * (MBi - MDi) + + // Final values + const FAr = T0r + T2r + const FAi = T0i + T2i + + const FCr = T0r - T2r + const FCi = T0i - T2i + + const FBr = T1r + T3i + const FBi = T1i - T3r + + const FDr = T1r - T3i + const FDi = T1i + T3r + + out[A] = FAr + out[A + 1] = FAi + out[B] = FBr + out[B + 1] = FBi + out[C] = FCr + out[C + 1] = FCi + out[D] = FDr + out[D + 1] = FDi + } + } + } +} + +// radix-2 implementation +// +// NOTE: Only called for len=4 +FFT.prototype._singleTransform2 = function _singleTransform2(outOff, off, + step) { + const out = this._out + const data = this._data + + const evenR = data[off] + const evenI = data[off + 1] + const oddR = data[off + step] + const oddI = data[off + step + 1] + + const leftR = evenR + oddR + const leftI = evenI + oddI + const rightR = evenR - oddR + const rightI = evenI - oddI + + out[outOff] = leftR + out[outOff + 1] = leftI + out[outOff + 2] = rightR + out[outOff + 3] = rightI +} + +// radix-4 +// +// NOTE: Only called for len=8 +FFT.prototype._singleTransform4 = function _singleTransform4(outOff, off, + step) { + const out = this._out + const data = this._data + const inv = this._inv ? -1 : 1 + const step2 = step * 2 + const step3 = step * 3 + + // Original values + const Ar = data[off] + const Ai = data[off + 1] + const Br = data[off + step] + const Bi = data[off + step + 1] + const Cr = data[off + step2] + const Ci = data[off + step2 + 1] + const Dr = data[off + step3] + const Di = data[off + step3 + 1] + + // Pre-Final values + const T0r = Ar + Cr + const T0i = Ai + Ci + const T1r = Ar - Cr + const T1i = Ai - Ci + const T2r = Br + Dr + const T2i = Bi + Di + const T3r = inv * (Br - Dr) + const T3i = inv * (Bi - Di) + + // Final values + const FAr = T0r + T2r + const FAi = T0i + T2i + + const FBr = T1r + T3i + const FBi = T1i - T3r + + const FCr = T0r - T2r + const FCi = T0i - T2i + + const FDr = T1r - T3i + const FDi = T1i + T3r + + out[outOff] = FAr + out[outOff + 1] = FAi + out[outOff + 2] = FBr + out[outOff + 3] = FBi + out[outOff + 4] = FCr + out[outOff + 5] = FCi + out[outOff + 6] = FDr + out[outOff + 7] = FDi +} + +// Real input radix-4 implementation +FFT.prototype._realTransform4 = function _realTransform4() { + let out = this._out + let size = this._csize + + // Initial step (permute and transform) + let width = this._width + let step = 1 << width + let len = (size / step) << 1 + + let outOff + let t + let bitrev = this._bitrev + if (len === 4) { + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t] + this._singleRealTransform2(outOff, off >>> 1, step >>> 1) + } + } else { + // len === 8 + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t] + this._singleRealTransform4(outOff, off >>> 1, step >>> 1) + } + } + + // Loop through steps in decreasing order + let inv = this._inv ? -1 : 1 + let table = this.table + for (step >>= 2; step >= 2; step >>= 2) { + len = (size / step) << 1 + let halfLen = len >>> 1 + let quarterLen = halfLen >>> 1 + let hquarterLen = quarterLen >>> 1 + + // Loop through offsets in the data + for (outOff = 0; outOff < size; outOff += len) { + for (let i = 0, k = 0; i <= hquarterLen; i += 2, k += step) { + let A = outOff + i + let B = A + quarterLen + let C = B + quarterLen + let D = C + quarterLen + + // Original values + let Ar = out[A] + let Ai = out[A + 1] + let Br = out[B] + let Bi = out[B + 1] + let Cr = out[C] + let Ci = out[C + 1] + let Dr = out[D] + let Di = out[D + 1] + + // Middle values + let MAr = Ar + let MAi = Ai + + let tableBr = table[k] + let tableBi = inv * table[k + 1] + let MBr = Br * tableBr - Bi * tableBi + let MBi = Br * tableBi + Bi * tableBr + + let tableCr = table[2 * k] + let tableCi = inv * table[2 * k + 1] + let MCr = Cr * tableCr - Ci * tableCi + let MCi = Cr * tableCi + Ci * tableCr + + let tableDr = table[3 * k] + let tableDi = inv * table[3 * k + 1] + let MDr = Dr * tableDr - Di * tableDi + let MDi = Dr * tableDi + Di * tableDr + + // Pre-Final values + let T0r = MAr + MCr + let T0i = MAi + MCi + let T1r = MAr - MCr + let T1i = MAi - MCi + let T2r = MBr + MDr + let T2i = MBi + MDi + let T3r = inv * (MBr - MDr) + let T3i = inv * (MBi - MDi) + + // Final values + let FAr = T0r + T2r + let FAi = T0i + T2i + + let FBr = T1r + T3i + let FBi = T1i - T3r + + out[A] = FAr + out[A + 1] = FAi + out[B] = FBr + out[B + 1] = FBi + + // Output final middle point + if (i === 0) { + let FCr = T0r - T2r + let FCi = T0i - T2i + out[C] = FCr + out[C + 1] = FCi + continue + } + + // Do not overwrite ourselves + if (i === hquarterLen) { continue } + + // In the flipped case: + // MAi = -MAi + // MBr=-MBi, MBi=-MBr + // MCr=-MCr + // MDr=MDi, MDi=MDr + let ST0r = T1r + let ST0i = -T1i + let ST1r = T0r + let ST1i = -T0i + let ST2r = -inv * T3i + let ST2i = -inv * T3r + let ST3r = -inv * T2i + let ST3i = -inv * T2r + + let SFAr = ST0r + ST2r + let SFAi = ST0i + ST2i + + let SFBr = ST1r + ST3i + let SFBi = ST1i - ST3r + + let SA = outOff + quarterLen - i + let SB = outOff + halfLen - i + + out[SA] = SFAr + out[SA + 1] = SFAi + out[SB] = SFBr + out[SB + 1] = SFBi + } + } + } +} + +// radix-2 implementation +// +// NOTE: Only called for len=4 +FFT.prototype._singleRealTransform2 = function _singleRealTransform2(outOff, + off, + step) { + const out = this._out + const data = this._data + + const evenR = data[off] + const oddR = data[off + step] + + const leftR = evenR + oddR + const rightR = evenR - oddR + + out[outOff] = leftR + out[outOff + 1] = 0 + out[outOff + 2] = rightR + out[outOff + 3] = 0 +} + +// radix-4 +// +// NOTE: Only called for len=8 +FFT.prototype._singleRealTransform4 = function _singleRealTransform4(outOff, + off, + step) { + const out = this._out + const data = this._data + const inv = this._inv ? -1 : 1 + const step2 = step * 2 + const step3 = step * 3 + + // Original values + const Ar = data[off] + const Br = data[off + step] + const Cr = data[off + step2] + const Dr = data[off + step3] + + // Pre-Final values + const T0r = Ar + Cr + const T1r = Ar - Cr + const T2r = Br + Dr + const T3r = inv * (Br - Dr) + + // Final values + const FAr = T0r + T2r + + const FBr = T1r + const FBi = -T3r + + const FCr = T0r - T2r + + const FDr = T1r + const FDi = T3r + + out[outOff] = FAr + out[outOff + 1] = 0 + out[outOff + 2] = FBr + out[outOff + 3] = FBi + out[outOff + 4] = FCr + out[outOff + 5] = 0 + out[outOff + 6] = FDr + out[outOff + 7] = FDi +} + +export default FFT diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/pitch-shifter/ola-processor.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/pitch-shifter/ola-processor.js new file mode 100644 index 000000000..bcbc2b269 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/pitch-shifter/ola-processor.js @@ -0,0 +1,224 @@ +/* eslint-disable no-var */ + +const WEBAUDIO_BLOCK_SIZE = 128 + + +/** Overlap-Add Node */ +class OLAProcessor extends globalThis.AudioWorkletProcessor { + constructor(options) { + super(options) + + this.keepReturnTrue = true + this.processNow = false + + this.nbInputs = options.numberOfInputs + this.nbOutputs = options.numberOfOutputs + + this.blockSize = options.processorOptions.blockSize + // TODO for now, the only support hop size is the size of a web audio block + this.hopSize = WEBAUDIO_BLOCK_SIZE + + this.nbOverlaps = this.blockSize / this.hopSize + + this.lastSilencedHopCount = 0 + this.nbOverlaps2x = this.nbOverlaps * 2 + this.fakeEmptyInputs = [new Array(2).fill(new Float32Array(WEBAUDIO_BLOCK_SIZE))] + + + // pre-allocate input buffers (will be reallocated if needed) + this.inputBuffers = new Array(this.nbInputs) + this.inputBuffersHead = new Array(this.nbInputs) + this.inputBuffersToSend = new Array(this.nbInputs) + // assume 2 channels per input + for (var i = 0; i < this.nbInputs; i++) { + this.allocateInputChannels(i, 2) + } + // pre-allocate input buffers (will be reallocated if needed) + this.outputBuffers = new Array(this.nbOutputs) + this.outputBuffersToRetrieve = new Array(this.nbOutputs) + // assume 2 channels per output + for (i = 0; i < this.nbOutputs; i++) { + this.allocateOutputChannels(i, 2) + } + + this.port.onmessage = (e) => this.keepReturnTrue = false + } + + /** Handles dynamic reallocation of input/output channels buffer + (channel numbers may vary during lifecycle) **/ + reallocateChannelsIfNeeded(inputs, outputs, force) { + for (var i = 0; i < this.nbInputs; i++) { + let nbChannels = inputs[i].length + if (force || (nbChannels != this.inputBuffers[i].length)) { + this.allocateInputChannels(i, nbChannels) + // console.log("reallocateChannelsIfNeeded"); + } + } + + for (i = 0; i < this.nbOutputs; i++) { + let nbChannels = outputs[i].length + if (force || (nbChannels != this.outputBuffers[i].length)) { + this.allocateOutputChannels(i, nbChannels) + // console.log("reallocateChannelsIfNeeded"); + } + } + } + + allocateInputChannels(inputIndex, nbChannels) { + // allocate input buffers + // console.log("allocateInputChannels"); + + this.inputBuffers[inputIndex] = new Array(nbChannels) + for (var i = 0; i < nbChannels; i++) { + this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE) + this.inputBuffers[inputIndex][i].fill(0) + } + + // allocate input buffers to send and head pointers to copy from + // (cannot directly send a pointer/subarray because input may be modified) + this.inputBuffersHead[inputIndex] = new Array(nbChannels) + this.inputBuffersToSend[inputIndex] = new Array(nbChannels) + for (i = 0; i < nbChannels; i++) { + this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i].subarray(0, this.blockSize) + this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize) + } + } + + allocateOutputChannels(outputIndex, nbChannels) { + // allocate output buffers + this.outputBuffers[outputIndex] = new Array(nbChannels) + for (var i = 0; i < nbChannels; i++) { + this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize) + this.outputBuffers[outputIndex][i].fill(0) + } + + // allocate output buffers to retrieve + // (cannot send a pointer/subarray because new output has to be add to existing output) + this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels) + for (i = 0; i < nbChannels; i++) { + this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize) + this.outputBuffersToRetrieve[outputIndex][i].fill(0) + } + } + + checkForNotSilence(value) { + return value !== 0 + } + + /** Read next web audio block to input buffers **/ + readInputs(inputs) { + // when playback is paused, we may stop receiving new samples + /* if (inputs[0].length && inputs[0][0].length == 0) { + for (var i = 0; i < this.nbInputs; i++) { + for (var j = 0; j < this.inputBuffers[i].length; j++) { + this.inputBuffers[i][j].fill(0, this.blockSize); + } + } + return; + } */ + + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + let webAudioBlock = inputs[i][j] + this.inputBuffers[i][j]?.set(webAudioBlock, this.blockSize) + } + } + } + + /** Shift left content of input buffers to receive new web audio block **/ + shiftInputBuffers() { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE) + } + } + } + + /** Copy contents of input buffers to buffer actually sent to process **/ + prepareInputBuffersToSend() { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j]) + } + } + } + + /** Add contents of output buffers just processed to output buffers **/ + handleOutputBuffersToRetrieve() { + for (let i = 0; i < this.nbOutputs; i++) { + for (let j = 0; j < this.outputBuffers[i].length; j++) { + for (let k = 0; k < this.blockSize; k++) { + this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps + } + } + } + } + + /** Write next web audio block from output buffers **/ + writeOutputs(outputs) { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE) + outputs[i][j]?.set(webAudioBlock) + } + } + } + + /** Shift left content of output buffers to receive new web audio block **/ + shiftOutputBuffers() { + for (let i = 0; i < this.nbOutputs; i++) { + for (let j = 0; j < this.outputBuffers[i].length; j++) { + this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE) + this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0) + } + } + } + + process(inputs, outputs, params) { + // console.log(inputs[0].length ? "active" : "inactive"); + // this.reallocateChannelsIfNeeded(inputs, outputs); + // if (inputs[0][0].some(this.checkForNotSilence) || inputs[0][1].some(this.checkForNotSilence)) + // console.log(inputs[0].length) + if (inputs[0].length < 2) { + // DUE TO CHROME BUG/INCONSISTENCY, WHEN INACTIVE SILENT NODE IS CONNECTED, inputs[0] IS EITHER EMPTY OR CONTAINS 1 CHANNEL OF SILENT AUDIO DATA, REQUIRES SPECIAL HANDLING + // if (inputs[0][0].some(this.checkForNotSilence)) console.warn("single channel not silence exception!"); + if (this.lastSilencedHopCount < this.nbOverlaps2x) { + // ALLOW nbOverlaps2x BLOCKS OF SILENCE TO COME THROUGH TO ACCOMODATE LATENCY TAIL + this.lastSilencedHopCount++ + inputs = this.fakeEmptyInputs + this.processNow = true + } else { + // console.warn("skipping processing"); + if (this.lastSilencedHopCount === this.nbOverlaps2x) { + this.lastSilencedHopCount++ + this.reallocateChannelsIfNeeded(this.fakeEmptyInputs, outputs, true) + // console.warn("reallocateChannels"); + } + this.processNow = false // ENABLES SKIPPING UNNEEDED PROCESSING OF SILENT INPUT + } + } else { + if (this.lastSilencedHopCount) { + this.lastSilencedHopCount = 0 + // this.reallocateChannelsIfNeeded(inputs, outputs, true); + // console.warn("reallocateChannels"); + } + this.processNow = true + } + if (this.processNow) { + this.readInputs(inputs) + this.shiftInputBuffers() + this.prepareInputBuffersToSend() + this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params) + this.handleOutputBuffersToRetrieve() + this.writeOutputs(outputs) + this.shiftOutputBuffers() + } + return this.keepReturnTrue + } + + /* processOLA(inputs, outputs, params) { + console.assert(false, "Not overriden"); + } */ +} + +export default OLAProcessor diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/pitch-shifter/phase-vocoder.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/pitch-shifter/phase-vocoder.js new file mode 100644 index 000000000..8761dbfa1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/plugins/player/pitch-shifter/phase-vocoder.js @@ -0,0 +1,176 @@ +// https://github.com/olvb/phaze/issues/26#issuecomment-1573938170 +// https://github.com/olvb/phaze +import FFT from './fft' +import OLAProcessor from './ola-processor' + + +const DEFAULT_BUFFERED_BLOCK_SIZE = 4096 + +function genHannWindow(length) { + let win = new Float32Array(length) + for (let i = 0; i < length; i++) { + win[i] = 0.8 * (1 - Math.cos(2 * Math.PI * i / length)) + } + return win +} + +class PhaseVocoderProcessor extends OLAProcessor { + static get parameterDescriptors() { + return [{ + name: 'pitchFactor', + defaultValue: 1.0, + automationRate: 'k-rate', + }, /* , + { + name: 'pitchCents', + defaultValue: 0.0, + automationRate: 'k-rate' + } */] + } + + constructor(options) { + (options.processorOptions ??= {}).blockSize ??= DEFAULT_BUFFERED_BLOCK_SIZE + super(options) + + this.fftSize = this.blockSize + this.timeCursor = 0 + + this.hannWindow = genHannWindow(this.blockSize) + + // prepare FFT and pre-allocate buffers + this.fft = new FFT(this.fftSize) + this.freqComplexBuffer = this.fft.createComplexArray() + this.freqComplexBufferShifted = this.fft.createComplexArray() + this.timeComplexBuffer = this.fft.createComplexArray() + this.magnitudes = new Float32Array(this.fftSize / 2 + 1) + this.peakIndexes = new Int32Array(this.magnitudes.length) + this.nbPeaks = 0 + } + + processOLA(inputs, outputs, parameters) { + // k-rate automation, param arrays only have single value + const pitchFactor = parameters.pitchFactor[0]/* || Math.pow(2, (parameters.pitchCents[0]/12)) */ + + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < inputs[i].length; j++) { + // big assumption here: output is symetric to input + let input = inputs[i][j] + let output = outputs[i][j] + + this.applyHannWindow(input) + + this.fft.realTransform(this.freqComplexBuffer, input) + + this.computeMagnitudes() + this.findPeaks() + this.shiftPeaks(pitchFactor) + + this.fft.completeSpectrum(this.freqComplexBufferShifted) + this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted) + this.fft.fromComplexArray(this.timeComplexBuffer, output) + + this.applyHannWindow(output) + } + } + + this.timeCursor += this.hopSize + } + + /** Apply Hann window in-place */ + applyHannWindow(input) { + for (let i = 0; i < this.blockSize; i++) { + input[i] = input[i] * this.hannWindow[i] + } + } + + /** Compute squared magnitudes for peak finding **/ + computeMagnitudes() { + let i = 0; let j = 0 + while (i < this.magnitudes.length) { + let real = this.freqComplexBuffer[j] + let imag = this.freqComplexBuffer[j + 1] + // no need to sqrt for peak finding + this.magnitudes[i] = real ** 2 + imag ** 2 + i += 1 + j += 2 + } + } + + /** Find peaks in spectrum magnitudes **/ + findPeaks() { + this.nbPeaks = 0 + let i = 2 + let end = this.magnitudes.length - 2 + + while (i < end) { + let mag = this.magnitudes[i] + + if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) { + i++ + continue + } + if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) { + i++ + continue + } + + this.peakIndexes[this.nbPeaks] = i + this.nbPeaks++ + i += 2 + } + } + + /** Shift peaks and regions of influence by pitchFactor into new specturm */ + shiftPeaks(pitchFactor) { + // zero-fill new spectrum + this.freqComplexBufferShifted.fill(0) + + for (let i = 0; i < this.nbPeaks; i++) { + let peakIndex = this.peakIndexes[i] + let peakIndexShifted = Math.round(peakIndex * pitchFactor) + + if (peakIndexShifted > this.magnitudes.length) break + + // find region of influence + let startIndex = 0 + let endIndex = this.fftSize + if (i > 0) { + startIndex = peakIndex - Math.floor((peakIndex - this.peakIndexes[i - 1]) / 2) + } + if (i < this.nbPeaks - 1) { + endIndex = peakIndex + Math.ceil((this.peakIndexes[i + 1] - peakIndex) / 2) + } + + // shift whole region of influence around peak to shifted peak + let startOffset = startIndex - peakIndex + let endOffset = endIndex - peakIndex + for (let j = startOffset; j < endOffset; j++) { + let binIndex = peakIndex + j + let binIndexShifted = peakIndexShifted + j + + if (binIndexShifted >= this.magnitudes.length) break + + // apply phase correction + let omegaDelta = 2 * Math.PI * (binIndexShifted - binIndex) / this.fftSize + let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor) + let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor) + + let indexReal = binIndex * 2 + let indexImag = indexReal + 1 + let valueReal = this.freqComplexBuffer[indexReal] + let valueImag = this.freqComplexBuffer[indexImag] + + let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag + let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal + + let indexShiftedReal = binIndexShifted * 2 + let indexShiftedImag = indexShiftedReal + 1 + this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal + this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag + } + } + } +} + +globalThis.registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor) + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/router.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/router.ts new file mode 100644 index 000000000..ec04c7d6a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/router.ts @@ -0,0 +1,72 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +// import Vue from 'vue' +import { createRouter, createWebHashHistory } from 'vue-router' + + +const router = createRouter({ + history: createWebHashHistory(), + routes: [ + { + path: '/search', + name: 'Search', + component: require('./views/Search/index.vue').default, + meta: { + name: 'Search', + }, + }, + { + path: '/songList/list', + name: 'SongList', + component: require('./views/songList/List/index.vue').default, + meta: { + name: 'SongList', + }, + }, + { + path: '/songList/detail', + name: 'SongListDetail', + component: require('./views/songList/Detail/index.vue').default, + meta: { + name: 'SongList', + }, + }, + { + path: '/leaderboard', + name: 'Leaderboard', + component: require('./views/Leaderboard/index.vue').default, + meta: { + name: 'Leaderboard', + }, + }, + { + path: '/list', + name: 'List', + component: require('./views/List/index.vue').default, + meta: { + name: 'List', + }, + }, + { + path: '/download', + name: 'Download', + component: require('./views/Download/index.vue').default, + meta: { + name: 'Download', + }, + }, + { + path: '/setting', + name: 'Setting', + component: require('./views/Setting/index.vue').default, + meta: { + name: 'Setting', + }, + }, + { path: '/:pathMatch(.*)*', redirect: '/search' }, + ], + linkActiveClass: 'active-link', + linkExactActiveClass: 'exact-active-link', +}) + + +export default router diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/dislikeList/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/dislikeList/action.ts new file mode 100644 index 000000000..7200beae4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/dislikeList/action.ts @@ -0,0 +1,92 @@ +import { markRaw } from '@common/utils/vueTools' + + +import { dislikeInfo, dislikeRuleCount } from './state' +import { SPLIT_CHAR } from '@common/constants' + + +export const hasDislike = (info: LX.Music.MusicInfo | LX.Download.ListItem) => { + if ('progress' in info) info = info.metadata.musicInfo + const name = info.name?.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() ?? '' + const singer = info.singer?.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() ?? '' + + return dislikeInfo.musicNames.has(name) || dislikeInfo.singerNames.has(singer) || + dislikeInfo.names.has(`${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}`) +} + +export const initDislikeInfo = ({ musicNames, rules, names, singerNames }: LX.Dislike.DislikeInfo) => { + dislikeInfo.names = markRaw(names) + dislikeInfo.singerNames = markRaw(singerNames) + dislikeInfo.musicNames = markRaw(musicNames) + dislikeInfo.rules = rules + dislikeRuleCount.value = dislikeInfo.musicNames.size + dislikeInfo.singerNames.size + dislikeInfo.names.size +} + +const initNameSet = () => { + dislikeInfo.names.clear() + dislikeInfo.musicNames.clear() + dislikeInfo.singerNames.clear() + const list: string[] = [] + for (const item of dislikeInfo.rules.split('\n')) { + if (!item) continue + let [name, singer] = item.split(SPLIT_CHAR.DISLIKE_NAME) + if (name) { + name = name.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + if (singer) { + singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + const rule = `${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}` + dislikeInfo.names.add(rule) + list.push(rule) + } else { + dislikeInfo.musicNames.add(name) + list.push(name) + } + } else if (singer) { + singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + dislikeInfo.singerNames.add(singer) + list.push(`${SPLIT_CHAR.DISLIKE_NAME}${singer}`) + } + } + dislikeInfo.rules = Array.from(new Set(list)).join('\n') + dislikeRuleCount.value = dislikeInfo.musicNames.size + dislikeInfo.singerNames.size + dislikeInfo.names.size +} + +export const addDislikeInfo = (infos: LX.Dislike.DislikeMusicInfo[]) => { + dislikeInfo.rules += '\n' + infos.map(info => `${info.name ?? ''}${SPLIT_CHAR.DISLIKE_NAME}${info.singer ?? ''}`).join('\n') + initNameSet() + return dislikeInfo.rules +} + +export const overwirteDislikeInfo = (rules: string) => { + dislikeInfo.rules = rules + initNameSet() + return dislikeInfo.rules +} + +export const clearDislikeInfo = () => { + dislikeInfo.rules = '' + initNameSet() + return dislikeInfo.rules +} + + +// export const updateDislikeInfo = (info: LX.Dislike.ListItem) => { +// const targetInfo = dislikeInfo.list.find(i => i.id == info.id) +// if (!targetInfo) return +// targetInfo.name = info.name +// targetInfo.singer = info.singer +// initNameSet() +// } + +// export const removeDislikeInfo = (ids: string[]) => { +// for (const id of ids) { +// dislikeInfo.list.splice(dislikeInfo.list.findIndex(info => info.id == id), 1) +// } +// initNameSet() +// } + +// export const clearDislikeInfo = () => { +// dislikeInfo.rules = '' +// initNameSet() +// } + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/dislikeList/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/dislikeList/index.ts new file mode 100644 index 000000000..b71e70d2d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/dislikeList/index.ts @@ -0,0 +1,3 @@ + +export * as action from './action' +export * from './state' diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/dislikeList/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/dislikeList/state.ts new file mode 100644 index 000000000..39a3f2c8c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/dislikeList/state.ts @@ -0,0 +1,13 @@ +import { markRaw, ref } from '@common/utils/vueTools' + +// import { deduplicationList } from '@common/utils/renderer' + + +export const dislikeInfo: LX.Dislike.DislikeInfo = markRaw({ + names: markRaw(new Set()), + musicNames: markRaw(new Set()), + singerNames: markRaw(new Set()), + rules: '', +}) + +export const dislikeRuleCount = ref(0) diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/download/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/download/action.ts new file mode 100644 index 000000000..313d38e71 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/download/action.ts @@ -0,0 +1,427 @@ +import { + downloadTasksGet, + // downloadListClear, + downloadTasksCreate, + downloadTasksRemove, + downloadTasksUpdate, +} from '@renderer/utils/ipc' +import { + downloadList, +} from './state' +import { markRaw, toRaw } from '@common/utils/vueTools' +import { getMusicUrl, getPicUrl, getLyricInfo } from '@renderer/core/music/online' +import { appSetting } from '../setting' +import { qualityList } from '..' +import { proxyCallback } from '@renderer/worker/utils' +import { arrPush, arrUnshift, joinPath } from '@renderer/utils' +import { DOWNLOAD_STATUS } from '@common/constants' +import { proxy } from '../index' +import { buildSavePath } from './utils' + +const waitingUpdateTasks = new Map() +let timer: NodeJS.Timeout | null = null +const throttleUpdateTask = (tasks: LX.Download.ListItem[]) => { + for (const task of tasks) waitingUpdateTasks.set(task.id, toRaw(task)) + if (timer) return + timer = setTimeout(() => { + timer = null + void downloadTasksUpdate(Array.from(waitingUpdateTasks.values())) + waitingUpdateTasks.clear() + }, 100) +} + +const runingTask = new Map() + +// const initDownloadList = (list: LX.Download.ListItem[]) => { +// downloadList.splice(0, downloadList.length, ...list) +// } + +export const getDownloadList = async(): Promise => { + if (!downloadList.length) { + const list = await downloadTasksGet() + for (const downloadInfo of list) { + markRaw(downloadInfo.metadata) + switch (downloadInfo.status) { + case DOWNLOAD_STATUS.RUN: + case DOWNLOAD_STATUS.WAITING: + downloadInfo.status = DOWNLOAD_STATUS.PAUSE + downloadInfo.statusText = window.i18n.t('download___status_paused') + default: + break + } + } + arrPush(downloadList, list) + } + return downloadList +} + +const addTasks = async(list: LX.Download.ListItem[]) => { + const addMusicLocationType = appSetting['list.addMusicLocationType'] + + await downloadTasksCreate(list.map(i => toRaw(i)), addMusicLocationType) + + if (addMusicLocationType === 'top') { + arrUnshift(downloadList, list) + } else { + arrPush(downloadList, list) + } + window.app_event.downloadListUpdate() +} + +const setStatusText = (downloadInfo: LX.Download.ListItem, text: string) => { // 设置状态文本 + downloadInfo.statusText = text + throttleUpdateTask([downloadInfo]) +} + +const setUrl = (downloadInfo: LX.Download.ListItem, url: string) => { + downloadInfo.metadata.url = url + throttleUpdateTask([downloadInfo]) +} + +const updateFilePath = (downloadInfo: LX.Download.ListItem, filePath: string) => { + downloadInfo.metadata.filePath = filePath + throttleUpdateTask([downloadInfo]) +} + +const setProgress = (downloadInfo: LX.Download.ListItem, progress: LX.Download.ProgressInfo) => { + downloadInfo.total = progress.total + downloadInfo.downloaded = progress.downloaded + downloadInfo.writeQueue = progress.writeQueue + if (progress.progress == 100) { + downloadInfo.speed = '' + downloadInfo.progress = 99.99 + setStatusText(downloadInfo, window.i18n.t('download_status_write_queue', { num: progress.writeQueue })) + } else { + downloadInfo.speed = progress.speed + downloadInfo.progress = progress.progress + } + throttleUpdateTask([downloadInfo]) +} + +const setStatus = (downloadInfo: LX.Download.ListItem, status: LX.Download.DownloadTaskStatus, statusText?: string) => { // 设置状态及状态文本 + if (statusText == null) { + switch (status) { + case DOWNLOAD_STATUS.RUN: + statusText = window.i18n.t('download___status_running') + break + case DOWNLOAD_STATUS.WAITING: + statusText = window.i18n.t('download___status_waiting') + break + case DOWNLOAD_STATUS.PAUSE: + statusText = window.i18n.t('download___status_paused') + break + case DOWNLOAD_STATUS.ERROR: + statusText = window.i18n.t('download___status_error') + break + case DOWNLOAD_STATUS.COMPLETED: + statusText = window.i18n.t('download___status_completed') + break + default: + statusText = '' + break + } + } + + if (downloadInfo.statusText == statusText && downloadInfo.status == status) return + + if (status == DOWNLOAD_STATUS.COMPLETED) downloadInfo.isComplate = true + downloadInfo.statusText = statusText + downloadInfo.status = status + throttleUpdateTask([downloadInfo]) +} + +// 修复 1.1.x版本 酷狗源歌词格式 +const fixKgLyric = (lrc: string) => /\[00:\d\d:\d\d.\d+\]/.test(lrc) ? lrc.replace(/(?:\[00:(\d\d:\d\d.\d+\]))/gm, '[$1') : lrc + +const getProxy = () => { + return proxy.enable && proxy.host ? { + host: proxy.host, + port: parseInt(proxy.port || '80'), + } : proxy.envProxy ? { + host: proxy.envProxy.host, + port: parseInt(proxy.envProxy.port || '80'), + } : undefined +} +/** + * 设置歌曲meta信息 + * @param downloadInfo 下载任务信息 + */ +const saveMeta = (downloadInfo: LX.Download.ListItem) => { + if (downloadInfo.metadata.quality === 'ape') return + const isUseOtherSource = appSetting['download.isUseOtherSource'] + const tasks: [Promise, Promise] = [ + appSetting['download.isEmbedPic'] + ? downloadInfo.metadata.musicInfo.meta.picUrl + ? Promise.resolve(downloadInfo.metadata.musicInfo.meta.picUrl) + : getPicUrl({ musicInfo: downloadInfo.metadata.musicInfo, isRefresh: false, allowToggleSource: isUseOtherSource }).catch(err => { + console.log(err) + return null + }) + : Promise.resolve(null), + appSetting['download.isEmbedLyric'] + ? getLyricInfo({ musicInfo: downloadInfo.metadata.musicInfo, isRefresh: false, allowToggleSource: isUseOtherSource }).catch(err => { + console.log(err) + return null + }) + : Promise.resolve(null), + ] + void Promise.all(tasks).then(([imgUrl, lyrics]) => { + const info = { + filePath: downloadInfo.metadata.filePath, + isEmbedLyricLx: appSetting['download.isEmbedLyricLx'], + isEmbedLyricT: appSetting['download.isEmbedLyricT'], + isEmbedLyricR: appSetting['download.isEmbedLyricR'], + title: downloadInfo.metadata.musicInfo.name, + artist: downloadInfo.metadata.musicInfo.singer?.replaceAll('、', ';'), + album: downloadInfo.metadata.musicInfo.meta.albumName, + APIC: imgUrl, + } + void window.lx.worker.download.writeMeta(info, lyrics ?? { lyric: '' }, getProxy()) + }) +} + +/** + * 保存歌词文件 + * @param downloadInfo 下载任务信息 + */ +const downloadLyric = (downloadInfo: LX.Download.ListItem) => { + if (!appSetting['download.isDownloadLrc']) return + void getLyricInfo({ + musicInfo: downloadInfo.metadata.musicInfo, + isRefresh: false, + allowToggleSource: appSetting['download.isUseOtherSource'], + }).then(lrcs => { + if (lrcs.lyric) { + lrcs.lyric = fixKgLyric(lrcs.lyric) + const info = { + filePath: downloadInfo.metadata.filePath.substring(0, downloadInfo.metadata.filePath.lastIndexOf('.')) + '.lrc', + format: appSetting['download.lrcFormat'], + downloadLxlrc: appSetting['download.isDownloadLxLrc'], + downloadTlrc: appSetting['download.isDownloadTLrc'], + downloadRlrc: appSetting['download.isDownloadRLrc'], + } + void window.lx.worker.download.saveLrc(lrcs, info) + } + }) +} + +const getUrl = async(downloadInfo: LX.Download.ListItem, isRefresh: boolean = false) => { + let toggleMusicInfo = downloadInfo.metadata.musicInfo.meta.toggleMusicInfo + return (toggleMusicInfo ? getMusicUrl({ + musicInfo: toggleMusicInfo, + isRefresh, + quality: downloadInfo.metadata.quality, + allowToggleSource: false, + }) : Promise.reject(new Error('not found'))).catch(() => { + return getMusicUrl({ + musicInfo: downloadInfo.metadata.musicInfo, + isRefresh: false, + quality: downloadInfo.metadata.quality, + allowToggleSource: appSetting['download.isUseOtherSource'], + }) + }).catch(() => '') +} +const handleRefreshUrl = (downloadInfo: LX.Download.ListItem) => { + setStatusText(downloadInfo, window.i18n.t('download_status_error_refresh_url')) + let toggleMusicInfo = downloadInfo.metadata.musicInfo.meta.toggleMusicInfo + ;(toggleMusicInfo ? getMusicUrl({ + musicInfo: toggleMusicInfo, + isRefresh: true, + quality: downloadInfo.metadata.quality, + allowToggleSource: false, + }) : Promise.reject(new Error('not found'))).catch(() => { + return getMusicUrl({ + musicInfo: downloadInfo.metadata.musicInfo, + isRefresh: true, + quality: downloadInfo.metadata.quality, + allowToggleSource: appSetting['download.isUseOtherSource'], + }) + }) + .catch(() => '') + .then(url => { + // commit('setStatusText', { downloadInfo, text: '链接刷新成功' }) + setUrl(downloadInfo, url) + void window.lx.worker.download.updateUrl(downloadInfo.id, url) + }) + .catch(err => { + console.log(err) + handleError(downloadInfo, err.message) + }) +} +const handleError = (downloadInfo: LX.Download.ListItem, message?: string) => { + setStatus(downloadInfo, DOWNLOAD_STATUS.ERROR, message) + void window.lx.worker.download.removeTask(downloadInfo.id) + runingTask.delete(downloadInfo.id) + void checkStartTask() +} + +const handleStartTask = async(downloadInfo: LX.Download.ListItem) => { + if (!downloadInfo.metadata.url) { + setStatusText(downloadInfo, window.i18n.t('download_status_url_getting')) + const url = await getUrl(downloadInfo) + if (!url) { + handleError(downloadInfo, window.i18n.t('download_status_error_url_failed')) + return + } + setUrl(downloadInfo, url) + if (downloadInfo.status != DOWNLOAD_STATUS.RUN) return + } + + const savePath = buildSavePath(downloadInfo) + const filePath = joinPath(savePath, downloadInfo.metadata.fileName) + if (downloadInfo.metadata.filePath != filePath) updateFilePath(downloadInfo, filePath) + + setStatusText(downloadInfo, window.i18n.t('download_status_start')) + + await window.lx.worker.download.startTask(toRaw(downloadInfo), savePath, appSetting['download.skipExistFile'], proxyCallback((event: LX.Download.DownloadTaskActions) => { + // console.log(event) + switch (event.action) { + case 'start': + setStatus(downloadInfo, DOWNLOAD_STATUS.RUN) + break + case 'complete': + downloadInfo.progress = 100 + saveMeta(downloadInfo) + downloadLyric(downloadInfo) + void window.lx.worker.download.removeTask(downloadInfo.id) + runingTask.delete(downloadInfo.id) + setStatus(downloadInfo, DOWNLOAD_STATUS.COMPLETED) + void checkStartTask() + break + case 'refreshUrl': + handleRefreshUrl(downloadInfo) + break + case 'statusText': + setStatusText(downloadInfo, event.data) + break + case 'progress': + setProgress(downloadInfo, event.data) + break + case 'error': + handleError(downloadInfo, event.data.error + ? window.i18n.t(event.data.error) + (event.data.message ?? '') + : event.data.message, + ) + break + default: + break + } + }), getProxy()) +} +const startTask = async(downloadInfo: LX.Download.ListItem) => { + setStatus(downloadInfo, DOWNLOAD_STATUS.RUN) + runingTask.set(downloadInfo.id, downloadInfo) + void handleStartTask(downloadInfo) +} + +const getStartTask = (list: LX.Download.ListItem[]): LX.Download.ListItem | null => { + let downloadCount = 0 + const waitList = list.filter(item => { + if (item.status == DOWNLOAD_STATUS.WAITING) return true + if (item.status == DOWNLOAD_STATUS.RUN) ++downloadCount + return false + }) + // console.log(downloadCount, waitList) + return downloadCount < appSetting['download.maxDownloadNum'] ? waitList.shift() ?? null : null +} + +const checkStartTask = async() => { + if (runingTask.size >= appSetting['download.maxDownloadNum']) return + let result = getStartTask(downloadList) + // console.log(result) + while (result) { + await startTask(result) + result = getStartTask(downloadList) + } +} + +/** + * 过滤重复任务 + * @param list + */ +const filterTask = (list: LX.Download.ListItem[]) => { + const set = new Set() + for (const item of downloadList) set.add(item.id) + return list.filter(item => { + if (set.has(item.id)) return false + markRaw(item.metadata) + set.add(item.id) + return true + }) +} +/** + * 创建下载任务 + * @param list 要下载的歌曲 + * @param quality 下载音质 + */ +export const createDownloadTasks = async(list: LX.Music.MusicInfoOnline[], quality: LX.Quality, listId?: string) => { + if (!list.length) return + const tasks = filterTask(await window.lx.worker.download.createDownloadTasks(list, quality, + appSetting['download.fileName'], + toRaw(qualityList.value), listId), + ) + + if (tasks.length) await addTasks(tasks) + void checkStartTask() +} + +/** + * 开始下载任务 + * @param list + */ +export const startDownloadTasks = async(list: LX.Download.ListItem[]) => { + for (const downloadInfo of list) { + switch (downloadInfo.status) { + case DOWNLOAD_STATUS.PAUSE: + case DOWNLOAD_STATUS.ERROR: + if (runingTask.size < appSetting['download.maxDownloadNum']) void startTask(downloadInfo) + else setStatus(downloadInfo, DOWNLOAD_STATUS.WAITING) + default: + break + } + } + void checkStartTask() +} + +/** + * 暂停下载任务 + * @param list + */ +export const pauseDownloadTasks = async(list: LX.Download.ListItem[]) => { + for (const downloadInfo of list) { + switch (downloadInfo.status) { + case DOWNLOAD_STATUS.RUN: + void window.lx.worker.download.pauseTask(downloadInfo.id) + runingTask.delete(downloadInfo.id) + case DOWNLOAD_STATUS.WAITING: + case DOWNLOAD_STATUS.ERROR: + setStatus(downloadInfo, DOWNLOAD_STATUS.PAUSE) + default: + break + } + } + void checkStartTask() +} + +/** + * 移除下载任务 + * @param ids 要移除的任务Id + */ +export const removeDownloadTasks = async(ids: string[]) => { + await downloadTasksRemove(ids) + + const idsSet = new Set(ids) + const newList = downloadList.filter(task => { + if (runingTask.has(task.id)) { + void window.lx.worker.download.removeTask(task.id) + runingTask.delete(task.id) + } + return !idsSet.has(task.id) + }) + downloadList.splice(0, downloadList.length) + arrPush(downloadList, newList) + + + void checkStartTask() + window.app_event.downloadListUpdate() +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/download/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/download/state.ts new file mode 100644 index 000000000..e37108426 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/download/state.ts @@ -0,0 +1,13 @@ +import { reactive, ref, markRaw } from '@common/utils/vueTools' +import { DOWNLOAD_STATUS } from '@common/constants' + +export const isInitedList = ref(false) + +export const setInited = () => { + isInitedList.value = true +} + +export const downloadList = reactive([]) +// export const downloadListMap = new Map() + +export const downloadStatus = markRaw(DOWNLOAD_STATUS) diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/download/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/download/utils.ts new file mode 100644 index 000000000..17278b95d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/download/utils.ts @@ -0,0 +1,27 @@ +import { appSetting } from '@renderer/store/setting' +import { defaultList, loveList, userLists } from '@renderer/store/list/listManage' +import { filterFileName } from '@common/utils/common' +import { clipFileNameLength } from '@common/utils/tools' +import { joinPath } from '@common/utils/nodejs' + +export const buildSavePath = (musicInfo: LX.Download.ListItem) => { + let savePath = appSetting['download.savePath'] + if (appSetting['download.isSavePathGroupByListName']) { + let dirName: string | undefined + const listId = musicInfo.metadata.listId + switch (listId) { + case defaultList.id: + dirName = window.i18n.t(defaultList.name) + break + case loveList.id: + dirName = window.i18n.t(loveList.name) + break + default: + dirName = userLists.find(list => list.id === listId)?.name + break + } + if (dirName) dirName = filterFileName(dirName) + savePath = joinPath(savePath, clipFileNameLength(dirName ?? window.i18n.t(defaultList.name))) + } + return savePath +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/hotSearch.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/hotSearch.ts new file mode 100644 index 000000000..3055a3bab --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/hotSearch.ts @@ -0,0 +1,77 @@ +import { reactive, markRaw } from '@common/utils/vueTools' +import music from '@renderer/utils/musicSdk' + +// import { deduplicationList } from '@common/utils/renderer' + +export type Source = LX.OnlineSource | 'all' + +interface SourceLists extends Partial> { + 'all': string[] +} + +export const sources: Source[] = markRaw([]) + +export const sourceList: SourceLists = markRaw({ + all: reactive([]), +}) + + +for (const source of music.sources) { + if (!music[source.id as LX.OnlineSource]?.hotSearch) continue + sources.push(source.id as LX.OnlineSource) + sourceList[source.id as LX.OnlineSource] = reactive([]) +} +sources.push('all') + + +const setList = (source: LX.OnlineSource, list: string[]): string[] => { + return sourceList[source] = list.slice(0, 20) +} + +const setLists = (lists: Array<{ source: LX.OnlineSource, list: string[] }>): string[] => { + let wordsMap = new Map() + for (const { source, list } of lists) { + if (!sourceList[source]?.length) sourceList[source] = list.slice(0, 20) + for (let item of list) { + item = item.trim() + wordsMap.set(item, (wordsMap.get(item) ?? 0) + 1) + } + } + const wordsMapArr = Array.from(wordsMap) + wordsMapArr.sort((a, b) => a[0].localeCompare(b[0])) + wordsMapArr.sort((a, b) => b[1] - a[1]) + const words = wordsMapArr.map(item => item[0]) + return sourceList.all = words.slice(0, sources.length * 10) +} + +export const getList = async(source: Source): Promise => { + if (source == 'all') { + let task = [] + for (const source of sources) { + if (source == 'all') continue + task.push( + sourceList[source]?.length + ? Promise.resolve({ source, list: sourceList[source] }) + : (music[source]?.hotSearch.getList() ?? Promise.reject(new Error('source not found: ' + source))).catch((err: any) => { + console.log(err) + return { source, list: [] } + }), + ) + } + return Promise.all(task).then((results: any[]) => { + return setLists(results) + }) + } else { + if (sourceList[source]?.length) return Promise.resolve(sourceList[source]) + if (!music[source]?.hotSearch) { + setList(source, []) + return Promise.resolve([]) + } + return music[source]?.hotSearch.getList().then(data => setList(source, data.list)) + } +} + + +export const clearList = (source: Source) => { + sourceList[source] = [] +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/index.ts new file mode 100644 index 000000000..585c83451 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/index.ts @@ -0,0 +1,167 @@ +import { ref, reactive, shallowRef, markRaw, computed, watch } from '@common/utils/vueTools' +import { windowSizeList as configWindowSizeList } from '@common/config' +import { appSetting } from './setting' +import pkg from '../../../package.json' +import { type ProgressInfo } from 'electron-updater' +import music from '@renderer/utils/musicSdk' +process.versions.app = pkg.version + +export const apiSource = ref(null) +export const proxy: { + enable: boolean + host: string + port: string + + envProxy?: { + host: string + port: string + } +} = { + enable: false, + host: '', + port: '', +} +export const sync: { + enable: boolean + mode: LX.AppSetting['sync.mode'] + isShowSyncMode: boolean + isShowAuthCodeModal: boolean + deviceName: string + type: keyof LX.Sync.ModeTypes + server: { + port: string + status: { + status: boolean + message: string + address: string[] + code: string + devices: LX.Sync.ServerKeyInfo[] + } + } + client: { + host: string + status: { + status: boolean + message: string + address: string[] + } + } +} = reactive({ + enable: false, + mode: 'server', + isShowSyncMode: false, + isShowAuthCodeModal: false, + deviceName: '', + type: 'list', + server: { + port: '', + status: { + status: false, + message: '', + address: [], + code: '', + devices: [], + }, + }, + client: { + host: '', + status: { + status: false, + message: '', + address: [], + }, + }, +}) + +export const openAPI = reactive({ + address: '', + message: '', +}) + + +export const windowSizeActive = computed(() => { + return windowSizeList.find(i => i.id === appSetting['common.windowSizeId']) ?? windowSizeList[0] +}) + +export const getSourceI18nPrefix = () => { + return appSetting['common.sourceNameType'] == 'real' ? 'source_' : 'source_alias_' +} + +export const sourceNames = computed(() => { + const prefix = getSourceI18nPrefix() + const sourceNames: Record = { + kw: 'kw', + tx: 'tx', + kg: 'kg', + mg: 'mg', + wy: 'wy', + all: window.i18n.t(prefix + 'all' as any), + } + for (const { id } of music.sources) { + sourceNames[id as LX.OnlineSource] = window.i18n.t(prefix + id as any) + } + + return sourceNames +}) + +export const windowSizeList = markRaw(configWindowSizeList) + +export const isShowPact = ref(false) + +export const versionInfo = window.lxData.versionInfo = reactive<{ + version: string + newVersion: { + version: string + desc: string + history?: LX.VersionInfo[] + } | null + showModal: boolean + isUnknown: boolean + isLatest: boolean + reCheck: boolean + status: LX.UpdateStatus + downloadProgress: ProgressInfo | null +}>({ + version: pkg.version, + newVersion: null, + showModal: false, + reCheck: false, + isUnknown: false, + isLatest: false, + status: 'checking', + downloadProgress: null, +}) +export const userApi = reactive<{ + list: LX.UserApi.UserApiInfo[] + status: boolean + message?: string + apis: Partial +}>({ + list: [], + status: false, + message: 'initing', + apis: {}, +}) + +export const isShowChangeLog = ref(false) + + +export const isFullscreen = ref(false) +watch(isFullscreen, isFullscreen => { + window.lx.rootOffset = window.dt || isFullscreen ? 0 : 8 +}, { immediate: true }) + +export const themeShouldUseDarkColors = ref(window.shouldUseDarkColors) + + +export const qualityList = shallowRef({}) +export const setQualityList = (_qualityList: LX.QualityList) => { + qualityList.value = _qualityList +} + +export const themeId = ref('green') +export const themeInfo: LX.ThemeInfo = { + themes: [], + userThemes: [], + dataPath: '', +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/leaderboard/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/leaderboard/action.ts new file mode 100644 index 000000000..d07e3050e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/leaderboard/action.ts @@ -0,0 +1,126 @@ +// import { getLeaderboardSetting } from '@renderer/utils/data' +import { deduplicationList, toNewMusicInfo } from '@renderer/utils' +import musicSdk from '@renderer/utils/musicSdk' +import { markRaw, markRawList } from '@common/utils/vueTools' +import { boards, type Board, listDetailInfo, type ListDetailInfo } from './state' + +const cache = new Map() + +export const setBoard = (board: Board, source: LX.OnlineSource) => { + boards[source] = markRaw(board) +} + +export const setListDetail = (result: ListDetailInfo, id: string, page: number) => { + listDetailInfo.list = markRaw([...result.list]) + listDetailInfo.id = id + listDetailInfo.source = result.source + if (page == 1 || (result.total && result.list.length)) listDetailInfo.total = result.total + else listDetailInfo.total = result.limit * page + listDetailInfo.limit = result.limit + listDetailInfo.page = page + + if (result.list.length) listDetailInfo.noItemLabel = '' + else if (page == 1) listDetailInfo.noItemLabel = window.i18n.t('no_item') +} +export const clearListDetail = () => { + listDetailInfo.list = [] + listDetailInfo.id = '' + listDetailInfo.source = null + listDetailInfo.total = 0 + listDetailInfo.limit = 30 + listDetailInfo.page = 1 + listDetailInfo.key = null + listDetailInfo.noItemLabel = '' +} + +export const getBoardsList = async(source: LX.OnlineSource) => { + // const source = (await getLeaderboardSetting()).source as LX.OnlineSource + return musicSdk[source]?.leaderboard.getBoards() as Promise +} + +/** + * 获取排行榜内单页歌曲 + * @param id 排行榜id {souce}__{id} + * @param isRefresh 是否跳过缓存 + * @returns + */ +export const getListDetail = async(id: string, page: number, isRefresh = false): Promise => { + // let [source, bangId] = tabId.split('__') + // if (!bangId) return + let key = `${id}__${page}` + + if (!isRefresh && cache.has(key)) return cache.get(key) + + const [source, bangId] = id.split('__') as [LX.OnlineSource, string] + + return musicSdk[source]?.leaderboard?.getList(bangId, page).then((result: ListDetailInfo) => { + result.list = markRawList(deduplicationList(result.list.map(m => toNewMusicInfo(m)) as LX.Music.MusicInfoOnline[])) + cache.set(key, result) + return result + }) +} + + +/** + * 获取排行榜内全部歌曲 + * @param id 排行榜id {souce}__{id} + * @param isRefresh 是否跳过缓存 + * @returns + */ +export const getListDetailAll = async(id: string, isRefresh = false): Promise => { + const [source, bangId] = id.split('__') as [LX.OnlineSource, string] + // console.log(source, id) + // eslint-disable-next-line @typescript-eslint/promise-function-async + const loadData = async(id: string, page: number): Promise => { + let key = `${source}__${id}__${page}` + if (!isRefresh && cache.has(key)) return cache.get(key) + + return musicSdk[source]?.leaderboard.getList(id, page).then((result: ListDetailInfo) => { + result.list = markRawList(deduplicationList(result.list.map(m => toNewMusicInfo(m)) as LX.Music.MusicInfoOnline[])) + cache.set(key, result) + return result + }) ?? Promise.reject(new Error('source not found' + source)) + } + // eslint-disable-next-line @typescript-eslint/promise-function-async + return loadData(bangId, 1).then((result: ListDetailInfo) => { + if (result.total <= result.limit) return result.list + + let maxPage = Math.ceil(result.total / result.limit) + // eslint-disable-next-line @typescript-eslint/promise-function-async + const loadDetail = (loadPage = 2): Promise => { + return loadPage == maxPage + ? loadData(bangId, loadPage).then((result: ListDetailInfo) => result.list) + // eslint-disable-next-line @typescript-eslint/promise-function-async + : loadData(bangId, loadPage).then((result1: ListDetailInfo) => loadDetail(++loadPage).then((result2: ListDetailInfo['list']) => [...result1.list, ...result2])) + } + return loadDetail().then(result2 => [...result.list, ...result2]) + }).then((list: ListDetailInfo['list']) => deduplicationList(list)) +} + + +/** + * 获取并设置排行榜内单页歌曲 + * @param id 排行榜id {souce}__{id} + * @param isRefresh 是否跳过缓存 + * @returns + */ +export const getAndSetListDetail = async(id: string, page: number, isRefresh = false) => { + // let [source, bangId] = tabId.split('__') + // if (!bangId) return + let key = `${id}__${page}` + + if (!isRefresh && listDetailInfo.key == key && listDetailInfo.list.length) return + + listDetailInfo.key = key + listDetailInfo.noItemLabel = window.i18n.t('list__loading') + + return getListDetail(id, page, isRefresh).then((result: ListDetailInfo) => { + if (key != listDetailInfo.key) return + setListDetail(result, id, page) + }).catch((error: any) => { + clearListDetail() + listDetailInfo.noItemLabel = window.i18n.t('list__load_failed') + console.log(error) + throw error + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/leaderboard/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/leaderboard/state.ts new file mode 100644 index 000000000..e8ac8934f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/leaderboard/state.ts @@ -0,0 +1,47 @@ +import { reactive, markRaw, shallowReactive } from '@common/utils/vueTools' +import music from '@renderer/utils/musicSdk' + +export type Source = LX.OnlineSource + +export const sources: LX.OnlineSource[] = markRaw([]) + +for (const source of music.sources) { + if (!music[source.id as LX.OnlineSource]?.leaderboard?.getBoards) continue + sources.push(source.id as LX.OnlineSource) +} + +export interface BoardItem { + id: string + name: string + bangid: string +} +export interface Board { + list: BoardItem[] + source: LX.OnlineSource +} +type Boards = Partial> + +export const boards = shallowReactive({}) + +export interface ListDetailInfo { + list: LX.Music.MusicInfoOnline[] + total: number + page: number + source: LX.OnlineSource | null + limit: number + key: string | null + id: string + noItemLabel: string +} + +export const listDetailInfo = reactive({ + list: [], + total: 0, + page: 1, + limit: 30, + key: null, + source: null, + id: '', + noItemLabel: '', +}) + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/action.ts new file mode 100644 index 000000000..a2f6f9954 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/action.ts @@ -0,0 +1,106 @@ +// import { } from '@renderer/utils/ipc' + +import { appSetting } from '@renderer/store/setting' +import { fetchingListStatus, listUpdateTimes, allMusicList, userLists, tempListMeta } from './state' +import { + registerListAction, + createUserList as createUserListAction, + addListMusics as addListMusicsAction, + moveListMusics as moveListMusicsAction, + overwriteListMusics, +} from '@renderer/store/list/listManage' +import { toRaw } from '@common/utils/vueTools' +import { LIST_IDS } from '@common/constants' + +export const registerAction = (onListChanged: (listIds: string[]) => void) => { + return registerListAction(appSetting, onListChanged) +} + +/** + * 从缓存获取列表内歌曲,前提是知道列表之前已被获取过,否则返回空数组 + * @param listId 列表ID + * @returns + */ +export const getListMusicsFromCache = (listId: string | null): LX.Music.MusicInfo[] => { + if (!listId) return [] + if (allMusicList.has(listId)) return allMusicList.get(listId) as LX.Music.MusicInfo[] + return [] +} + +export const setFetchingListStatus = (id: string, status: boolean) => { + fetchingListStatus[id] = status +} + +export const setUpdateTime = (id: string, time: string) => { + listUpdateTimes[id] = time +} + +export const addListMusics = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType?: LX.AddMusicLocationType) => { + return addListMusicsAction({ + id, + musicInfos: toRaw(musicInfos), + addMusicLocationType: addMusicLocationType ?? appSetting['list.addMusicLocationType'], + }) +} + +export const moveListMusics = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType?: LX.AddMusicLocationType) => { + return moveListMusicsAction({ + fromId, + toId, + musicInfos: toRaw(musicInfos), + addMusicLocationType: addMusicLocationType ?? appSetting['list.addMusicLocationType'], + }) +} + +export const createUserList = async({ name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, position = -1 }: { + name?: string + id?: string + list?: LX.Music.MusicInfo[] + source?: LX.OnlineSource + sourceListId?: string + position?: number +}) => { + await createUserListAction({ + position: position < 0 ? userLists.length : position, + listInfos: [ + { + id, + name: name ?? 'list', + source, + sourceListId, + locationUpdateTime: position < 0 ? null : Date.now(), + }, + ], + }) + if (list) await addListMusics(id, list) +} + + +export const setTempList = async(id: string, list: LX.Music.MusicInfoOnline[]) => { + tempListMeta.id = id + await overwriteListMusics({ + listId: LIST_IDS.TEMP, + musicInfos: list, + }) +} + +export { + addListMusicsAction, + moveListMusicsAction, +} + +export { + getUserLists, + removeUserList, + updateUserList, + updateUserListPosition, + getListMusics, + removeListMusics, + updateListMusics, + updateListMusicsPosition, + overwriteListMusics, + clearListMusics, + overwriteListFull, + checkListExistMusic, + getMusicExistListIds, +} from '@renderer/store/list/listManage' diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/action.ts new file mode 100644 index 000000000..6a84ddd88 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/action.ts @@ -0,0 +1,312 @@ +import { markRaw, markRawList, toRaw } from '@common/utils/vueTools' +import { + allMusicList, + defaultList, + loveList, + tempList, + userLists, +} from './state' +import { overwriteListPosition, overwriteListUpdateInfo, removeListPosition, removeListUpdateInfo } from '@renderer/utils/data' +import { LIST_IDS } from '@common/constants' +import { arrPush, arrUnshift } from '@common/utils/common' + +export const setUserLists = (lists: LX.List.UserListInfo[]) => { + userLists.splice(0, userLists.length, ...lists) + return userLists +} + +export const setMusicList = (listId: string, musicList: LX.Music.MusicInfo[]) => { + const list = markRawList(musicList) + allMusicList.set(listId, list) + return list +} + +const overwriteMusicList = (id: string, list: LX.Music.MusicInfo[]) => { + // console.log(id, list) + markRawList(list) + let targetList = allMusicList.get(id) + if (targetList) { + targetList.splice(0, targetList.length) + arrPush(targetList, list) + } else { + allMusicList.set(id, list) + } +} +const removeMusicList = (id: string) => { + allMusicList.delete(id) +} + +const createUserList = ({ + name, + id, + source, + sourceListId, + locationUpdateTime, +}: LX.List.UserListInfo, position: number) => { + if (position < 0 || position >= userLists.length) { + userLists.push({ + name, + id, + source, + sourceListId, + locationUpdateTime, + }) + } else { + userLists.splice(position, 0, { + name, + id, + source, + sourceListId, + locationUpdateTime, + }) + } +} + +const updateList = ({ + name, + id, + source, + sourceListId, + meta, + locationUpdateTime, +}: LX.List.UserListInfo & { meta?: { id?: string } }) => { + let targetList + switch (id) { + case defaultList.id: + case loveList.id: + break + case tempList.id: + tempList.meta = meta ?? {} + break + default: + targetList = userLists.find(l => l.id == id) + if (!targetList) return + targetList.name = name + targetList.source = source + targetList.sourceListId = sourceListId + targetList.locationUpdateTime = locationUpdateTime + break + } +} + +const removeUserList = (id: string) => { + const index = userLists.findIndex(l => l.id == id) + if (index < 0) return + userLists.splice(index, 1) + // removeMusicList(id) +} + +const overwriteUserList = (lists: LX.List.UserListInfo[]) => { + userLists.splice(0, userLists.length, ...lists) +} + + +// const sendMyListUpdateEvent = (ids: string[]) => { +// window.app_event.myListUpdate(ids) +// } + + +export const listDataOverwrite = ({ defaultList, loveList, userList, tempList }: MakeOptional): string[] => { + const updatedListIds: string[] = [] + const newUserIds: string[] = [] + const newUserListInfos = userList.map(({ list, ...listInfo }) => { + newUserIds.push(listInfo.id) + if (allMusicList.has(listInfo.id)) { + overwriteMusicList(listInfo.id, list) + updatedListIds.push(listInfo.id) + } + return listInfo + }) + for (const list of userLists) { + if (!allMusicList.has(list.id) || newUserIds.includes(list.id)) continue + removeMusicList(list.id) + updatedListIds.push(list.id) + } + overwriteUserList(newUserListInfos) + + if (allMusicList.has(LIST_IDS.DEFAULT)) { + overwriteMusicList(LIST_IDS.DEFAULT, defaultList) + updatedListIds.push(LIST_IDS.DEFAULT) + } + + overwriteMusicList(LIST_IDS.LOVE, loveList) + updatedListIds.push(LIST_IDS.LOVE) + + if (tempList && allMusicList.has(LIST_IDS.TEMP)) { + overwriteMusicList(LIST_IDS.TEMP, tempList) + updatedListIds.push(LIST_IDS.TEMP) + } + const newIds = [LIST_IDS.DEFAULT, LIST_IDS.LOVE, ...userList.map(l => l.id)] + if (tempList) newIds.push(LIST_IDS.TEMP) + void overwriteListPosition(newIds) + void overwriteListUpdateInfo(newIds) + return updatedListIds +} + +export const userListCreate = ({ name, id, source, sourceListId, position, locationUpdateTime }: { + name: string + id: string + source?: LX.OnlineSource + sourceListId?: string + position: number + locationUpdateTime: number | null +}) => { + if (userLists.some(item => item.id == id)) return + const newList: LX.List.UserListInfo = { + name, + id, + source, + sourceListId, + locationUpdateTime, + } + createUserList(newList, position) +} + +export const userListsRemove = (ids: string[]) => { + const changedIds = [] + for (const id of ids) { + removeUserList(id) + void removeListPosition(id) + void removeListUpdateInfo(id) + if (!allMusicList.has(id)) continue + removeMusicList(id) + changedIds.push(id) + } + + return changedIds +} + +export const userListsUpdate = (listInfos: LX.List.UserListInfo[]) => { + for (const info of listInfos) { + updateList(info) + } +} + +export const userListsUpdatePosition = (position: number, ids: string[]) => { + const newUserLists = [...userLists] + + // console.log(position, ids) + + const updateLists: LX.List.UserListInfo[] = [] + + // const targetItem = list[position] + const map = new Map() + for (const item of newUserLists) map.set(item.id, item) + for (const id of ids) { + const listInfo = map.get(id)! + listInfo.locationUpdateTime = Date.now() + updateLists.push(listInfo) + map.delete(id) + } + newUserLists.splice(0, newUserLists.length, ...newUserLists.filter(mInfo => map.has(mInfo.id))) + newUserLists.splice(Math.min(position, newUserLists.length), 0, ...updateLists) + + setUserLists(newUserLists) +} + +export const listMusicOverwrite = (listId: string, musicInfos: LX.Music.MusicInfo[]): string[] => { + const isExist = allMusicList.has(listId) + overwriteMusicList(listId, musicInfos) + return isExist || listId == loveList.id ? [listId] : [] +} + +export const listMusicClear = (ids: string[]): string[] => { + const changedIds: string[] = [] + for (const id of ids) { + const list = allMusicList.get(id) + if (!list?.length) continue + overwriteMusicList(id, []) + changedIds.push(id) + } + return changedIds +} + +export const listMusicAdd = (id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType): string[] => { + const targetList = allMusicList.get(id) + if (!targetList) return id == loveList.id ? [id] : [] + + const listSet = new Set() + for (const item of targetList) listSet.add(item.id) + musicInfos = musicInfos.filter(item => { + if (listSet.has(item.id)) return false + markRaw(item) + listSet.add(item.id) + return true + }) + switch (addMusicLocationType) { + case 'top': + arrUnshift(targetList, musicInfos) + break + case 'bottom': + default: + arrPush(targetList, musicInfos) + break + } + + return [id] +} + +export const listMusicMove = (fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType): string[] => { + return [ + ...listMusicRemove(fromId, musicInfos.map(musicInfo => musicInfo.id)), + ...listMusicAdd(toId, musicInfos, addMusicLocationType), + ] +} + +export const listMusicRemove = (listId: string, ids: string[]): string[] => { + let targetList = allMusicList.get(listId) + if (!targetList) return listId == loveList.id ? [listId] : [] + + const idsSet = new Set(ids) + const newList = targetList.filter(mInfo => !idsSet.has(mInfo.id)) + targetList.splice(0, targetList.length) + arrPush(targetList, newList) + + return [listId] +} + +export const listMusicUpdateInfo = (musicInfos: LX.List.ListActionMusicUpdate): string[] => { + const updateListIds = new Set() + for (const { id, musicInfo } of musicInfos) { + const targetList = allMusicList.get(id) + if (!targetList) continue + const index = targetList.findIndex(l => l.id == musicInfo.id) + if (index < 0) continue + const info: LX.Music.MusicInfo = { ...targetList[index] } + Object.assign(info, { + name: musicInfo.name, + singer: musicInfo.singer, + source: musicInfo.source, + interval: musicInfo.interval, + meta: musicInfo.meta, + }) + targetList.splice(index, 1, markRaw(info)) + updateListIds.add(id) + } + return Array.from(updateListIds) +} + +export const listMusicUpdatePosition = async(listId: string, position: number, ids: string[]): Promise => { + let targetList = allMusicList.get(listId) + if (!targetList) return listId == loveList.id ? [listId] : [] + + + // const infos = Array(ids.length) + // for (let i = targetList.length; i--;) { + // const item = targetList[i] + // const index = ids.indexOf(item.id) + // if (index < 0) continue + // infos.splice(index, 1, targetList.splice(i, 1)[0]) + // } + // targetList.splice(Math.min(position, targetList.length - 1), 0, ...infos) + + // console.time('ts') + + const list = await window.lx.worker.main.createSortedList(toRaw(targetList), position, ids) + markRawList(list) + targetList.splice(0, targetList.length) + arrPush(targetList, list) + + // console.timeEnd('ts') + return [listId] +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/index.ts new file mode 100644 index 000000000..f409b996e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/index.ts @@ -0,0 +1,2 @@ +export * from './rendererListManage' +export * from './state' diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/rendererListManage.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/rendererListManage.ts new file mode 100644 index 000000000..d0542b802 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/rendererListManage.ts @@ -0,0 +1,253 @@ +import { toRaw } from '@common/utils/vueTools' +import { rendererInvoke, rendererOff, rendererOn } from '@common/rendererIpc' +import { PLAYER_EVENT_NAME } from '@common/ipcNames' +import { + userListCreate, + listDataOverwrite, + userListsRemove, + userListsUpdate, + userListsUpdatePosition, + listMusicAdd, + listMusicMove, + listMusicRemove, + listMusicOverwrite, + listMusicUpdateInfo, + listMusicUpdatePosition, + setMusicList, + setUserLists, + listMusicClear, +} from './action' +import { allMusicList } from './state' + +/** + * 获取用户列表 + * @returns 所有用户列表 + */ +export const getUserLists = async() => { + const lists = await rendererInvoke(PLAYER_EVENT_NAME.list_get) + return setUserLists(lists) +} + +/** + * 添加用户列表 + * @param data + */ +export const createUserList = async(data: LX.List.ListActionAdd) => { + data.listInfos = data.listInfos.map(info => toRaw(info)) + await rendererInvoke(PLAYER_EVENT_NAME.list_add, data) +} + +/** + * 移除用户列表及列表内歌曲 + * @param data + */ +export const removeUserList = async(data: LX.List.ListActionRemove) => { + await rendererInvoke(PLAYER_EVENT_NAME.list_remove, data) +} + +/** + * 更新用户列表 + * @param data + */ +export const updateUserList = async(data: LX.List.ListActionUpdate) => { + data = data.map(info => toRaw(info)) + await rendererInvoke(PLAYER_EVENT_NAME.list_update, data) +} + +/** + * 批量移动用户列表位置 + * @param data + */ +export const updateUserListPosition = async(data: LX.List.ListActionUpdatePosition) => { + await rendererInvoke(PLAYER_EVENT_NAME.list_update_position, data) +} + +/** + * 获取列表内的歌曲 + * @param listId + */ +export const getListMusics = async(listId: string | null): Promise => { + if (!listId) return [] + if (allMusicList.has(listId)) return allMusicList.get(listId)! + const list = await rendererInvoke(PLAYER_EVENT_NAME.list_music_get, listId) + return setMusicList(listId, list) +} + +/** + * 批量添加歌曲到列表 + * @param data + */ +export const addListMusics = async(data: LX.List.ListActionMusicAdd) => { + await rendererInvoke(PLAYER_EVENT_NAME.list_music_add, data) +} + +/** + * 跨列表批量移动歌曲 + * @param data + */ +export const moveListMusics = async(data: LX.List.ListActionMusicMove) => { + await rendererInvoke(PLAYER_EVENT_NAME.list_music_move, data) +} + +/** + * 批量删除列表内歌曲 + * @param data + */ +export const removeListMusics = async(data: LX.List.ListActionMusicRemove) => { + await rendererInvoke(PLAYER_EVENT_NAME.list_music_remove, data) +} + +/** + * 批量更新列表内歌曲 + * @param data + */ +export const updateListMusics = async(data: LX.List.ListActionMusicUpdate) => { + await rendererInvoke(PLAYER_EVENT_NAME.list_music_update, data) +} + +/** + * 批量移动列表内歌曲的位置 + * @param data + */ +export const updateListMusicsPosition = async(data: LX.List.ListActionMusicUpdatePosition) => { + await rendererInvoke(PLAYER_EVENT_NAME.list_music_update_position, data) +} + +/** + * 覆盖列表内的歌曲 + * @param data + */ +export const overwriteListMusics = async(data: LX.List.ListActionMusicOverwrite) => { + await rendererInvoke(PLAYER_EVENT_NAME.list_music_overwrite, data) +} + +/** + * 清空列表内的歌曲 + * @param ids + */ +export const clearListMusics = async(ids: LX.List.ListActionMusicClear) => { + await rendererInvoke(PLAYER_EVENT_NAME.list_music_clear, ids) +} + +/** + * 覆盖全部列表数据 + * @param data + */ +export const overwriteListFull = async(data: LX.List.ListActionDataOverwrite) => { + data.defaultList = toRaw(data.defaultList) + data.loveList = toRaw(data.loveList) + if (data.tempList) { + data.tempList = toRaw(data.tempList) + } + data.userList = data.userList.map(info => { + return { + ...info, + list: toRaw(info.list), + } + }) + + await rendererInvoke(PLAYER_EVENT_NAME.list_data_overwire, data) +} + +/** + * 检查音乐是否存在列表中 + * @param listId + * @param musicInfoId + */ +export const checkListExistMusic = async(listId: string, musicInfoId: string): Promise => { + return rendererInvoke(PLAYER_EVENT_NAME.list_music_check_exist, { listId, musicInfoId }) +} + +/** + * 获取所有存在该音乐的列表id + * @param musicInfoId + */ +export const getMusicExistListIds = async(musicInfoId: string): Promise => { + return rendererInvoke(PLAYER_EVENT_NAME.list_music_get_list_ids, musicInfoId) +} + + +const noop = () => {} + + +export const registerListAction = (appSetting: LX.AppSetting, onListChanged: (listIds: string[]) => void = noop) => { + const list_data_overwrite = ({ params: datas }: LX.IpcRendererEventParams) => { + const updatedListIds = listDataOverwrite(datas) + if (updatedListIds.length) onListChanged(updatedListIds) + } + const list_create = ({ params: { position, listInfos } }: LX.IpcRendererEventParams) => { + for (const list of listInfos) { + userListCreate({ ...list, position }) + } + } + const list_remove = ({ params: ids }: LX.IpcRendererEventParams) => { + const updatedListIds = userListsRemove(ids) + if (updatedListIds.length) onListChanged(updatedListIds) + } + const list_update = ({ params: listInfos }: LX.IpcRendererEventParams) => { + userListsUpdate(listInfos) + } + const list_update_position = ({ params: { position, ids } }: LX.IpcRendererEventParams) => { + userListsUpdatePosition(position, ids) + } + const list_music_add = ({ params: { id, musicInfos, addMusicLocationType } }: LX.IpcRendererEventParams) => { + addMusicLocationType ??= appSetting['list.addMusicLocationType'] + const updatedListIds = listMusicAdd(id, musicInfos, addMusicLocationType) + if (updatedListIds.length) onListChanged(updatedListIds) + } + const list_music_move = ({ params: { fromId, toId, musicInfos, addMusicLocationType } }: LX.IpcRendererEventParams) => { + addMusicLocationType ??= appSetting['list.addMusicLocationType'] + const updatedListIds = listMusicMove(fromId, toId, musicInfos, addMusicLocationType) + if (updatedListIds.length) onListChanged(updatedListIds) + } + const list_music_remove = ({ params: { listId, ids } }: LX.IpcRendererEventParams) => { + // console.log(listId, ids) + const updatedListIds = listMusicRemove(listId, ids) + if (updatedListIds.length) onListChanged(updatedListIds) + } + const list_music_update = ({ params: musicInfos }: LX.IpcRendererEventParams) => { + const updatedListIds = listMusicUpdateInfo(musicInfos) + if (updatedListIds.length) onListChanged(updatedListIds) + } + const list_music_update_position = ({ params: { listId, position, ids } }: LX.IpcRendererEventParams) => { + void listMusicUpdatePosition(listId, position, ids).then(updatedListIds => { + if (updatedListIds.length) onListChanged(updatedListIds) + }) + } + const list_music_overwrite = ({ params: { listId, musicInfos } }: LX.IpcRendererEventParams) => { + const updatedListIds = listMusicOverwrite(listId, musicInfos) + if (updatedListIds.length) onListChanged(updatedListIds) + } + const list_music_clear = ({ params: ids }: LX.IpcRendererEventParams) => { + const updatedListIds = listMusicClear(ids) + if (updatedListIds.length) onListChanged(updatedListIds) + } + + rendererOn(PLAYER_EVENT_NAME.list_data_overwire, list_data_overwrite) + rendererOn(PLAYER_EVENT_NAME.list_add, list_create) + rendererOn(PLAYER_EVENT_NAME.list_remove, list_remove) + rendererOn(PLAYER_EVENT_NAME.list_update, list_update) + rendererOn(PLAYER_EVENT_NAME.list_update_position, list_update_position) + rendererOn(PLAYER_EVENT_NAME.list_music_add, list_music_add) + rendererOn(PLAYER_EVENT_NAME.list_music_move, list_music_move) + rendererOn(PLAYER_EVENT_NAME.list_music_remove, list_music_remove) + rendererOn(PLAYER_EVENT_NAME.list_music_update, list_music_update) + rendererOn(PLAYER_EVENT_NAME.list_music_update_position, list_music_update_position) + rendererOn(PLAYER_EVENT_NAME.list_music_overwrite, list_music_overwrite) + rendererOn(PLAYER_EVENT_NAME.list_music_clear, list_music_clear) + + return () => { + rendererOff(PLAYER_EVENT_NAME.list_data_overwire, list_data_overwrite) + rendererOff(PLAYER_EVENT_NAME.list_add, list_create) + rendererOff(PLAYER_EVENT_NAME.list_remove, list_remove) + rendererOff(PLAYER_EVENT_NAME.list_update, list_update) + rendererOff(PLAYER_EVENT_NAME.list_update_position, list_update_position) + rendererOff(PLAYER_EVENT_NAME.list_music_add, list_music_add) + rendererOff(PLAYER_EVENT_NAME.list_music_move, list_music_move) + rendererOff(PLAYER_EVENT_NAME.list_music_remove, list_music_remove) + rendererOff(PLAYER_EVENT_NAME.list_music_update, list_music_update) + rendererOff(PLAYER_EVENT_NAME.list_music_update_position, list_music_update_position) + rendererOff(PLAYER_EVENT_NAME.list_music_overwrite, list_music_overwrite) + rendererOff(PLAYER_EVENT_NAME.list_music_clear, list_music_clear) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/state.ts new file mode 100644 index 000000000..59d8e258a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/listManage/state.ts @@ -0,0 +1,23 @@ +import { LIST_IDS } from '@common/constants' +import { markRaw, reactive } from '@common/utils/vueTools' + +export const allMusicList: Map = markRaw(new Map()) + +export const defaultList = markRaw({ + id: LIST_IDS.DEFAULT, + name: 'list__name_default', + // name: '试听列表', +}) + +export const loveList = markRaw({ + id: LIST_IDS.LOVE, + name: 'list__name_love', + // name: '我的收藏', +}) +export const tempList = markRaw({ + id: LIST_IDS.TEMP, + name: '临时列表', + meta: {}, +}) + +export const userLists: LX.List.UserListInfo[] = reactive([]) diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/state.ts new file mode 100644 index 000000000..53b046236 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/state.ts @@ -0,0 +1,43 @@ +import { reactive } from '@common/utils/vueTools' + +export { + allMusicList, + defaultList, + loveList, + tempList, + userLists, +} from '@renderer/store/list/listManage' +// import { reactive, ref, markRaw, Ref } from '@common/utils/vueTools' + +// // const TEMP_LIST = 'TEMP_LIST' + +// export const isInitedList: Ref = ref(false) + +// export const allList: Map = window.lxData.allList = markRaw(new Map()) + +// export const defaultList: Omit = reactive({ +// id: 'default', +// name: '试听列表', +// }) + +// export const loveList: Omit = reactive({ +// id: 'love', +// name: '我的收藏', +// }) + +// export const tempList: Omit = reactive({ +// id: 'temp', +// name: '临时列表', +// meta: {}, +// }) + +export const tempListMeta = { + id: '', +} + + +// export const userLists: LX.List.UserListInfo[] = window.lxData.userLists = reactive([]) + +export const fetchingListStatus = reactive>({}) + +export const listUpdateTimes = reactive>({}) diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/syncSourceList.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/syncSourceList.ts new file mode 100644 index 000000000..ad6ac65bd --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/list/syncSourceList.ts @@ -0,0 +1,31 @@ +import { setListUpdateTime } from '@renderer/utils/data' +import { setFetchingListStatus, overwriteListMusics, setUpdateTime } from './action' +import { getListDetailAll } from '@renderer/store/songList/action' +import { getListDetailAll as getBoardListAll } from '@renderer/store/leaderboard/action' +import { dateFormat } from '@common/utils/common' + +const fetchList = async(id: string, source: LX.OnlineSource, sourceListId: string) => { + setFetchingListStatus(id, true) + + let promise + if (/^board__/.test(sourceListId)) { + const id = sourceListId.replace(/^board__/, '') + promise = id ? getBoardListAll(id, true) : Promise.reject(new Error('id not defined: ' + sourceListId)) + } else { + promise = getListDetailAll(sourceListId, source, true) + } + return promise.finally(() => { + setFetchingListStatus(id, false) + }) +} + +export default async(targetListInfo: LX.List.UserListInfo) => { + // console.log(targetListInfo) + if (!targetListInfo.source || !targetListInfo.sourceListId) return + const list = await fetchList(targetListInfo.id, targetListInfo.source, targetListInfo.sourceListId) + // console.log(list) + void overwriteListMusics({ listId: targetListInfo.id, musicInfos: list }) + const now = Date.now() + void setListUpdateTime(targetListInfo.id, now) + setUpdateTime(targetListInfo.id, dateFormat(now)) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/action.ts new file mode 100644 index 000000000..87977fc1a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/action.ts @@ -0,0 +1,253 @@ +// import { reactive, ref, shallowRef } from '@common/utils/vueTools' +import { + type PlayerMusicInfo, + musicInfo, + isPlay, + status, + statusText, + isShowPlayerDetail, + isShowPlayComment, + isShowLrcSelectContent, + playInfo, + playMusicInfo, + playedList, + tempPlayList, +} from './state' +import { getListMusicsFromCache } from '@renderer/store/list/action' +import { downloadList } from '@renderer/store/download/state' +import { setProgress } from './playProgress' +import { playNext } from '@renderer/core/player' +import { LIST_IDS } from '@common/constants' +import { toRaw } from '@common/utils/vueTools' +import { arrPush, arrUnshift } from '@common/utils/common' + + +type PlayerMusicInfoKeys = keyof typeof musicInfo + +const musicInfoKeys: PlayerMusicInfoKeys[] = Object.keys(musicInfo) as PlayerMusicInfoKeys[] + +export const setMusicInfo = (_musicInfo: Partial) => { + for (const key of musicInfoKeys) { + const val = _musicInfo[key] + if (val !== undefined) { + // @ts-expect-error + musicInfo[key] = val + } + } +} + +export const setPlay = (val: boolean) => { + isPlay.value = val +} + +export const setStatus = (val: string) => { + console.log('setStatus', val) + status.value = val +} + + +export const setStatusText = (val: string) => { + statusText.value = val +} + +export const setAllStatus = (val: string) => { + console.log('setAllStatus', val) + status.value = val + statusText.value = val +} + + +export const setShowPlayerDetail = (val: boolean) => { + isShowPlayerDetail.value = val +} + +export const setShowPlayComment = (val: boolean) => { + isShowPlayComment.value = val +} + +export const setShowPlayLrcSelectContentLrc = (val: boolean) => { + isShowLrcSelectContent.value = val +} + +export const setPlayListId = (listId: string | null) => { + playInfo.playerListId = listId +} + +export const getList = (listId: string | null): Array => { + return listId == LIST_IDS.DOWNLOAD ? downloadList : getListMusicsFromCache(listId) +} + +/** + * 更新播放位置 + * @returns 播放位置 + */ +export const updatePlayIndex = () => { + const indexInfo = getPlayIndex(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay) + // console.log(indexInfo) + playInfo.playIndex = indexInfo.playIndex + playInfo.playerPlayIndex = indexInfo.playerPlayIndex + + return indexInfo +} + +export const getPlayIndex = (listId: string | null, musicInfo: LX.Download.ListItem | LX.Music.MusicInfo | null, isTempPlay: boolean): { + playIndex: number + playerPlayIndex: number +} => { + const playerList = getList(playInfo.playerListId) + + // if (listIndex < 0) throw new Error('music info not found') + // playInfo.playIndex = listIndex + + let playIndex = -1 + let playerPlayIndex = -1 + if (playerList.length) { + playerPlayIndex = Math.min(playInfo.playerPlayIndex, playerList.length - 1) + } + + const list = getList(listId) + if (list.length && musicInfo) { + const currentId = musicInfo.id + playIndex = list.findIndex(m => m.id == currentId) + if (!isTempPlay) { + if (playIndex < 0) { + playerPlayIndex = playerPlayIndex < 1 ? (list.length - 1) : (playerPlayIndex - 1) + } else { + playerPlayIndex = playIndex + } + } + } + + return { + playIndex, + playerPlayIndex, + } +} + +export const resetPlayerMusicInfo = () => { + setMusicInfo({ + id: null, + pic: null, + lrc: null, + tlrc: null, + rlrc: null, + lxlrc: null, + rawlrc: null, + name: '', + singer: '', + album: '', + }) +} + +const setPlayerMusicInfo = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem | null) => { + if (musicInfo) { + setMusicInfo('progress' in musicInfo ? { + id: musicInfo.id, + pic: musicInfo.metadata.musicInfo.meta.picUrl, + name: musicInfo.metadata.musicInfo.name, + singer: musicInfo.metadata.musicInfo.singer, + album: musicInfo.metadata.musicInfo.meta.albumName ?? '', + lrc: null, + tlrc: null, + rlrc: null, + lxlrc: null, + rawlrc: null, + } : { + id: musicInfo.id, + pic: musicInfo.meta.picUrl, + name: musicInfo.name, + singer: musicInfo.singer, + album: musicInfo.meta.albumName ?? '', + lrc: null, + tlrc: null, + rlrc: null, + lxlrc: null, + rawlrc: null, + }) + } else resetPlayerMusicInfo() +} + +/** + * 设置当前播放歌曲的信息 + * @param listId 歌曲所属的列表id + * @param musicInfo 歌曲信息 + * @param isTempPlay 是否临时播放 + */ +export const setPlayMusicInfo = (listId: string | null, musicInfo: LX.Download.ListItem | LX.Music.MusicInfo | null, isTempPlay: boolean = false) => { + musicInfo = toRaw(musicInfo) + + playMusicInfo.listId = listId + playMusicInfo.musicInfo = musicInfo + playMusicInfo.isTempPlay = isTempPlay + + setPlayerMusicInfo(musicInfo) + + setProgress(0, 0) + + if (musicInfo == null) { + playInfo.playIndex = -1 + playInfo.playerListId = null + playInfo.playerPlayIndex = -1 + } else { + const { playIndex, playerPlayIndex } = getPlayIndex(listId, musicInfo, isTempPlay) + + playInfo.playIndex = playIndex + playInfo.playerPlayIndex = playerPlayIndex + window.app_event.musicToggled() + } +} + +/** + * 将歌曲添加到已播放列表 + * @param playMusicInfo playMusicInfo对象 + */ +export const addPlayedList = (playMusicInfo: LX.Player.PlayMusicInfo) => { + const id = playMusicInfo.musicInfo.id + if (playedList.some(m => m.musicInfo.id === id)) return + playedList.push(playMusicInfo) +} +/** + * 将歌曲从已播放列表移除 + * @param index 歌曲位置 + */ +export const removePlayedList = (index: number) => { + playedList.splice(index, 1) +} +/** + * 清空已播放列表 + */ +export const clearPlayedList = () => { + playedList.splice(0, playedList.length) +} + +/** + * 添加歌曲到稍后播放列表 + * @param list 歌曲列表 + */ +export const addTempPlayList = (list: LX.Player.TempPlayListItem[]) => { + const topList: Array> = [] + const bottomList = list.filter(({ isTop, ...musicInfo }) => { + if (isTop) { + topList.push(musicInfo) + return false + } + return true + }) + if (topList.length) arrUnshift(tempPlayList, topList.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true }))) + if (bottomList.length) arrPush(tempPlayList, bottomList.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true }))) + + if (!playMusicInfo.musicInfo) void playNext() +} +/** + * 从稍后播放列表移除歌曲 + * @param index 歌曲位置 + */ +export const removeTempPlayList = (index: number) => { + tempPlayList.splice(index, 1) +} +/** + * 清空稍后播放列表 + */ +export const clearTempPlayeList = () => { + tempPlayList.splice(0, tempPlayList.length) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/lyric.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/lyric.ts new file mode 100644 index 000000000..9a2135710 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/lyric.ts @@ -0,0 +1,37 @@ +import { reactive } from '@common/utils/vueTools' + +export interface Line { + text: string + time: number + extendedLyrics: string[] + dom_line: HTMLDivElement +} + +export const lyric = reactive<{ + lines: Line[] + text: string + line: number + offset: number // 歌词延迟 + tempOffset: number // 歌词临时延迟 +}>({ + lines: [], + text: '', + line: 0, + offset: 0, // 歌词延迟 + tempOffset: 0, // 歌词临时延迟 +}) + +export const setLines = (lines: Line[]) => { + if (!lines.length && !lyric.lines.length) return + lyric.lines = lines +} +export const setText = (text: string, line: number) => { + lyric.text = text + lyric.line = line +} +export const setOffset = (offset: number) => { + lyric.offset = offset +} +export const setTempOffset = (offset: number) => { + lyric.tempOffset = offset +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/playProgress.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/playProgress.ts new file mode 100644 index 000000000..9ac356263 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/playProgress.ts @@ -0,0 +1,27 @@ +import { reactive } from '@common/utils/vueTools' +import { formatPlayTime2 } from '@common/utils/common' + +export const playProgress = reactive({ + nowPlayTime: 0, + maxPlayTime: 0, + progress: 0, + nowPlayTimeStr: '00:00', + maxPlayTimeStr: '00:00', +}) + +export const setNowPlayTime = (time: number) => { + playProgress.nowPlayTime = time + playProgress.nowPlayTimeStr = formatPlayTime2(time) + playProgress.progress = playProgress.maxPlayTime ? time / playProgress.maxPlayTime : 0 +} + +export const setMaxplayTime = (time: number) => { + playProgress.maxPlayTime = time + playProgress.maxPlayTimeStr = formatPlayTime2(time) + playProgress.progress = time ? playProgress.nowPlayTime / time : 0 +} + +export const setProgress = (currentTime: number, totalTime: number) => { + setMaxplayTime(totalTime) + setNowPlayTime(currentTime) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/playbackRate.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/playbackRate.ts new file mode 100644 index 000000000..4907dc9d3 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/playbackRate.ts @@ -0,0 +1,8 @@ +import { ref } from '@common/utils/vueTools' + + +export const playbackRate = ref(1) + +export const setPlaybackRate = (num: number) => { + playbackRate.value = num +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/state.ts new file mode 100644 index 000000000..48aea238b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/state.ts @@ -0,0 +1,73 @@ +import { reactive, shallowReactive, ref } from '@common/utils/vueTools' + +export interface PlayerMusicInfo { + id: string | null + pic: string | null + lrc: string | null + tlrc: string | null + rlrc: string | null + lxlrc: string | null + rawlrc: string | null + // url: string | null + name: string + singer: string + album: string +} + +export const musicInfo = window.lxData.musicInfo = reactive({ + id: null, + pic: null, + lrc: null, + tlrc: null, + rlrc: null, + lxlrc: null, + rawlrc: null, + // url: null, + name: '', + singer: '', + album: '', +}) + +export const isPlay = ref(false) + +export const status = window.lxData.status = ref('') + +export const statusText = ref('') + +export const isShowPlayerDetail = ref(false) + +export const isShowPlayComment = ref(false) + +export const isShowLrcSelectContent = ref(false) + +export const playMusicInfo = shallowReactive<{ + /** + * 当前播放歌曲的列表 id + */ + musicInfo: LX.Player.PlayMusicInfo['musicInfo'] | null + /** + * 当前播放歌曲的列表 id + */ + listId: LX.Player.PlayMusicInfo['listId'] | null + /** + * 是否属于 “稍后播放” + */ + isTempPlay: boolean +}>({ + listId: null, + musicInfo: null, + isTempPlay: false, +}) +export const playInfo = shallowReactive({ + playIndex: -1, + playerListId: null, + playerPlayIndex: -1, +}) + + +export const playedList = window.lxData.playedList = shallowReactive([]) + +export const tempPlayList = shallowReactive([]) + +window.lxData.playInfo = playInfo +window.lxData.playMusicInfo = playMusicInfo diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/volume.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/volume.ts new file mode 100644 index 000000000..82946c1cf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/player/volume.ts @@ -0,0 +1,13 @@ +import { ref } from '@common/utils/vueTools' + + +export const volume = ref(0) +export const isMute = ref(false) + +export const setVolume = (num: number) => { + volume.value = num +} + +export const setMute = (flag: boolean) => { + isMute.value = flag +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/action.ts new file mode 100644 index 000000000..ad76b5b7e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/action.ts @@ -0,0 +1,44 @@ + +import { throttle } from '@common/utils/common' +import { toRaw } from '@common/utils/vueTools' +import { + getSearchHistoryList, + saveSearchHistoryList, +} from '@renderer/utils/ipc' +import { appSetting } from '../setting' +import { searchText, historyList } from './state' + + +export const setSearchText = (text: string) => { + searchText.value = text +} + +let isInitedSearchHistory = false +const saveSearchHistoryListThrottle = throttle((list: LX.List.SearchHistoryList) => { + saveSearchHistoryList(list) +}, 500) + + +export const getHistoryList = async() => { + if (isInitedSearchHistory || historyList.length) return + historyList.push(...(await getSearchHistoryList() ?? [])) + isInitedSearchHistory ||= true +} +export const addHistoryWord = async(word: string) => { + if (!appSetting['search.isShowHistorySearch']) return + if (!isInitedSearchHistory) await getHistoryList() + let index = historyList.indexOf(word) + if (index == 0) return + if (index > -1) historyList.splice(index, 1) + if (historyList.length >= 15) historyList.splice(14, historyList.length - 14) + historyList.unshift(word) + saveSearchHistoryListThrottle(toRaw(historyList)) +} +export const removeHistoryWord = (index: number) => { + historyList.splice(index, 1) + saveSearchHistoryListThrottle(toRaw(historyList)) +} +export const clearHistoryList = (id: string) => { + historyList.splice(0, historyList.length) + saveSearchHistoryList([]) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/music/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/music/action.ts new file mode 100644 index 000000000..9f4e53d9c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/music/action.ts @@ -0,0 +1,127 @@ +import { markRaw } from '@common/utils/vueTools' +import music from '@renderer/utils/musicSdk' +import { deduplicationList, toNewMusicInfo } from '@renderer/utils' +import { sortInsert, similar } from '@common/utils/common' + +import { sources, maxPages, listInfos } from './state' + +interface SearchResult { + list: LX.Music.MusicInfo[] + allPage: number + limit: number + total: number + source: LX.OnlineSource +} + + +/** + * 按搜索关键词重新排序列表 + * @param list 歌曲列表 + * @param keyword 搜索关键词 + * @returns 排序后的列表 + */ +const handleSortList = (list: LX.Music.MusicInfo[], keyword: string) => { + let arr: any[] = [] + for (const item of list) { + sortInsert(arr, { + num: similar(keyword, `${item.name} ${item.singer}`), + data: item, + }) + } + return arr.map(item => item.data).reverse() +} + + +const setLists = (results: SearchResult[], page: number, text: string): LX.Music.MusicInfo[] => { + let pages = [] + let totals = [] + let limit = 0 + let list = [] + for (const source of results) { + maxPages[source.source] = source.allPage + limit = Math.max(source.limit, limit) + if (source.allPage < page) continue + list.push(...source.list) + pages.push(source.allPage) + totals.push(source.total) + } + list = deduplicationList(list.map(s => markRaw(toNewMusicInfo(s)))) + let listInfo = listInfos.all + listInfo.maxPage = Math.max(0, ...pages) + const total = Math.max(0, ...totals) + if (page == 1 || (total && list.length)) listInfo.total = total + else listInfo.total = limit * page + // listInfo.limit = limit + listInfo.page = page + listInfo.list = handleSortList(list, text) + if (text && !list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item') + else listInfo.noItemLabel = '' + return listInfo.list +} + +const setList = (datas: SearchResult, page: number, text: string): LX.Music.MusicInfo[] => { + // console.log(datas.source, datas.list) + let listInfo = listInfos[datas.source]! + listInfo.list = deduplicationList(datas.list.map(s => markRaw(toNewMusicInfo(s)))) + if (page == 1 || (datas.total && datas.list.length)) listInfo.total = datas.total + else listInfo.total = datas.limit * page + listInfo.maxPage = datas.allPage + listInfo.page = page + listInfo.limit = datas.limit + if (text && !datas.list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item') + else listInfo.noItemLabel = '' + return listInfo.list +} + +export const resetListInfo = (sourceId: LX.OnlineSource | 'all'): [] => { + let listInfo = listInfos[sourceId] + if (!listInfo) return [] + listInfo.list = [] + listInfo.page = 0 + listInfo.maxPage = 0 + listInfo.total = 0 + listInfo.noItemLabel = '' + return [] +} + +export const search = async(text: string, page: number, sourceId: LX.OnlineSource | 'all'): Promise => { + const listInfo = listInfos[sourceId] + if (!text) return resetListInfo(sourceId) + const key = `${page}__${text}` + if (sourceId == 'all') { + listInfo!.noItemLabel = window.i18n.t('list__loading') + listInfo!.key = key + let task = [] + for (const source of sources) { + if (source == 'all') continue + task.push((music[source]?.musicSearch.search(text, page, listInfos.all.limit) ?? Promise.reject(new Error('source not found: ' + source))).catch((error: any) => { + console.log(error) + return { + allPage: 1, + limit: 30, + list: [], + source, + total: 0, + } + })) + } + return Promise.all(task).then((results: SearchResult[]) => { + if (key != listInfo!.key) return [] + return setLists(results, page, text) + }) + } else { + if (listInfo?.key == key && listInfo?.list.length) return listInfo?.list + listInfo!.noItemLabel = window.i18n.t('list__loading') + listInfo!.key = key + return music[sourceId].musicSearch.search(text, page, listInfo!.limit).then((data: SearchResult) => { + if (key != listInfo!.key) return [] + return setList(data, page, text) + }).catch((error: any) => { + resetListInfo(sourceId) + listInfo!.noItemLabel = window.i18n.t('list__load_failed') + console.log(error) + throw error + }) + } +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/music/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/music/index.ts new file mode 100644 index 000000000..72a08a85c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/music/index.ts @@ -0,0 +1,3 @@ + +export * from './action' +export * from './state' diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/music/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/music/state.ts new file mode 100644 index 000000000..0c08d50f2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/music/state.ts @@ -0,0 +1,48 @@ +import { reactive, markRaw } from '@common/utils/vueTools' +import music from '@renderer/utils/musicSdk' + +// import { deduplicationList } from '@common/utils/renderer' + +export declare interface ListInfo { + list: LX.Music.MusicInfo[] + total: number + page: number + maxPage: number + limit: number + key: string | null + noItemLabel: string +} + +interface ListInfos extends Partial> { + 'all': ListInfo +} + +export const sources: Array = markRaw([]) + +export const listInfos: ListInfos = markRaw({ + all: reactive({ + page: 1, + maxPage: 0, + limit: 30, + total: 0, + list: [], + key: null, + noItemLabel: '', + }), +}) +export const maxPages: Partial> = {} +for (const source of music.sources) { + if (!music[source.id as LX.OnlineSource]?.musicSearch) continue + sources.push(source.id as LX.OnlineSource) + listInfos[source.id as LX.OnlineSource] = reactive({ + page: 1, + maxPage: 0, + limit: 30, + total: 0, + list: [], + key: '', + noItemLabel: '', + }) + maxPages[source.id as LX.OnlineSource] = 0 +} +sources.push('all') diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/songlist/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/songlist/action.ts new file mode 100644 index 000000000..bc508eea5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/songlist/action.ts @@ -0,0 +1,128 @@ +import { markRawList } from '@common/utils/vueTools' +import music from '@renderer/utils/musicSdk' +import { sortInsert, similar } from '@common/utils/common' + +import type { ListInfoItem } from './state' +import { sources, maxPages, listInfos } from './state' + +interface SearchResult { + list: ListInfoItem[] + limit: number + total: number + source: LX.OnlineSource +} + + +/** + * 按搜索关键词重新排序列表 + * @param list 歌曲列表 + * @param keyword 搜索关键词 + * @returns 排序后的列表 + */ +const handleSortList = (list: ListInfoItem[], keyword: string) => { + let arr: any[] = [] + for (const item of list) { + sortInsert(arr, { + num: similar(keyword, item.name), + data: item, + }) + } + return arr.map(item => item.data).reverse() +} + + +let maxTotals: Partial> = { + +} +const setLists = (results: SearchResult[], page: number, text: string): ListInfoItem[] => { + let totals = [] + let limit = 0 + let list = [] + for (const source of results) { + list.push(...source.list) + totals.push(source.total) + maxTotals[source.source] = source.total + maxPages[source.source] = Math.ceil(source.total / source.limit) + limit = Math.max(source.limit, limit) + } + markRawList(list) + + let listInfo = listInfos.all + const total = Math.max(0, ...totals) + if (page == 1 || (total && list.length)) listInfo.total = total + else listInfo.total = limit * page + listInfo.page = page + listInfo.list = handleSortList(list, text) + if (text && !list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item') + else listInfo.noItemLabel = '' + return listInfo.list +} + +const setList = (datas: SearchResult, page: number, text: string): ListInfoItem[] => { + // console.log(datas.source, datas.list) + let listInfo = listInfos[datas.source]! + listInfo.list = markRawList(datas.list) + if (page == 1 || (datas.total && datas.list.length)) listInfo.total = datas.total + else listInfo.total = datas.limit * page + listInfo.page = page + listInfo.limit = datas.limit + if (text && !datas.list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item') + else listInfo.noItemLabel = '' + return listInfo.list +} + +export const resetListInfo = (sourceId: LX.OnlineSource | 'all'): [] => { + let listInfo = listInfos[sourceId] + if (!listInfo) return [] + listInfo.page = 1 + listInfo.limit = 20 + listInfo.total = 0 + listInfo.list = [] + listInfo.key = null + listInfo.noItemLabel = '' + listInfo.tagId = '' + listInfo.sortId = '' + return [] +} + +export const search = async(text: string, page: number, sourceId: LX.OnlineSource | 'all'): Promise => { + const listInfo = listInfos[sourceId]! + if (!text) return resetListInfo(sourceId) + const key = `${page}__${sourceId}__${text}` + if (listInfo.key == key && listInfo.list.length) return listInfo.list + if (sourceId == 'all') { + listInfo.noItemLabel = window.i18n.t('list__loading') + listInfo.key = key + let task = [] + for (const source of sources) { + if (source == 'all' || (page > 1 && page > (maxPages[source]!))) continue + task.push((music[source]?.songList.search(text, page, listInfos.all.limit) ?? Promise.reject(new Error('source not found: ' + source))).catch((error: any) => { + console.log(error) + return { + list: [], + total: 0, + limit: listInfos.all.limit, + source, + } + })) + } + return Promise.all(task).then((results: SearchResult[]) => { + if (key != listInfo.key) return [] + return setLists(results, page, text) + }) + } else { + if (listInfo?.key == key && listInfo?.list.length) return listInfo?.list + listInfo.noItemLabel = window.i18n.t('list__loading') + listInfo.key = key + return (music[sourceId]?.songList.search(text, page, listInfo.limit).then((data: SearchResult) => { + if (key != listInfo.key) return [] + return setList(data, page, text) + }) ?? Promise.reject(new Error('source not found: ' + sourceId))).catch((error: any) => { + resetListInfo(sourceId) + listInfo.noItemLabel = window.i18n.t('list__load_failed') + console.log(error) + throw error + }) + } +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/songlist/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/songlist/index.ts new file mode 100644 index 000000000..72a08a85c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/songlist/index.ts @@ -0,0 +1,3 @@ + +export * from './action' +export * from './state' diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/songlist/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/songlist/state.ts new file mode 100644 index 000000000..035590067 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/songlist/state.ts @@ -0,0 +1,48 @@ +import { reactive, markRaw } from '@common/utils/vueTools' +import music from '@renderer/utils/musicSdk' + +// import { deduplicationList } from '@common/utils/renderer' + +import { type ListInfo } from '@renderer/store/songList/state' + +export type { ListInfoItem } from '@renderer/store/songList/state' + +export const sources: Array = markRaw([]) + +export type SearchListInfo = Omit + + +interface ListInfos extends Partial> { + 'all': SearchListInfo +} + + +export const listInfos: ListInfos = markRaw({ + all: reactive({ + page: 1, + limit: 15, + total: 0, + list: [], + key: null, + noItemLabel: '', + tagId: '', + sortId: '', + }), +}) +export const maxPages: Partial> = {} +for (const source of music.sources) { + if (!music[source.id as LX.OnlineSource]?.songList?.search) continue + sources.push(source.id as LX.OnlineSource) + listInfos[source.id as LX.OnlineSource] = reactive({ + page: 1, + limit: 18, + total: 0, + list: [], + key: null, + noItemLabel: '', + tagId: '', + sortId: '', + }) + maxPages[source.id as LX.OnlineSource] = 0 +} +sources.push('all') diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/state.ts new file mode 100644 index 000000000..7389741ef --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/search/state.ts @@ -0,0 +1,9 @@ +import { ref, shallowReactive } from '@common/utils/vueTools' + + +export const searchText = ref('') + +export type onlineSource = LX.OnlineSource + + +export const historyList = shallowReactive([]) diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/setting.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/setting.ts new file mode 100644 index 000000000..57c635151 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/setting.ts @@ -0,0 +1,123 @@ +import { reactive, computed } from '@common/utils/vueTools' +import defaultSetting from '@common/defaultSetting' +import { updateSetting as saveSetting } from '@renderer/utils/ipc' + +export const appSetting = window.lxData.appSetting = reactive({ ...defaultSetting }) + +export const isShowAnimation = computed(() => { + return appSetting['common.isShowAnimation'] +}) + + +export const initSetting = (newSetting: LX.AppSetting) => { + mergeSetting(newSetting) +} + +export const mergeSetting = (newSetting: Partial) => { + for (const [key, value] of Object.entries(newSetting)) { + // @ts-expect-error + appSetting[key] = value + } +} + +export const updateSetting = window.lxData.updateSetting = (setting: Partial) => { + // console.warn(setting) + void saveSetting(setting) +} + +/** + * 保存是否同意协议 + * @param isAgreePact 是否同意协议 + */ +export const saveAgreePact = (isAgreePact: boolean) => { + updateSetting({ 'common.isAgreePact': isAgreePact }) +} + +/** + * 保存音频输出id + * @param id 媒体驱动id + */ +export const saveMediaDeviceId = (id: string) => { + updateSetting({ 'player.mediaDeviceId': id }) +} + +/** + * 保存音量大小 + * @param volume 音量 + */ +export const saveVolume = (volume: number) => { + updateSetting({ 'player.volume': volume }) +} + +/** + * 设置是否静音 + * @param isMute 是否静音 + */ +export const saveVolumeIsMute = (isMute: boolean) => { + updateSetting({ 'player.isMute': isMute }) +} + +/** + * 设置播放速率 + * @param rate 播放速率 + */ +export const savePlaybackRate = (rate: number) => { + updateSetting({ 'player.playbackRate': rate }) +} + + +/** + * 设置是否开启桌面歌词 + * @param enabled + */ +export const setVisibleDesktopLyric = (enabled: boolean) => { + updateSetting({ 'desktopLyric.enable': enabled }) +} + +/** + * 设置是否锁定桌面歌词 + * @param isLock + */ +export const setLockDesktopLyric = (isLock: boolean) => { + updateSetting({ 'desktopLyric.isLock': isLock }) +} + +/** + * 设置切歌模式 + * @param mode + */ +export const setTogglePlayMode = (mode: LX.AppSetting['player.togglePlayMethod']) => { + updateSetting({ 'player.togglePlayMethod': mode }) +} + +/** + * 设置API id + * @param sourceId + */ +export const setApiSource = (sourceId: string) => { + updateSetting({ 'common.apiSource': sourceId }) +} + +/** + * 设置播放详情页歌词字体大小 + * @param size 字体大小 + */ +export const setPlayDetailLyricFont = (size: number) => { + updateSetting({ 'playDetail.style.fontSize': size }) +} + +/** + * 设置播放详情页歌词对齐方式 + * @param align 对齐方式 + */ +export const setPlayDetailLyricAlign = (align: LX.AppSetting['playDetail.style.align']) => { + updateSetting({ 'playDetail.style.align': align }) +} + +/** + * 设置播放详情页音频可视化 + * @param enable 是否启用 + */ +export const setEnableAudioVisualization = (enable: boolean) => { + updateSetting({ 'player.audioVisualization': enable }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/songList/action.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/songList/action.ts new file mode 100644 index 000000000..ddd5847c2 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/songList/action.ts @@ -0,0 +1,213 @@ +// import { getSongListSetting } from '@renderer/utils/data' +import { deduplicationList, toNewMusicInfo } from '@renderer/utils' +import musicSdk from '@renderer/utils/musicSdk' +import { markRaw, markRawList } from '@common/utils/vueTools' +import { + tags, + listInfo, + listDetailInfo, + selectListInfo, + isVisibleListDetail, + openSongListInputInfo, +} from './state' +import type { + ListDetailInfo, + ListInfoItem, + ListInfo, + TagInfo, +} from './state' + +const cache = new Map() + +export const setTags = (tagInfo: TagInfo, source: LX.OnlineSource) => { + tags[source] = markRaw(tagInfo) +} + +export const clearList = () => { + listInfo.list = [] + listInfo.total = 0 + listInfo.noItemLabel = '' + listInfo.page = 1 + listInfo.key = '' +} + +export const setList = (result: ListInfo, tagId: string, sortId: string, page: number) => { + listInfo.list = markRaw([...result.list]) + if (page == 1 || (result.total && result.list.length)) listInfo.total = result.total + else listInfo.total = result.limit * page + listInfo.limit = result.limit + listInfo.page = page + listInfo.source = result.source + listInfo.tagId = tagId + listInfo.sortId = sortId + if (result.list.length) listInfo.noItemLabel = '' + else if (page == 1) listInfo.noItemLabel = window.i18n.t('no_item') +} +export const setListDetail = (result: ListDetailInfo, id: string, page: number) => { + listDetailInfo.list = markRaw([...result.list]) + listDetailInfo.id = id + listDetailInfo.source = result.source + if (page == 1 || (result.total && result.list.length)) listDetailInfo.total = result.total + else listDetailInfo.total = result.limit * page + listDetailInfo.limit = result.limit + listDetailInfo.page = page + listDetailInfo.info = markRaw({ ...result.info }) + if (result.list.length) listDetailInfo.noItemLabel = '' + else if (page == 1) listDetailInfo.noItemLabel = window.i18n.t('no_item') +} + +export const setSelectListInfo = (info: ListInfoItem) => { + selectListInfo.author = info.author + selectListInfo.desc = info.desc + selectListInfo.id = info.id + selectListInfo.img = info.img + selectListInfo.name = info.name + selectListInfo.play_count = info.play_count + selectListInfo.source = info.source +} +export const clearListDetail = () => { + listDetailInfo.list = [] + listDetailInfo.id = '' + listDetailInfo.source = 'kw' + listDetailInfo.total = 0 + listDetailInfo.limit = 30 + listDetailInfo.page = 1 + listDetailInfo.key = null + listDetailInfo.info = {} + listDetailInfo.noItemLabel = '' +} + +export const getTags = async(source: T) => { + return musicSdk[source]?.songList.getTags() as Promise> +} + + +/** + * 获取歌单列表 + * @param source 歌单源 + * @param tabId 类型id + * @param sortId 排序 + * @param page 页数 + * @param isRefresh 是否跳过缓存 + * @returns + */ +export const getAndSetList = async(source: LX.OnlineSource, tabId: string, sortId: string, page: number, isRefresh = false) => { + // let source = rootState.setting.songList.source + // let tabId = rootState.setting.songList.tagInfo.id + // let sortId = rootState.setting.songList.sortId + // console.log(sortId) + let key = `slist__${source}__${sortId}__${tabId}__${page}` + // if (state.list.list.length && state.list.key == key) return + if (!isRefresh) { + if (listInfo.key == key && listInfo.list.length) return + if (cache.has(key)) { + listInfo.key = key + setList(cache.get(key), tabId, sortId, page) + return + } + } + listInfo.noItemLabel = window.i18n.t('list__loading') + listInfo.key = key + // clearList() + return musicSdk[source]?.songList.getList(sortId, tabId, page).then((result: ListInfo) => { + cache.set(key, result) + if (key != listInfo.key) return + setList(result, tabId, sortId, page) + }).catch((error: any) => { + clearList() + listInfo.noItemLabel = window.i18n.t('list__load_failed') + console.log(error) + throw error + }) +} + +/** + * 获取歌单内单页歌曲 + * @param id 歌单id + * @param source 歌单源 + * @param isRefresh 是否跳过缓存 + * @returns + */ +export const getListDetail = async(id: string, source: LX.OnlineSource, page: number, isRefresh = false): Promise => { + let key = `sdetail__${source}__${id}__${page}` + if (!isRefresh && cache.has(key)) return cache.get(key) + + return musicSdk[source]?.songList.getListDetail(id, page).then((result: ListDetailInfo) => { + result.list = markRawList(deduplicationList(result.list.map(m => toNewMusicInfo(m)) as LX.Music.MusicInfoOnline[])) + cache.set(key, result) + return result + }) +} + +/** + * 获取歌单内全部歌曲 + * @param id 歌单id + * @param source 歌单源 + * @param isRefresh 是否跳过缓存 + * @returns + */ +export const getListDetailAll = async(id: string, source: LX.OnlineSource, isRefresh = false): Promise => { + // console.log(source, id) + // eslint-disable-next-line @typescript-eslint/promise-function-async + const loadData = (id: string, page: number): Promise => { + let key = `sdetail__${source}__${id}__${page}` + if (isRefresh && cache.has(key)) cache.delete(key) + return cache.has(key) + ? Promise.resolve(cache.get(key)) + : musicSdk[source]?.songList.getListDetail(id, page).then((result: ListDetailInfo) => { + result.list = markRawList(deduplicationList(result.list.map(m => toNewMusicInfo(m)) as LX.Music.MusicInfoOnline[])) + cache.set(key, result) + return result + }) ?? Promise.reject(new Error('source not found' + source)) + } + // eslint-disable-next-line @typescript-eslint/promise-function-async + return loadData(id, 1).then((result: ListDetailInfo) => { + if (result.total <= result.limit) return result.list + + let maxPage = Math.ceil(result.total / result.limit) + // eslint-disable-next-line @typescript-eslint/promise-function-async + const loadDetail = (loadPage = 2): Promise => { + return loadPage == maxPage + ? loadData(id, loadPage).then((result: ListDetailInfo) => result.list) + // eslint-disable-next-line @typescript-eslint/promise-function-async + : loadData(id, loadPage).then((result1: ListDetailInfo) => loadDetail(++loadPage).then((result2: ListDetailInfo['list']) => [...result1.list, ...result2])) + } + return loadDetail().then(result2 => [...result.list, ...result2]) + }).then((list: ListDetailInfo['list']) => deduplicationList(list)) +} + + +/** + * 获取并设置歌单内单页歌曲 + * @param id 歌单id + * @param source 歌单源 + * @param isRefresh 是否跳过缓存 + * @returns + */ +export const getAndSetListDetail = async(id: string, source: LX.OnlineSource, page: number, isRefresh = false) => { + let key = `sdetail__${source}__${id}__${page}` + + if (!isRefresh && listDetailInfo.key == key && listDetailInfo.list.length) return + + listDetailInfo.key = key + listDetailInfo.noItemLabel = window.i18n.t('list__loading') + + return getListDetail(id, source, page, isRefresh).then((result: ListDetailInfo) => { + if (key != listDetailInfo.key) return + setListDetail(result, id, page) + }).catch((error: any) => { + clearListDetail() + listDetailInfo.noItemLabel = window.i18n.t('list__load_failed') + console.log(error) + throw error + }) +} + +export const setVisibleListDetail = (visible: boolean) => { + isVisibleListDetail.value = visible +} + +export const setOpenSongListInputInfo = (text: string, source: string) => { + openSongListInputInfo.text = text + openSongListInputInfo.source = source +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/songList/state.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/songList/state.ts new file mode 100644 index 000000000..93cd67c8e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/songList/state.ts @@ -0,0 +1,125 @@ +import { reactive, markRaw, ref, shallowReactive } from '@common/utils/vueTools' +import music from '@renderer/utils/musicSdk' + +export interface SortInfo { + name: string + id: string +} + +export const sources: LX.OnlineSource[] = markRaw([]) +export const sortList = markRaw>>({}) + +for (const source of music.sources) { + const songList = music[source.id as LX.OnlineSource]?.songList + if (!songList) continue + sources.push(source.id as LX.OnlineSource) + sortList[source.id as LX.OnlineSource] = songList.sortList as SortInfo[] +} + +export interface TagInfoItem { + parent_id: string + parent_name: string + id: string + name: string + source: T +} +export interface TagInfoTypeItem { + name: string + list: Array> +} +export interface TagInfo { + tags: Array> + hotTag: Array> + source: Source +} + +type Tags = Partial> + +export const tags = shallowReactive({}) + + +export interface ListInfoItem { + play_count: string + id: string + author: string + name: string + time?: string + img: string + // grade: basic.favorcnt / 10, + desc: string | null + source: LX.OnlineSource + total?: string +} +export interface ListInfo { + list: ListInfoItem[] + total: number + page: number + limit: number + key: string | null + noItemLabel: string + source?: LX.OnlineSource + tagId: string + sortId: string +} + +export interface ListDetailInfo { + list: LX.Music.MusicInfoOnline[] + source: LX.OnlineSource + desc: string | null + total: number + page: number + limit: number + key: string | null + id: string + info: { + name?: string + img?: string + desc?: string + author?: string + play_count?: string + } + noItemLabel: string +} + +export const listInfo = reactive({ + list: [], + total: 0, + page: 1, + limit: 30, + key: null, + noItemLabel: '', + source: 'kw', + tagId: '', + sortId: '', +}) + +export const listDetailInfo = reactive({ + list: [], + id: '', + desc: null, + total: 0, + page: 1, + limit: 30, + key: null, + source: 'kw', + info: {}, + noItemLabel: '', +}) + +export const selectListInfo = markRaw({ + play_count: '', + id: '', + author: '', + name: '', + time: '', + img: '', + // grade: basic.favorcnt / 10, + desc: '', + source: 'kw', +}) + +export const isVisibleListDetail = ref(false) +export const openSongListInputInfo = markRaw({ + text: '', + source: '', +}) diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/soundEffect.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/soundEffect.ts new file mode 100644 index 000000000..f9a659814 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/soundEffect.ts @@ -0,0 +1,96 @@ +import { reactive, toRaw } from '@common/utils/vueTools' +import { + getUserSoundEffectConvolutionPresetList, + getUserSoundEffectEQPresetList, + // getUserSoundEffectPitchShifterPresetList, + saveUserSoundEffectConvolutionPresetList, + saveUserSoundEffectEQPresetList, + // saveUserSoundEffectPitchShifterPresetList, +} from '@renderer/utils/ipc' + +let userEqPresetList: LX.SoundEffect.EQPreset[] | null = null + +export const getUserEQPresetList = async() => { + if (userEqPresetList == null) { + // eslint-disable-next-line require-atomic-updates + userEqPresetList = reactive(await getUserSoundEffectEQPresetList()) + } + return userEqPresetList +} +export const saveUserEQPreset = async(preset: LX.SoundEffect.EQPreset) => { + if (userEqPresetList == null) { + // eslint-disable-next-line require-atomic-updates + userEqPresetList = reactive(await getUserSoundEffectEQPresetList()) + } + const target = userEqPresetList.find(p => p.id == preset.id) + if (target) Object.assign(target, preset) + else userEqPresetList.push(preset) + saveUserSoundEffectEQPresetList(toRaw(userEqPresetList)) +} +export const removeUserEQPreset = async(id: string) => { + if (userEqPresetList == null) { + // eslint-disable-next-line require-atomic-updates + userEqPresetList = reactive(await getUserSoundEffectEQPresetList()) + } + const index = userEqPresetList.findIndex(p => p.id == id) + if (index < 0) return + userEqPresetList.splice(index, 1) + saveUserSoundEffectEQPresetList(toRaw(userEqPresetList)) +} + + +let userConvolutionPresetList: LX.SoundEffect.ConvolutionPreset[] | null = null +export const getUserConvolutionPresetList = async() => { + if (userConvolutionPresetList == null) { + // eslint-disable-next-line require-atomic-updates + userConvolutionPresetList = reactive(await getUserSoundEffectConvolutionPresetList()) + } + return userConvolutionPresetList +} +export const saveUserConvolutionPreset = async(preset: LX.SoundEffect.ConvolutionPreset) => { + if (userConvolutionPresetList == null) { + // eslint-disable-next-line require-atomic-updates + userConvolutionPresetList = reactive(await getUserSoundEffectConvolutionPresetList()) + } + const target = userConvolutionPresetList.find(p => p.id == preset.id) + if (target) Object.assign(target, preset) + else userConvolutionPresetList.push(preset) + saveUserSoundEffectConvolutionPresetList(toRaw(userConvolutionPresetList)) +} +export const removeUserConvolutionPreset = async(id: string) => { + if (userConvolutionPresetList == null) { + // eslint-disable-next-line require-atomic-updates + userConvolutionPresetList = reactive(await getUserSoundEffectConvolutionPresetList()) + } + const index = userConvolutionPresetList.findIndex(p => p.id == id) + if (index < 0) return + userConvolutionPresetList.splice(index, 1) + saveUserSoundEffectConvolutionPresetList(toRaw(userConvolutionPresetList)) +} + + +// let userPitchShifterPresetList: LX.SoundEffect.PitchShifterPreset[] | null = null +// export const getUserPitchShifterPresetList = async() => { +// if (userEqPresetList == null) { +// userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList()) +// } +// return userPitchShifterPresetList +// } +// export const saveUserPitchShifterPreset = async(preset: LX.SoundEffect.PitchShifterPreset) => { +// if (userPitchShifterPresetList == null) { +// userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList()) +// } +// const target = userPitchShifterPresetList.find(p => p.id == preset.id) +// if (target) Object.assign(target, preset) +// else userPitchShifterPresetList.push(preset) +// saveUserSoundEffectPitchShifterPresetList(toRaw(userPitchShifterPresetList)) +// } +// export const removeUserPitchShifterPreset = async(id: string) => { +// if (userPitchShifterPresetList == null) { +// userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList()) +// } +// const index = userPitchShifterPresetList.findIndex(p => p.id == id) +// if (index < 0) return +// userPitchShifterPresetList.splice(index, 1) +// saveUserSoundEffectPitchShifterPresetList(toRaw(userPitchShifterPresetList)) +// } diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/store/utils.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/utils.ts new file mode 100644 index 000000000..7a66853b9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/store/utils.ts @@ -0,0 +1,82 @@ +// import { getListFromState } from './list' +// import { downloadList } from './download' + + +// export const getList = (listId: string | null): LX.Download.ListItem[] | LX.Music.MusicInfo[] => { +// return listId == 'download' ? downloadList : getListFromState(listId) +// } +import { encodePath, isUrl } from '@common/utils/common' +import { joinPath } from '@common/utils/nodejs' +import { markRaw, shallowReactive } from '@common/utils/vueTools' +import { getThemes as getTheme } from '@renderer/utils/ipc' +import { qualityList, themeInfo, themeShouldUseDarkColors } from './index' + +export const assertApiSupport = (source: LX.Source): boolean => { + return source == 'local' || qualityList.value[source] != null +} + +export const buildBgUrl = (originUrl: string, dataPath: string): string => { + return isUrl(originUrl) + ? `url(${originUrl})` + : `url(${encodePath(joinPath(dataPath, originUrl).replaceAll('\\', '/'))})` +} + +export const getThemes = (callback: (themeInfo: LX.ThemeInfo) => void) => { + if (themeInfo.themes.length) { + callback(themeInfo) + return + } + void getTheme().then(info => { + themeInfo.themes = markRaw(info.themes) + themeInfo.userThemes = shallowReactive(info.userThemes) + themeInfo.dataPath = info.dataPath + callback(themeInfo) + }) +} +export const buildThemeColors = (theme: LX.Theme, dataPath: string) => { + if (theme.isCustom && theme.config.extInfo['--background-image'] != 'none') { + theme = copyTheme(theme) + theme.config.extInfo['--background-image'] = buildBgUrl(theme.config.extInfo['--background-image'], dataPath) + } + const colors: Record = { + ...theme.config.themeColors, + ...theme.config.extInfo, + } + + return colors +} + +export const copyTheme = (theme: LX.Theme): LX.Theme => { + return { + ...theme, + config: { + ...theme.config, + extInfo: { ...theme.config.extInfo }, + themeColors: { ...theme.config.themeColors }, + }, + } +} + +export const findTheme = (themeInfo: LX.ThemeInfo, id: string): LX.Theme | undefined => { + let theme = themeInfo.themes.find(theme => theme.id == id) + if (theme) return theme + theme = themeInfo.userThemes.find(theme => theme.id == id) + return theme +} + +export const applyTheme = (id: string, lightId: string, darkId: string, dataPath: string) => { + getThemes((themeInfo) => { + let themeId = id == 'auto' + ? themeShouldUseDarkColors.value + ? darkId + : lightId + : id + + let theme = findTheme(themeInfo, themeId) + if (!theme) { + themeId = id == 'auto' && themeShouldUseDarkColors.value ? 'black' : 'green' + theme = themeInfo.themes.find(theme => theme.id == themeId)! + } + window.setTheme(buildThemeColors(theme, dataPath)) + }) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/tsconfig.json b/plugins/lx-music-ztools/vendor/lx/src/renderer/tsconfig.json new file mode 100644 index 000000000..774e21f1e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "isolatedModules": true, + "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ + "@common/*": ["../common/*"], + "@renderer/*": ["../renderer/*"], + // "@lyric/*": ["../renderer-lyric/*"], + "@static/*": ["../static/*"], + "@root/*": ["../*"], + }, + "typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */ + "./types" + ], + }, + "vueCompilerOptions": { + "plugins": [ + "@vue/language-plugin-pug" + ] + } + // "include": [ + // "./**/*.ts", + // // "./**/*.js", + // "./**/*.vue", + // "./**/*.json", + // ], +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/types/app.d.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/types/app.d.ts new file mode 100644 index 000000000..0b0d31f29 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/types/app.d.ts @@ -0,0 +1,105 @@ +/* eslint-disable no-var */ +import { type AppEventTypes, type KeyEventTypes } from '@renderer/event' +import { type MainTypes, type DownloadTypes } from '@renderer/worker/utils' +import { type I18n } from '@renderer/plugins/i18n' +// interface LX.EnvParams { +// deeplink?: string | null +// cmdParams: LX.CmdParams +// workAreaSize?: Electron.Size +// } + + +interface Lx { + // appSetting: LX.AppSetting + isEditingHotKey: boolean + isPlayedStop: boolean + appHotKeyConfig: LX.HotKeyConfigAll + restorePlayInfo: LX.Player.SavedPlayInfo | null + worker: { + main: MainTypes + download: DownloadTypes + } + isProd: boolean + songListInfo: { + fromName: string + searchKey: string | null + searchPosition?: number + songlistKey: string | null + songlistPosition?: number + } + rootOffset: number + apiInitPromise: [Promise, boolean, (success: boolean) => void] +} + +declare global { + interface Window { + ELECTRON_DISABLE_SECURITY_WARNINGS?: string + dt: boolean + shouldUseDarkColors: boolean + lx: Lx + app_event: AppEventTypes + key_event: KeyEventTypes + i18n: I18n + + lxData: any + + setTheme: (colors: Record) => void + setLang: (lang?: string) => void + } + + module NodeJS { + interface ProcessVersions { + app: string + } + } + + // const ENVIRONMENT: NodeJS.ProcessEnv + + + namespace LX { + interface KeyDownEevent { + /** + * 原始事件 + */ + event: KeyEvent | null + + /** + * 按下的按键数组 + */ + keys: string[] + + /** + * 按下的按键组合 + * + * 类似:`shift`、`mod+a` + * + * 其中 `Ctrl` 的名称为 `mod`, 对应 MacOS 上的 `Command` 键 + */ + key: string + /** + * 当前触发此事件的单个按键(不包括之前已按下的键) + */ + eventKey: string + /** + * 按键操作类型 + */ + type: 'down' | 'up' + } + + type LyricFormat = 'gbk' | 'utf8' + + class KeyEvent extends KeyboardEvent { + /** + * 此事件是否标记为 已被处理,如果设置为`true`,则停止触发key event事件 + */ + lx_handled?: boolean + } + } + + var COMMIT_ID: string + var COMMIT_DATE: string +} + + +// declare const ELECTRON_DISABLE_SECURITY_WARNINGS: string +// declare const userApiPath: string diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/types/common.d.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/types/common.d.ts new file mode 100644 index 000000000..7436b1114 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/types/common.d.ts @@ -0,0 +1,19 @@ +import '@common/types/app_setting' +import '@common/types/common' +import '@common/types/user_api' +import '@common/types/sync' +import '@common/types/list_sync' +import '@common/types/music' +import '@common/types/list' +import '@common/types/download_list' +import '@common/types/player' +import '@common/types/shims_vue' +import '@common/types/utils' +import '@common/types/theme' +import '@common/types/desktop_lyric' +import '@common/types/ipc_renderer' +import '@common/types/config_files' +import '@common/types/music_metadata' +import '@common/types/sound_effect' +import '@common/types/dislike_list' +import '@common/types/open_api' diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/types/i18n.d.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/types/i18n.d.ts new file mode 100644 index 000000000..1f75813eb --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/types/i18n.d.ts @@ -0,0 +1,7 @@ +import { type I18n } from '@root/lang' + +declare module 'vue' { + interface ComponentCustomProperties { + $t: I18n['t'] + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/types/player.d.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/types/player.d.ts new file mode 100644 index 000000000..f627e71f0 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/types/player.d.ts @@ -0,0 +1,57 @@ + +declare namespace LX { + namespace Player { + interface PlayMusicInfo { + /** + * 当前播放歌曲的列表 id + */ + musicInfo: LX.Download.ListItem | LX.Music.MusicInfo + /** + * 当前播放歌曲的列表 id + */ + listId: string | null + /** + * 是否属于 “稍后播放” + */ + isTempPlay: boolean + } + + interface PlayInfo { + /** + * 当前正在播放歌曲 index + */ + playIndex: number + /** + * 播放器的播放列表 id + */ + playerListId: string | null + /** + * 播放器播放歌曲 index + */ + playerPlayIndex: number + } + + interface TempPlayListItem { + /** + * 播放列表id + */ + listId: string | null + /** + * 歌曲信息 + */ + musicInfo: LX.Music.MusicInfo | LX.Download.ListItem + /** + * 是否添加到列表顶部 + */ + isTop?: boolean + } + + interface SavedPlayInfo { + time: number + maxTime: number + listId: string + index: number + } + + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/types/worker.d.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/types/worker.d.ts new file mode 100644 index 000000000..ad284fd5a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/types/worker.d.ts @@ -0,0 +1,10 @@ +import { type workerMainTypes } from '@renderer/worker/main/index' +import { type workerDownloadTypes } from '@renderer/worker/download/index' + + +declare global { + namespace LX { + type WorkerMainTypes = workerMainTypes + type WorkerDownloadTypes = workerDownloadTypes + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useDrag.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useDrag.js new file mode 100644 index 000000000..6204e2f39 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useDrag.js @@ -0,0 +1,49 @@ +import Sortable, { AutoScroll } from 'sortablejs/modular/sortable.core.esm' +import { onMounted } from '@common/utils/vueTools' +import { clearDownKeys } from '@renderer/event' + +Sortable.mount(new AutoScroll()) + +const noop = () => {} + +export default ({ dom_list, dragingItemClassName, filter, onUpdate, onStart = noop, onEnd = noop }) => { + let sortable + + onMounted(() => { + sortable = Sortable.create(dom_list.value, { + animation: 150, + disabled: true, + forceFallback: false, + filter: filter ? '.' + filter : null, + ghostClass: dragingItemClassName, + onUpdate(event) { + onUpdate(event.newIndex, event.oldIndex) + }, + onMove(event) { + return filter ? !event.related.classList.contains(filter) : true + }, + onChoose() { + onStart() + }, + onUnchoose() { + onEnd() + // 处于拖动状态期间,键盘事件无法监听,拖动结束手动清理按下的键 + // window.app_event.emit(eventBaseName.setClearDownKeys) + clearDownKeys() + }, + onStart(event) { + window.app_event.dragStart() + }, + onEnd(event) { + window.app_event.dragEnd() + }, + }) + }) + + return { + setDisabled(enable) { + if (!sortable) return + sortable.option('disabled', enable) + }, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useIconSize.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useIconSize.ts new file mode 100644 index 000000000..be0866029 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useIconSize.ts @@ -0,0 +1,37 @@ +import { type Ref, onBeforeUnmount, onMounted, ref } from '@common/utils/vueTools' + +const onDomSizeChanged = (dom: HTMLElement, onChanged: (width: number, height: number) => void) => { + // 使用 ResizeObserver 监听大小变化 + const resizeObserver = new ResizeObserver(entries => { + for (let entry of entries) { + const { width, height } = entry.contentRect + // console.log(dom.offsetLeft, dom.offsetTop, left, top, width, height) + onChanged(Math.trunc(width), Math.trunc(height)) + } + }) + + resizeObserver.observe(dom) + + onChanged(dom.clientWidth, dom.clientHeight) + + return () => { + resizeObserver.disconnect() + } +} + +export const useIconSize = (parentDom: Ref, size: number) => { + const iconSize = ref('32px') + let unsub: (() => void) | null = null + + onMounted(() => { + if (!parentDom.value) return + unsub = onDomSizeChanged(parentDom.value, (width, height) => { + iconSize.value = Math.trunc(width * size) + 'px' + }) + }) + onBeforeUnmount(() => { + unsub?.() + }) + + return iconSize +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useImportTip.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useImportTip.js new file mode 100644 index 000000000..30b1323fb --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useImportTip.js @@ -0,0 +1,39 @@ +import { useI18n } from '@renderer/plugins/i18n' +import { dialog } from '@renderer/plugins/Dialog' + +export default () => { + const t = useI18n() + + + return (type) => { + let message + switch (type) { + case 'defautlList': + case 'playList': + case 'playList_v2': + message = t('list_import_tip__playlist') + break + case 'setting': + case 'setting_v2': + message = t('list_import_tip__setting') + break + case 'allData': + case 'allData_v2': + message = t('list_import_tip__alldata') + break + case 'playListPart': + case 'playListPart_v2': + message = t('list_import_tip__playlist_part') + break + + default: + message = t('list_import_tip__unknown') + break + } + + dialog({ + message, + confirmButtonText: t('ok'), + }) + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useKeyDown.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useKeyDown.ts new file mode 100644 index 000000000..19260a79e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useKeyDown.ts @@ -0,0 +1,38 @@ +import { onMounted, onBeforeUnmount, ref } from '@common/utils/vueTools' + +export default (name: string) => { + const keyDown = ref(false) + const down = `key_${name}_down` + const up = `key_${name}_up` + + const handle_key_down = (event: LX.KeyDownEevent) => { + if (!keyDown.value) { + // console.log(event) + switch ((event.event?.target as HTMLElement).tagName) { + case 'INPUT': + case 'SELECT': + case 'TEXTAREA': + return + default: if ((event.event?.target as HTMLElement).isContentEditable) return + } + + keyDown.value = true + } + } + + const handle_key_up = () => { + keyDown.value &&= false + } + + onMounted(() => { + window.key_event.on(down, handle_key_down) + window.key_event.on(up, handle_key_up) + }) + + onBeforeUnmount(() => { + window.key_event.off(down, handle_key_down) + window.key_event.off(up, handle_key_up) + }) + + return keyDown +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useLyric.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useLyric.js new file mode 100644 index 000000000..cf25e8a6c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useLyric.js @@ -0,0 +1,240 @@ +import { ref, onMounted, onBeforeUnmount, watch, nextTick } from '@common/utils/vueTools' +import { throttle, formatPlayTime2 } from '@common/utils/common' +import { scrollTo } from '@common/utils/renderer' +import { play } from '@renderer/core/player/action' +import { appSetting } from '@renderer/store/setting' +// import { player as eventPlayerNames } from '@renderer/event/names' + +export default ({ isPlay, lyric, playProgress, isShowLyricProgressSetting, offset }) => { + const dom_lyric = ref(null) + const dom_lyric_text = ref(null) + const dom_skip_line = ref(null) + const isMsDown = ref(false) + const isStopScroll = ref(false) + const timeStr = ref('--/--') + + let msDownY = 0 + let msDownScrollY = 0 + let timeout = null + let cancelScrollFn + let dom_lines + let isSetedLines = false + let point = { + x: null, + y: null, + } + let time = -1 + let dom_pre_line = null + let isSkipMouseEnter = false + + const handleSkipPlay = () => { + if (time == -1) return + handleSkipMouseLeave() + isStopScroll.value = false + window.app_event.setProgress(time) + if (!isPlay.value) play() + } + const handleSkipMouseEnter = () => { + isSkipMouseEnter = true + clearLyricScrollTimeout() + } + const handleSkipMouseLeave = () => { + isSkipMouseEnter = false + startLyricScrollTimeout() + } + + const throttleSetTime = throttle(() => { + if (!dom_skip_line.value) return + const rect = dom_skip_line.value.getBoundingClientRect() + point.x = rect.x + point.y = rect.y + let dom = document.elementFromPoint(point.x, point.y) + if (dom_pre_line === dom) return + if (dom.tagName == 'SPAN') { + dom = dom.parentNode.parentNode + } else if (dom.classList.contains('line')) { + dom = dom.parentNode + } + if (dom.time == null) { + if (lyric.lines.length) { + time = dom.classList.contains('pre') ? 0 : lyric.lines[lyric.lines.length - 1].time ?? 0 + time = Math.max(time - lyric.offset - lyric.tempOffset, 0) + time /= 1000 + if (time > playProgress.maxPlayTime) time = playProgress.maxPlayTime + timeStr.value = formatPlayTime2(time) + } else { + time = -1 + timeStr.value = '--:--' + } + } else { + time = dom.time + time = Math.max(time - lyric.offset - lyric.tempOffset, 0) + time /= 1000 + if (time > playProgress.maxPlayTime) time = playProgress.maxPlayTime + timeStr.value = formatPlayTime2(time) + } + dom_pre_line = dom + }) + const setTime = () => { + if (isShowLyricProgressSetting.value) throttleSetTime() + } + + const handleScrollLrc = (duration = 300) => { + if (!dom_lines?.length || !dom_lyric.value) return + if (isSkipMouseEnter) return + if (isStopScroll.value) return + let dom_p = dom_lines[lyric.line] + cancelScrollFn = scrollTo(dom_lyric.value, dom_p ? (dom_p.offsetTop - dom_lyric.value.clientHeight * 0.38) : 0, duration) + } + const clearLyricScrollTimeout = () => { + if (!timeout) return + clearTimeout(timeout) + timeout = null + } + const startLyricScrollTimeout = () => { + clearLyricScrollTimeout() + if (isSkipMouseEnter) return + timeout = setTimeout(() => { + timeout = null + isStopScroll.value = false + if (!isPlay.value) return + handleScrollLrc() + }, 3000) + } + const handleLyricDown = (y) => { + // console.log(event) + if (delayScrollTimeout) { + clearTimeout(delayScrollTimeout) + delayScrollTimeout = null + } + isMsDown.value = true + msDownY = y + msDownScrollY = dom_lyric.value.scrollTop + } + const handleLyricMouseDown = event => { + handleLyricDown(event.clientY) + } + const handleLyricTouchStart = event => { + if (event.changedTouches.length) { + const touch = event.changedTouches[0] + handleLyricDown(touch.clientY) + } + } + const handleMouseMsUp = event => { + isMsDown.value = false + } + const handleMove = (y) => { + if (isMsDown.value) { + isStopScroll.value ||= true + if (cancelScrollFn) { + cancelScrollFn() + cancelScrollFn = null + } + dom_lyric.value.scrollTop = msDownScrollY + msDownY - y + startLyricScrollTimeout() + setTime() + } + } + const handleMouseMsMove = event => { + handleMove(event.clientY) + } + const handleTouchMove = (e) => { + if (e.changedTouches.length) { + const touch = e.changedTouches[0] + handleMove(touch.clientY) + } + } + + const handleWheel = (event) => { + console.log(event.deltaY) + isStopScroll.value ||= true + if (cancelScrollFn) { + cancelScrollFn() + cancelScrollFn = null + } + dom_lyric.value.scrollTop = dom_lyric.value.scrollTop + event.deltaY + startLyricScrollTimeout() + setTime() + } + + const setLyric = (lines) => { + const dom_line_content = document.createDocumentFragment() + for (const line of lines) { + dom_line_content.appendChild(line.dom_line) + } + dom_lyric_text.value.textContent = '' + dom_lyric_text.value.appendChild(dom_line_content) + nextTick(() => { + dom_lines = dom_lyric.value.querySelectorAll('.line-content') + handleScrollLrc() + }) + } + + const initLrc = (lines, oLines) => { + isSetedLines = true + if (oLines) { + if (lines.length) { + setLyric(lines) + } else { + cancelScrollFn = scrollTo(dom_lyric.value, 0, 300, () => { + if (lyric.lines !== lines) return + setLyric(lines) + }, 50) + } + } else { + setLyric(lines) + } + } + + let delayScrollTimeout + const scrollLine = (line, oldLine) => { + if (line < 0) return + if (line == 0 && isSetedLines) return isSetedLines = false + isSetedLines &&= false + if (oldLine == null || line - oldLine != 1) return handleScrollLrc() + + if (appSetting['playDetail.isDelayScroll']) { + delayScrollTimeout = setTimeout(() => { + delayScrollTimeout = null + handleScrollLrc(600) + }, 600) + } else { + handleScrollLrc() + } + } + + watch(() => lyric.lines, initLrc) + watch(() => lyric.line, scrollLine) + + onMounted(() => { + document.addEventListener('mousemove', handleMouseMsMove) + document.addEventListener('mouseup', handleMouseMsUp) + document.addEventListener('touchmove', handleTouchMove) + document.addEventListener('touchend', handleMouseMsUp) + + initLrc(lyric.lines, null) + }) + + onBeforeUnmount(() => { + document.removeEventListener('mousemove', handleMouseMsMove) + document.removeEventListener('mouseup', handleMouseMsUp) + document.removeEventListener('touchmove', handleTouchMove) + document.removeEventListener('touchend', handleMouseMsUp) + }) + + return { + dom_lyric, + dom_lyric_text, + dom_skip_line, + isStopScroll, + isMsDown, + timeStr, + handleLyricMouseDown, + handleLyricTouchStart, + handleWheel, + handleSkipPlay, + handleSkipMouseEnter, + handleSkipMouseLeave, + handleScrollLrc, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useMenuLocation.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useMenuLocation.js new file mode 100644 index 000000000..26fb47206 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useMenuLocation.js @@ -0,0 +1,85 @@ +import { onMounted, onBeforeUnmount, watch, reactive, ref } from '@common/utils/vueTools' + + +export default ({ visible, location, onHide }) => { + const transition1 = 'transform, opacity' + const transition2 = 'transform, opacity, top, left' + let show = false + const dom_menu = ref(null) + const menuStyles = reactive({ + left: 0, + top: 0, + opacity: 0, + transitionProperty: 'transform, opacity', + transform: 'scale(.8, .7) translate(0,0)', + pointerEvents: 'none', + }) + + const handleShow = () => { + show = true + menuStyles.opacity = 1 + menuStyles.transform = `scale(1) translate(${handleGetOffsetXY(location.value.x, location.value.y)})` + menuStyles.pointerEvents = 'auto' + } + const handleHide = () => { + menuStyles.opacity = 0 + menuStyles.transform = 'scale(.8, .7) translate(0, 0)' + menuStyles.pointerEvents = 'none' + show = false + } + const handleGetOffsetXY = (left, top) => { + const listWidth = dom_menu.value.clientWidth + const listHeight = dom_menu.value.clientHeight + const dom_container_parant = dom_menu.value.offsetParent + const containerWidth = dom_container_parant.clientWidth + const containerHeight = dom_container_parant.clientHeight + const offsetWidth = containerWidth - left - listWidth + const offsetHeight = containerHeight - top - listHeight + let x = 0 + let y = 0 + if (containerWidth > listWidth && offsetWidth < 12) { + x = offsetWidth - 12 + } + if (containerHeight > listHeight && offsetHeight < 5) { + y = offsetHeight - 5 + } + return `${x}px, ${y}px` + } + const handleDocumentClick = (event) => { + if (!show) return + + if (event.target == dom_menu.value || dom_menu.value.contains(event.target)) return + + if (show && menuStyles.transitionProperty != transition1) menuStyles.transitionProperty = transition1 + + onHide() + } + + watch(visible, visible => { + visible ? handleShow() : handleHide() + }, { immediate: true }) + + watch(location, location => { + menuStyles.left = location.x - window.lx.rootOffset + 2 + 'px' + menuStyles.top = location.y - window.lx.rootOffset + 'px' + // nextTick(() => { + if (show) { + if (menuStyles.transitionProperty != transition2) menuStyles.transitionProperty = transition2 + menuStyles.transform = `scale(1) translate(${handleGetOffsetXY(location.x, location.y)})` + } + // }) + }, { deep: true }) + + onMounted(() => { + document.addEventListener('click', handleDocumentClick) + }) + + onBeforeUnmount(() => { + document.removeEventListener('click', handleDocumentClick) + }) + + return { + dom_menu, + menuStyles, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useNextTogglePlay.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useNextTogglePlay.ts new file mode 100644 index 000000000..37047eaec --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useNextTogglePlay.ts @@ -0,0 +1,38 @@ +import { appSetting, setTogglePlayMode } from '@renderer/store/setting' +import { + computed, +} from '@common/utils/vueTools' +import { useI18n } from '@renderer/plugins/i18n' + +// const playNextModes = [ +// 'listLoop', +// 'random', +// 'list', +// 'singleLoop', +// 'none', +// ] as const + +export default () => { + const t = useI18n() + const nextTogglePlayName = computed(() => { + switch (appSetting['player.togglePlayMethod']) { + case 'listLoop': return t('player__play_toggle_mode_list_loop') + case 'random': return t('player__play_toggle_mode_random') + case 'singleLoop': return t('player__play_toggle_mode_single_loop') + case 'list': return t('player__play_toggle_mode_list') + default: return t('player__play_toggle_mode_off') + } + }) + + const toggleNextPlayMode = (mode: LX.AppSetting['player.togglePlayMethod']) => { + if (mode == appSetting['player.togglePlayMethod']) return + // let index = playNextModes.indexOf(appSetting['player.togglePlayMethod']) + // if (++index >= playNextModes.length) index = 0 + setTogglePlayMode(mode) + } + + return { + nextTogglePlayName, + toggleNextPlayMode, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/usePlayProgress.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/usePlayProgress.js new file mode 100644 index 000000000..8e8dd4923 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/usePlayProgress.js @@ -0,0 +1,30 @@ +import { ref, onBeforeUnmount, toRef } from '@common/utils/vueTools' +import { playProgress } from '@renderer/store/player/playProgress' + +export default () => { + const isActiveTransition = ref(false) + const progress = toRef(playProgress, 'progress') + const nowPlayTimeStr = toRef(playProgress, 'nowPlayTimeStr') + const maxPlayTimeStr = toRef(playProgress, 'maxPlayTimeStr') + + const handleTransitionEnd = () => { + isActiveTransition.value = false + } + const handleActiveTransition = () => { + isActiveTransition.value = true + } + + window.app_event.on('activePlayProgressTransition', handleActiveTransition) + + onBeforeUnmount(() => { + window.app_event.off('activePlayProgressTransition', handleActiveTransition) + }) + + return { + nowPlayTimeStr, + maxPlayTimeStr, + progress, + isActiveTransition, + handleTransitionEnd, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useToggleDesktopLyric.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useToggleDesktopLyric.js new file mode 100644 index 000000000..236560000 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/compositions/useToggleDesktopLyric.js @@ -0,0 +1,34 @@ +import { + computed, +} from '@common/utils/vueTools' +import { useI18n } from '@renderer/plugins/i18n' +import { appSetting, setLockDesktopLyric, setVisibleDesktopLyric } from '@renderer/store/setting' + +export default () => { + const t = useI18n() + + const toggleDesktopLyricBtnTitle = computed(() => { + return `${ + appSetting['desktopLyric.enable'] + ? t('player__desktop_lyric_off') + : t('player__desktop_lyric_on') + }\n(${ + appSetting['desktopLyric.isLock'] + ? t('player__desktop_lyric_unlock') + : t('player__desktop_lyric_lock') + })` + }) + + const toggleDesktopLyric = () => { + setVisibleDesktopLyric(!appSetting['desktopLyric.enable']) + } + const toggleLockDesktopLyric = () => { + setLockDesktopLyric(!appSetting['desktopLyric.isLock']) + } + + return { + toggleDesktopLyricBtnTitle, + toggleDesktopLyric, + toggleLockDesktopLyric, + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/data.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/data.ts new file mode 100644 index 000000000..7911d42b8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/data.ts @@ -0,0 +1,188 @@ +/* eslint-disable @typescript-eslint/no-dynamic-delete */ +import { + saveListPositionInfo as saveListPositionInfoFromData, + getListPositionInfo as getListPositionInfoFromData, + saveListPrevSelectId as saveListPrevSelectIdFromData, + getListPrevSelectId as getListPrevSelectIdFromData, + saveListUpdateInfo as saveListUpdateInfoFromData, + getListUpdateInfo as getListUpdateInfoFromData, + saveSearchSetting as saveSearchSettingFromData, + getSearchSetting as getSearchSettingFromData, + saveSongListSetting as saveSongListSettingFromData, + getSongListSetting as getSongListSettingFromData, + saveLeaderboardSetting as saveLeaderboardSettingFromData, + getLeaderboardSetting as getLeaderboardSettingFromData, + saveViewPrevState as saveViewPrevStateFromData, +} from '@renderer/utils/ipc' +import { throttle } from '@common/utils' +import { type DEFAULT_SETTING, LIST_IDS } from '@common/constants' +import { dateFormat } from './index' +import { setUpdateTime } from '@renderer/store/list/action' + +let listPosition: LX.List.ListPositionInfo +let listPrevSelectId: string +let listUpdateInfo: LX.List.ListUpdateInfo + +let searchSetting: typeof DEFAULT_SETTING['search'] +let songListSetting: typeof DEFAULT_SETTING['songList'] +let leaderboardSetting: typeof DEFAULT_SETTING['leaderboard'] + +const saveListPositionThrottle = throttle(() => { + saveListPositionInfoFromData(listPosition) +}, 1000) +const saveSearchSettingThrottle = throttle(() => { + saveSearchSettingFromData(searchSetting) +}, 1000) +const saveSongListSettingThrottle = throttle(() => { + saveSongListSettingFromData(songListSetting) +}, 1000) +const saveLeaderboardSettingThrottle = throttle(() => { + saveLeaderboardSettingFromData(leaderboardSetting) +}, 1000) +const saveViewPrevStateThrottle = throttle((state) => { + saveViewPrevStateFromData(state) +}, 1000) + +const initPosition = async() => { + // eslint-disable-next-line require-atomic-updates + listPosition ??= await getListPositionInfoFromData() ?? {} +} +export const getListPosition = async(id: string): Promise => { + await initPosition() + return listPosition[id] ?? 0 +} +export const setListPosition = async(id: string, position?: number) => { + await initPosition() + listPosition[id] = position ?? 0 + saveListPositionThrottle() +} +export const removeListPosition = async(id: string) => { + await initPosition() + if (listPosition[id] == null) return + delete listPosition[id] + saveListPositionThrottle() +} +export const overwriteListPosition = async(ids: string[]) => { + await initPosition() + const removedIds = [] + for (const id of Object.keys(listPosition)) { + if (ids.includes(id)) continue + removedIds.push(id) + } + for (const id of removedIds) delete listPosition[id] + saveListPositionThrottle() +} + +const saveListPrevSelectIdThrottle = throttle(() => { + saveListPrevSelectIdFromData(listPrevSelectId) +}, 200) +export const getListPrevSelectId = async() => { + // eslint-disable-next-line require-atomic-updates + listPrevSelectId ??= await getListPrevSelectIdFromData() ?? LIST_IDS.DEFAULT + return listPrevSelectId ?? LIST_IDS.DEFAULT +} +export const saveListPrevSelectId = (id: string) => { + listPrevSelectId = id + saveListPrevSelectIdThrottle() +} + +const saveListUpdateInfo = throttle(() => { + saveListUpdateInfoFromData(listUpdateInfo) +}, 1000) + +const initListUpdateInfo = async() => { + if (listUpdateInfo == null) { + // eslint-disable-next-line require-atomic-updates + listUpdateInfo = await getListUpdateInfoFromData() ?? {} + for (const [id, info] of Object.entries(listUpdateInfo)) { + setUpdateTime(id, info.updateTime ? dateFormat(info.updateTime) : '') + } + } +} +export const getListUpdateInfo = async() => { + await initListUpdateInfo() + return listUpdateInfo +} +export const setListUpdateInfo = async(info: LX.List.ListUpdateInfo) => { + await initListUpdateInfo() + listUpdateInfo = info + saveListUpdateInfo() +} +export const setListAutoUpdate = async(id: string, enable: boolean) => { + await initListUpdateInfo() + const targetInfo = listUpdateInfo[id] ?? { updateTime: 0, isAutoUpdate: false } + targetInfo.isAutoUpdate = enable + listUpdateInfo[id] = targetInfo + saveListUpdateInfo() +} +export const setListUpdateTime = async(id: string, time: number) => { + await initListUpdateInfo() + const targetInfo = listUpdateInfo[id] ?? { updateTime: 0, isAutoUpdate: false } + targetInfo.updateTime = time + listUpdateInfo[id] = targetInfo + saveListUpdateInfo() +} +// export const setListUpdateInfo = (id, { updateTime, isAutoUpdate }) => { +// listUpdateInfo[id] = { updateTime, isAutoUpdate } +// saveListUpdateInfo() +// } +export const removeListUpdateInfo = async(id: string) => { + await initListUpdateInfo() + if (listUpdateInfo[id] == null) return + delete listUpdateInfo[id] + saveListUpdateInfo() +} +export const overwriteListUpdateInfo = async(ids: string[]) => { + await initListUpdateInfo() + const removedIds = [] + for (const id of Object.keys(listUpdateInfo)) { + if (ids.includes(id)) continue + removedIds.push(id) + } + for (const id of removedIds) delete listUpdateInfo[id] + saveListUpdateInfo() +} + + +export const getSearchSetting = async() => { + // eslint-disable-next-line require-atomic-updates + searchSetting ??= await getSearchSettingFromData() + return { ...searchSetting } +} +export const setSearchSetting = async(setting: Partial) => { + if (!searchSetting) await getSearchSetting() + let requiredSave = false + if (setting.source && searchSetting.source != setting.source) requiredSave = true + if (setting.type && searchSetting.type != setting.type) requiredSave = true + if (setting.temp_source && searchSetting.temp_source != setting.temp_source) requiredSave = true + + if (!requiredSave) return + searchSetting = Object.assign(searchSetting, setting) + saveSearchSettingThrottle() +} + +export const getSongListSetting = async() => { + // eslint-disable-next-line require-atomic-updates + songListSetting ??= await getSongListSettingFromData() + return { ...songListSetting } +} +export const setSongListSetting = async(setting: Partial) => { + if (!songListSetting) await getSongListSetting() + songListSetting = Object.assign(songListSetting, setting) + saveSongListSettingThrottle() +} + +export const getLeaderboardSetting = async() => { + // eslint-disable-next-line require-atomic-updates + leaderboardSetting ??= await getLeaderboardSettingFromData() + return { ...leaderboardSetting } +} +export const setLeaderboardSetting = async(setting: Partial) => { + if (!leaderboardSetting) await getLeaderboardSetting() + leaderboardSetting = Object.assign(leaderboardSetting, setting) + saveLeaderboardSettingThrottle() +} + +export const saveViewPrevState = (state: typeof DEFAULT_SETTING['viewPrevState']) => { + saveViewPrevStateThrottle(state) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/env.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/env.js new file mode 100644 index 000000000..553f01efa --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/env.js @@ -0,0 +1,5 @@ +const isDev = process.env.NODE_ENV === 'development' + +export const debug = isDev && true +export const debugRequest = isDev && false +export const debugDownload = isDev && false diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/index.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/index.ts new file mode 100644 index 000000000..4fe757ce1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/index.ts @@ -0,0 +1,82 @@ +import { dateFormat } from '@common/utils/common' + +export * from '@common/utils/renderer' +export * from '@common/utils/nodejs' +export * from '@common/utils/common' +export * from '@common/utils/tools' + +/** + * 格式化播放数量 + * @param {*} num 数字 + */ +export const formatPlayCount = (num: number): string => { + if (num > 100000000) return `${Math.trunc(num / 10000000) / 10}亿` + if (num > 10000) return `${Math.trunc(num / 1000) / 10}万` + return String(num) +} + + +/** + * 时间格式化 + */ +export const dateFormat2 = (time: number): string => { + let differ = Math.trunc((Date.now() - time) / 1000) + if (differ < 60) { + return window.i18n.t('date_format_second', { num: differ }) + } else if (differ < 3600) { + return window.i18n.t('date_format_minute', { num: Math.trunc(differ / 60) }) + } else if (differ < 86400) { + return window.i18n.t('date_format_hour', { num: Math.trunc(differ / 3600) }) + } else { + return dateFormat(time) + } +} + + +/** + * 设置标题 + */ +let dom_title = document.getElementsByTagName('title')[0] +export const setTitle = (title: string | null) => { + title ||= 'LX Music' + dom_title.innerText = title +} + + +// export const getProxyInfo = () => { +// return proxy.enable && proxy.host +// ? `http://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port}` +// : proxy.envProxy +// ? `http://${proxy.envProxy.host}:${proxy.envProxy.port}` +// : undefined +// } + + +export const getFontSizeWithScreen = (screenWidth: number = window.innerWidth): number => { + return screenWidth <= 1440 + ? 16 + : screenWidth <= 1920 + ? 18 + : screenWidth <= 2560 + ? 20 + : screenWidth <= 2560 ? 20 : 22 +} + + +export const deduplicationList = (list: T[]): T[] => { + const ids = new Set() + return list.filter(s => { + if (ids.has(s.id)) return false + ids.add(s.id) + return true + }) +} + +export const langS2T = async(str: string) => { + return window.lx.worker.main.langS2t(Buffer.from(str).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString()) +} + +export const decodeName = (str: string | null = '') => { + if (!str) return '' + return new window.DOMParser().parseFromString(str, 'text/html').body.textContent +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/ipc.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/ipc.ts new file mode 100644 index 000000000..ca4b8a708 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/ipc.ts @@ -0,0 +1,855 @@ +import { rendererSend, rendererInvoke, rendererOn, rendererOff } from '@common/rendererIpc' +import { HOTKEY_RENDERER_EVENT_NAME, WIN_MAIN_RENDERER_EVENT_NAME, CMMON_EVENT_NAME } from '@common/ipcNames' +import { type ProgressInfo, type UpdateDownloadedEvent, type UpdateInfo } from 'electron-updater' +import { markRaw } from '@common/utils/vueTools' +import * as hotKeys from '@common/hotKey' +import { APP_EVENT_NAMES, DATA_KEYS, DEFAULT_SETTING } from '@common/constants' + +type RemoveListener = () => void + +export const getSetting = async() => { + return rendererInvoke(CMMON_EVENT_NAME.get_app_setting) +} +export const updateSetting = async(setting: Partial) => { + await rendererInvoke(CMMON_EVENT_NAME.set_app_setting, setting) +} +export const onSettingChanged = (listener: LX.IpcRendererEventListenerParams>): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.on_config_change, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.on_config_change, listener) + } +} + +export const sendInited = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.inited) +} + +export const getOtherSource = async(id: string): Promise => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_other_source, id) +} +export const saveOtherSource = async(id: string, sourceInfo: LX.Music.MusicInfoOnline[]) => { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.save_other_source, { + id, + list: sourceInfo, + }) +} +export const clearOtherSource = async() => { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_other_source) +} +export const getOtherSourceCount = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_other_source_count) +} + +// export const updateDislikeInfo = async(dislikeInfo: LX.Dislike.ListItem[]) => { +// await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.update_dislike_music_infos, dislikeInfo) +// } +// export const removeDislikeInfo = async(ids: string[]) => { +// await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.remove_dislike_music_infos, ids) +// } +// export const clearDislikeInfo = async() => { +// await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_dislike_music_infos) +// } + +export const getHotKeyConfig = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_hot_key) +} + +export const setIgnoreMouseEvents = (ignore: boolean) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.set_ignore_mouse_events, ignore) +} + +export const getEnvParams = async() => { + return rendererInvoke(CMMON_EVENT_NAME.get_env_params) +} + +export const clearEnvParamsDeeplink = () => { + rendererSend(CMMON_EVENT_NAME.clear_env_params_deeplink) +} + +export const onDeeplink = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(CMMON_EVENT_NAME.deeplink, listener) + return () => { + rendererOff(CMMON_EVENT_NAME.deeplink, listener) + } +} + +export const checkUpdate = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.update_check) +} + +export const downloadUpdate = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.update_download_update) +} + +export const quitUpdate = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.quit_update) +} + +export const onUpdateAvailable = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_available, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_available, listener) + } +} + +export const onUpdateError = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_error, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_error, listener) + } +} + +export const onUpdateProgress = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_progress, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_progress, listener) + } +} + +export const onUpdateDownloaded = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_downloaded, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_downloaded, listener) + } +} + +export const onUpdateNotAvailable = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_not_available, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_not_available, listener) + } +} + + +export const importUserApi = async(fileText: string) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.import_user_api, fileText) +} +export const setUserApi = async(source: LX.UserApi.UserApiSetApiParams): Promise => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.set_user_api, source) +} +export const removeUserApi = async(ids: string[]) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.remove_user_api, ids) +} +export const onShowUserApiUpdateAlert = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.user_api_show_update_alert, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.user_api_show_update_alert, listener) + } +} +export const setAllowShowUserApiUpdateAlert = async(id: string, enable: boolean): Promise => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.user_api_set_allow_update_alert, { id, enable }) +} +export const onUserApiStatus = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.user_api_status, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.user_api_status, listener) + } +} +export const getUserApiList = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_user_api_list) +} +export const sendUserApiRequest = async({ requestKey, data }: LX.UserApi.UserApiRequestParams): Promise => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api, { + requestKey, + data, + }) +} +export const userApiRequestCancel = (requestKey: LX.UserApi.UserApiRequestCancelParams) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api_cancel, requestKey) +} + +// export const setDesktopLyricInfo = (type, data, info) => { +// rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.set_lyric_info, { +// type, +// data, +// info, +// }) +// } +// export const onGetDesktopLyricInfo = callback => { +// rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_info, callback) +// return () => { +// rendererOff(callback) +// } +// } + +export const sendPlayerStatus = (status: Partial) => { + rendererSend>(WIN_MAIN_RENDERER_EVENT_NAME.player_status, status) +} + + +export const sendOpenAPIAction = async(action: LX.OpenAPI.Actions) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.open_api_action, action) +} + +export const saveLastStartInfo = (version: string) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.lastStartInfo, + data: version, + }) +} +// 获取最后一次启动时的版本号 +export const getLastStartInfo = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.lastStartInfo) +} + +export const savePlayInfo = (playInfo: LX.Player.SavedPlayInfo) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.playInfo, + data: playInfo, + }) +} +// 获取上次关闭时的当前歌曲播放信息 +export const getPlayInfo = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.playInfo) +} + +export const saveSearchHistoryList = (list: LX.List.SearchHistoryList) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.searchHistoryList, + data: list, + }) +} +// 获取搜索历史列表 +export const getSearchHistoryList = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.searchHistoryList) +} + +export const saveListPositionInfo = (listPosition: LX.List.ListPositionInfo) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.listScrollPosition, + data: listPosition, + }) +} +// 获取搜索历史列表 +export const getListPositionInfo = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.listScrollPosition) +} + +export const saveListPrevSelectId = (listPosition: string | null) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.listPrevSelectId, + data: listPosition, + }) +} +// 获取上一次选中的列表id +export const getListPrevSelectId = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.listPrevSelectId) +} + +export const saveListUpdateInfo = (listPosition: LX.List.ListUpdateInfo) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.listUpdateInfo, + data: listPosition, + }) +} +// 获取列表更新记录 +export const getListUpdateInfo = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.listUpdateInfo) +} + +export const saveIgnoreVersion = (version: string) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.ignoreVersion, + data: version, + }) +} +// 获取忽略更新的版本号 +export const getIgnoreVersion = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.ignoreVersion) +} + +export const saveLeaderboardSetting = (source: typeof DEFAULT_SETTING['leaderboard']) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.leaderboardSetting, + data: source, + }) +} +export const getLeaderboardSetting = async() => { + return (await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.leaderboardSetting)) ?? { ...DEFAULT_SETTING.leaderboard } +} +export const saveSongListSetting = (setting: typeof DEFAULT_SETTING['songList']) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.songListSetting, + data: setting, + }) +} +export const getSongListSetting = async() => { + return (await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.songListSetting)) ?? { ...DEFAULT_SETTING.songList } +} +export const saveSearchSetting = (setting: typeof DEFAULT_SETTING['search']) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.searchSetting, + data: setting, + }) +} +export const getSearchSetting = async() => { + return (await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.searchSetting)) ?? { ...DEFAULT_SETTING.search } +} +export const saveViewPrevState = (state: typeof DEFAULT_SETTING['viewPrevState']) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, { + path: DATA_KEYS.viewPrevState, + data: state, + }) +} +export const getViewPrevState = async() => { + return (await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.viewPrevState)) ?? { ...DEFAULT_SETTING.viewPrevState } +} + + +export const getSystemFonts = async() => { + return rendererInvoke(CMMON_EVENT_NAME.get_system_fonts).catch(() => { + return [] + }) +} + +export const getUserSoundEffectEQPresetList = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_eq_preset) +} + +export const saveUserSoundEffectEQPresetList = (list: LX.SoundEffect.EQPreset[]) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_eq_preset, list) +} + +export const getUserSoundEffectConvolutionPresetList = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_convolution_preset) +} + +export const saveUserSoundEffectConvolutionPresetList = (list: LX.SoundEffect.ConvolutionPreset[]) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, list) +} + +// export const getUserSoundEffectPitchShifterPresetList = async() => { +// return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_pitch_shifter_preset) +// } + +// export const saveUserSoundEffectPitchShifterPresetList = (list: LX.SoundEffect.PitchShifterPreset[]) => { +// rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_pitch_shifter_preset, list) +// } + +export const allHotKeys = markRaw({ + local: [ + { + name: hotKeys.HOTKEY_PLAYER.toggle_play.name, + action: hotKeys.HOTKEY_PLAYER.toggle_play.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.prev.name, + action: hotKeys.HOTKEY_PLAYER.prev.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.next.name, + action: hotKeys.HOTKEY_PLAYER.next.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.seekbackward.name, + action: hotKeys.HOTKEY_PLAYER.seekbackward.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.seekforward.name, + action: hotKeys.HOTKEY_PLAYER.seekforward.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.music_dislike.name, + action: hotKeys.HOTKEY_PLAYER.music_dislike.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_COMMON.focusSearchInput.name, + action: hotKeys.HOTKEY_COMMON.focusSearchInput.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_COMMON.min.name, + action: hotKeys.HOTKEY_COMMON.min.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_COMMON.close.name, + action: hotKeys.HOTKEY_COMMON.close.action, + type: APP_EVENT_NAMES.winMainName, + }, + ], + global: [ + { + name: hotKeys.HOTKEY_COMMON.min_toggle.name, + action: hotKeys.HOTKEY_COMMON.min_toggle.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_COMMON.hide_toggle.name, + action: hotKeys.HOTKEY_COMMON.hide_toggle.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_COMMON.close.name, + action: hotKeys.HOTKEY_COMMON.close.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.toggle_play.name, + action: hotKeys.HOTKEY_PLAYER.toggle_play.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.prev.name, + action: hotKeys.HOTKEY_PLAYER.prev.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.next.name, + action: hotKeys.HOTKEY_PLAYER.next.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.seekbackward.name, + action: hotKeys.HOTKEY_PLAYER.seekbackward.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.seekforward.name, + action: hotKeys.HOTKEY_PLAYER.seekforward.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.volume_up.name, + action: hotKeys.HOTKEY_PLAYER.volume_up.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.volume_down.name, + action: hotKeys.HOTKEY_PLAYER.volume_down.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.volume_mute.name, + action: hotKeys.HOTKEY_PLAYER.volume_mute.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.music_love.name, + action: hotKeys.HOTKEY_PLAYER.music_love.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.music_unlove.name, + action: hotKeys.HOTKEY_PLAYER.music_unlove.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_PLAYER.music_dislike.name, + action: hotKeys.HOTKEY_PLAYER.music_dislike.action, + type: APP_EVENT_NAMES.winMainName, + }, + { + name: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_visible.name, + action: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_visible.action, + type: APP_EVENT_NAMES.winLyricName, + }, + { + name: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_lock.name, + action: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_lock.action, + type: APP_EVENT_NAMES.winLyricName, + }, + { + name: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_always_top.name, + action: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_always_top.action, + type: APP_EVENT_NAMES.winLyricName, + }, + ], +}) + +export const hotKeySetEnable = async(enable: boolean) => { + return rendererInvoke(HOTKEY_RENDERER_EVENT_NAME.enable, enable) +} + +export const hotKeySetConfig = async(config: LX.HotKeyActions) => { + return rendererInvoke(HOTKEY_RENDERER_EVENT_NAME.set_config, config) +} + +export const hotKeyGetStatus = async() => { + return rendererInvoke(HOTKEY_RENDERER_EVENT_NAME.status) +} + +// 主进程操作播放器状态 +export const onPlayerAction = (listener: LX.IpcRendererEventListenerParams<{ + action: LX.Player.StatusButtonActions + data?: unknown +}>): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.player_action_on_button_click, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.player_action_on_button_click, listener) + } +} +// export const setTaskbarThumbnailClip = async(clip: Electron.Rectangle) => { +// await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_set_thumbnail_clip, clip) +// } +// 播放器状态更新 通知主进程 +export const setPlayerAction = (buttons: LX.TaskBarButtonFlags) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.player_action_set_buttons, buttons) +} + +/** + * On Theme Change + * @param listener LX.IpcRendererEventListenerParams + * @returns RemoveListener Fn + */ +export const onThemeChange = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(CMMON_EVENT_NAME.theme_change, listener) + return () => { + rendererOff(CMMON_EVENT_NAME.theme_change, listener) + } +} + +/** + * 选择路径 + */ +export const showSelectDialog = async(options: Electron.OpenDialogOptions) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.show_select_dialog, options) +} + +/** + * 打开保存对话框 + */ +export const openSaveDir = async(options: Electron.SaveDialogOptions) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.show_save_dialog, options) +} + +/** + * 在资源管理器中定位文件 + */ +export const openDirInExplorer = async(path: string) => { + return rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.open_dir_in_explorer, path) +} + +/** + * 获取缓存大小 + */ +export const getCacheSize = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_cache_size) +} + +/** + * 清除缓存 + */ +export const clearCache = async() => { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_cache) +} + +/** + * 设置窗口大小 + * @param {*} width + * @param {*} height + */ +export const setWindowSize = (width: number, height: number) => { + const params: Partial = { + width, + height, + } + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.set_window_size, params) +} + + +export const getPlayerLyric = async(musicInfo: LX.Music.MusicInfo) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_palyer_lyric, musicInfo.id) +} + +export const getLyricRaw = async(musicInfo: LX.Music.MusicInfo): Promise => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_raw, musicInfo.id) +} + +export const clearLyricRaw = async() => { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_raw) +} + +export const getLyricRawCount = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_raw_count) +} + + +export const getLyricEdited = async(musicInfo: LX.Music.MusicInfo): Promise => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_edited, musicInfo.id) +} + +export const saveLyric = async(musicInfo: LX.Music.MusicInfo, lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo) => { + // console.log(musicInfo) + if ('rawlrcInfo' in lyricInfo) { + const { rawlrcInfo, ...info } = lyricInfo + const tasks = [ + rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_raw, { + id: musicInfo.id, + lyrics: rawlrcInfo, + }), + ] + if (info.lyric != rawlrcInfo.lyric) { + tasks.push(rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_edited, { + id: musicInfo.id, + lyrics: info, + })) + } + console.log(tasks) + await Promise.all(tasks) + } else { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_raw, { + id: musicInfo.id, + lyrics: lyricInfo, + }) + } +} +export const saveLyricEdited = async(musicInfo: LX.Music.MusicInfo, lyricInfo: LX.Music.LyricInfo) => { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_edited, { + id: musicInfo.id, + lyrics: lyricInfo, + }) +} +export const removeLyricEdited = async(musicInfo: LX.Music.MusicInfo) => { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.remove_lyric_edited, musicInfo.id) +} + +export const clearLyric = async() => { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_raw) +} + +export const clearLyricEdited = async() => { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_edited) +} + +export const getLyricEditedCount = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_edited_count) +} + + +export const saveTheme = async(theme: LX.Theme) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.save_theme, theme) +} +export const removeTheme = async(id: string) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.remove_theme, id) +} +export const getThemes = async() => { + return rendererInvoke<{ themes: LX.Theme[], userThemes: LX.Theme[], dataPath: string }>(WIN_MAIN_RENDERER_EVENT_NAME.get_themes) +} + +/** + * 从缓存获取歌曲URL + * @param musicInfo 歌曲信息 + * @param type URL音质 + * @returns + */ +export const getMusicUrl = async(musicInfo: LX.Music.MusicInfo, type: LX.Quality): Promise => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_music_url, `${musicInfo.id}_${type}`) +} + +/** + * 缓存歌曲URL + * @param musicInfo 歌曲信息 + * @param type URL音质 + * @param url 歌曲URL + */ +export const saveMusicUrl = async(musicInfo: LX.Music.MusicInfo, type: LX.Quality, url: string) => { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.save_music_url, { + id: `${musicInfo.id}_${type}`, + url, + }) +} +/** + * 清理所有缓存的歌曲URL + */ +export const clearMusicUrl = async() => { + await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_music_url) +} + +export const getMusicUrlCount = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.get_music_url_count) +} + +/** + * 退出应用 + */ +export const quitApp = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.quit) +} + +/** + * 关闭窗口 + */ +export const closeWindow = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.close) +} + +/** + * 最小化窗口 + */ +export const minWindow = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.min) +} + +/** + * 最大化窗口 + */ +export const maxWindow = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.max) +} + +/** + * 最小化、最大化窗口切换 + */ +export const minMaxWindowToggle = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.min_toggle) +} +/** + * 显示、隐藏窗口切换 + */ +export const showHideWindowToggle = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.hide_toggle) +} +/** + * 聚焦窗口 + */ +export const focusWindow = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.focus) +} +/** + * 是否启用电源锁 + */ +export const setPowerSaveBlocker = (enabled: boolean) => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.set_power_save_blocker, enabled) +} + +/** + * 窗口获取焦点事件 + * @param listener + * @returns + */ +export const onFocus = (listener: LX.IpcRendererEventListener): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.focus, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.focus, listener) + } +} + +/** + * 快捷键触发事件 + * @param listener + * @returns + */ +export const onKeyDown = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.key_down, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.key_down, listener) + } +} + +/** + * 快捷键设置更新事件 + * @param listener + * @returns + */ +export const onUpdateHotkey = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.set_hot_key_config, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.set_hot_key_config, listener) + } +} + +/** + * 设置全屏 + * @param isFullscreen 是否全屏 + * @returns + */ +export const setFullScreen = async(isFullscreen: boolean): Promise => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.fullscreen, isFullscreen) +} + +/** + * 打开开发者工具 + * @returns + */ +export const openDevTools = () => { + rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.open_dev_tools) +} + +/** + * 接收同步事件 + * @param listener + * @returns + */ +export const onSyncAction = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, listener) + } +} + +/** + * 发送同步事件 + * @param action + * @returns + */ +export const sendSyncAction = async(action: LX.Sync.SyncServiceActions) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, action) +} + +/** + * 获取同步服务端连接设备历史列表 + * @returns + */ +export const getSyncServerDevices = () => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.sync_get_server_devices) +} + +/** + * 移除同步服务端连接设备 + * @returns + */ +export const removeSyncServerDevice = (clientId: string) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.sync_remove_server_device, clientId) +} + + +// export const refreshSyncCode = async(): Promise => { +// return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.sync_generate_code) +// } + +// export const onSyncStatus = (listener: LX.IpcRendererEventListenerParams): RemoveListener => { +// rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.sync_status, listener) + +// return () => { +// rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.sync_status, listener) +// } +// } + +/** + * 桌面歌词进程创建事件 + * @param listener + * @returns + */ +export const onNewDesktopLyricProcess = (listener: LX.IpcRendererEventListener): RemoveListener => { + rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.process_new_desktop_lyric_client, listener) + return () => { + rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.process_new_desktop_lyric_client, listener) + } +} + + +export const downloadTasksGet = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.download_list_get) +} +export const downloadTasksCreate = async(list: LX.Download.ListItem[], addMusicLocationType: LX.AddMusicLocationType) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.download_list_add, { + list, + addMusicLocationType, + }) +} +export const downloadTasksUpdate = async(list: LX.Download.ListItem[]) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.download_list_update, list) +} +export const downloadTasksRemove = async(ids: string[]) => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.download_list_remove, ids) +} +export const downloadListClear = async() => { + return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.download_list_clear) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/keyBind.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/keyBind.ts new file mode 100644 index 000000000..c28475c1d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/keyBind.ts @@ -0,0 +1,126 @@ +import { isMac } from '@common/utils' + +const downKeys = new Set() + +export type KeyActionType = LX.KeyDownEevent['type'] +export type Keys = LX.KeyDownEevent['keys'] +export type Key = LX.KeyDownEevent['key'] +export type EventKey = LX.KeyDownEevent['eventKey'] +export type Event = LX.KeyDownEevent['event'] + +const handleEvent = (type: KeyActionType, event: LX.KeyEvent, keys: Keys, isEditing: boolean) => { + let eventKey = event.key + if (isMac) { + let index = keys.indexOf('meta') + if (index > -1) keys.splice(index, 1, 'mod') + if (eventKey == 'Meta') eventKey = 'mod' + } else { + let index = keys.indexOf('ctrl') + if (index > -1) keys.splice(index, 1, 'mod') + if (eventKey == 'Control') eventKey = 'mod' + } + let key = keys.join('+') + + switch (type) { + case 'down': + downKeys.add(key) + break + case 'up': + downKeys.delete(key) + break + } + handleSendEvent(key, eventKey, type, event, keys, isEditing) +} + +// 修饰键处理 +const eventModifiers = (event: LX.KeyEvent): string[] => { + let modifiers: string[] = [] + if (event.ctrlKey) modifiers.push('ctrl') + if (event.shiftKey) modifiers.push('shift') + if (event.altKey) modifiers.push('alt') + if (event.metaKey) modifiers.push('meta') + + return modifiers +} + +// 是否忽略事件(表单元素等默认忽略) +const assertStopCallback = (element: HTMLElement) => { + // if the element has the class "keybind" then no need to stop + if (element.classList.contains('key-bind')) return false + + // stop for input, select, and textarea + switch (element.tagName) { + case 'INPUT': + case 'SELECT': + case 'TEXTAREA': + return true + default: + return !!element.isContentEditable + } +} + +const handleKeyDown = (event: LX.KeyEvent) => { + // if (assertStopCallback(event.target)) return + // event.preventDefault() + let keys = eventModifiers(event) + switch (event.key) { + case 'Control': + case 'Alt': + case 'Meta': + case 'Shift': + break + case ' ': + keys.push('space') + break + default: + keys.push((event.code.includes('Numpad') ? event.code.replace(/^Numpad(\w{1,3})\w*$/i, 'num$1') : event.key).toLowerCase()) + break + } + handleEvent('down', event, keys, event.target ? assertStopCallback(event.target as HTMLElement) : false) +} + +const handleKeyUp = (event: LX.KeyEvent) => { + // if (assertStopCallback(event.target)) return + event.preventDefault() + let keys = eventModifiers(event) + switch (event.key) { + case 'Control': + keys.push('ctrl') + break + case ' ': + keys.push('space') + break + default: + keys.push((event.code.includes('Numpad') ? event.code.replace(/^Numpad(\w{1,3})\w*$/i, 'num$1') : event.key).toLowerCase()) + break + } + handleEvent('up', event, keys, event.target ? assertStopCallback(event.target as HTMLElement) : false) +} + +type HandleSendEvent = (key: Key, eventKey: EventKey, type: KeyActionType, event: Event, keys: Keys, isEditing: boolean) => void +let handleSendEvent: HandleSendEvent + +const bindKey = (handle: HandleSendEvent = () => {}) => { + handleSendEvent = handle + document.addEventListener('keydown', handleKeyDown) + document.addEventListener('keyup', handleKeyUp) +} + +const unbindKey = () => { + document.removeEventListener('keydown', handleKeyDown) + document.removeEventListener('keyup', handleKeyUp) +} + +const clearDownKeys = () => { + let keys = Array.from(downKeys) + for (let i = keys.length - 1; i > -1; i--) { + handleSendEvent(keys[i], keys[i], 'up', null, [keys[i]], false) + } + downKeys.clear() +} + +export default { + bindKey, + unbindKey, + clearDownKeys, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/message.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/message.ts new file mode 100644 index 000000000..6dec70505 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/message.ts @@ -0,0 +1,9 @@ +export const requestMsg = { + fail: '请求异常😮,可以多试几次,若还是不行就换一首吧。。。', + unachievable: '哦No😱...接口无法访问了!', + timeout: '请求超时', + // unachievable: '哦No😱...接口无法访问了!已帮你切换到临时接口,重试下看能不能播放吧~', + notConnectNetwork: '无法连接到服务器', + cancelRequest: '取消http请求', + tooManyRequests: '服务器繁忙', +} as const diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/music.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/music.ts new file mode 100644 index 000000000..f0f4915f6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/music.ts @@ -0,0 +1,241 @@ +import { checkPath, joinPath, extname, basename, readFile, getFileStats } from '@common/utils/nodejs' +import { formatPlayTime } from '@common/utils/common' +import { decodeKrc } from '@common/utils/lyricUtils/kg' +import { type IAudioMetadata } from 'music-metadata' + +export const checkDownloadFileAvailable = async(musicInfo: LX.Download.ListItem, savePath: string): Promise => { + return musicInfo.isComplate && !/\.ape$/.test(musicInfo.metadata.fileName) && + (await checkPath(musicInfo.metadata.filePath) || await checkPath(joinPath(savePath, musicInfo.metadata.fileName))) +} + +export const checkLocalFileAvailable = async(musicInfo: LX.Music.MusicInfoLocal): Promise => { + return checkPath(musicInfo.meta.filePath) +} + +/** + * 检查音乐文件是否存在 + * @param musicInfo + * @param savePath + */ +export const checkMusicFileAvailable = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, savePath: string): Promise => { + if ('progress' in musicInfo) { + return checkDownloadFileAvailable(musicInfo, savePath) + } else if (musicInfo.source == 'local') { + return checkLocalFileAvailable(musicInfo) + } else return true +} + +export const getDownloadFilePath = async(musicInfo: LX.Download.ListItem, savePath: string): Promise => { + if (musicInfo.isComplate && !/\.ape$/.test(musicInfo.metadata.fileName)) { + if (await checkPath(musicInfo.metadata.filePath)) return musicInfo.metadata.filePath + const path = joinPath(savePath, musicInfo.metadata.fileName) + if (await checkPath(path)) return path + } + return '' +} + +export const getLocalFilePath = async(musicInfo: LX.Music.MusicInfoLocal): Promise => { + return (await checkPath(musicInfo.meta.filePath)) ? musicInfo.meta.filePath : '' +} + + +/** + * 获取音乐文件路径 + * @param musicInfo + * @param savePath + * @returns + */ +export const getMusicFilePath = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, savePath: string): Promise => { + if ('progress' in musicInfo) { + return getDownloadFilePath(musicInfo, savePath) + } else if (musicInfo.source == 'local') { + return getLocalFilePath(musicInfo) + } + return '' +} + +/** + * 创建本地音乐信息对象 + * @param path 文件路径 + * @returns + */ +export const createLocalMusicInfo = async(path: string): Promise => { + if (!await checkPath(path)) return null + const { parseFile } = await import('music-metadata') + + let metadata + try { + metadata = await parseFile(path) + } catch (err) { + console.log(err) + return null + } + + // console.log(metadata) + let ext = extname(path) + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + let name = (metadata.common.title || basename(path, ext)).trim() + let singer = metadata.common.artists?.length ? metadata.common.artists.map(a => a.trim()).join('、') : '' + let interval = metadata.format.duration ? formatPlayTime(metadata.format.duration) : '' + let albumName = metadata.common.album?.trim() ?? '' + + return { + id: path, + name, + singer, + source: 'local', + interval, + meta: { + albumName, + filePath: path, + songId: path, + picUrl: '', + ext: ext.replace(/^\./, ''), + }, + } +} + +let prevFileInfo: { + path: string + promise: Promise +} = { + path: '', + promise: Promise.resolve(null), +} +const getFileMetadata = async(path: string) => { + if (prevFileInfo.path == path) return prevFileInfo.promise + prevFileInfo.path = path + return prevFileInfo.promise = checkPath(path).then(async(isExist) => { + return isExist ? import('music-metadata').then(async({ parseFile }) => parseFile(path)).catch(err => { + console.log(err) + return null + }) : null + }) +} +/** + * 获取歌曲文件封面图片 + * @param path 路径 + */ +export const getLocalMusicFilePic = async(path: string) => { + const filePath = new RegExp('\\' + extname(path) + '$') + let picPath = path.replace(filePath, '.jpg') + let stats = await getFileStats(picPath) + if (stats) return picPath + picPath = path.replace(filePath, '.png') + stats = await getFileStats(picPath) + if (stats) return picPath + const metadata = await getFileMetadata(path) + if (!metadata) return null + const { selectCover } = await import('music-metadata') + return selectCover(metadata.common.picture) +} + +// const timeExp = /^\[([\d:.]*)\]{1}/ +/** + * 解析歌词文件,分离可能存在的翻译、罗马音歌词 + * @param lrc 歌词内容 + * @returns + */ +// export const parseLyric = (lrc: string): LX.Music.LyricInfo => { +// const lines = lrc.split(/\r\n|\r|\n/) +// const lyrics: string[][] = [] +// const map = new Map() + +// for (let i = 0; i < lines.length; i++) { +// const line = lines[i].trim() +// let result = timeExp.exec(line) +// if (result) { +// const index = map.get(result[1]) ?? 0 +// if (!lyrics[index]) lyrics[index] = [] +// lyrics[index].push(line) +// map.set(result[1], index + 1) +// } else { +// if (!lyrics[0]) lyrics[0] = [] +// lyrics[0].push(line) +// } +// } +// const lyricInfo: LX.Music.LyricInfo = { +// lyric: lyrics[0].join('\n'), +// tlyric: '', +// } +// if (lyrics[1]) lyricInfo.tlyric = lyrics[1].join('\n') +// if (lyrics[2]) lyricInfo.rlyric = lyrics[2].join('\n') + +// return lyricInfo +// } + +type IComment = NonNullable extends Array ? U : never + +/** + * 获取歌曲文件歌词 + * @param path 路径 + */ +export const getLocalMusicFileLyric = async(path: string): Promise => { + // 尝试读取同目录下的同名lrc文件 + const filePath = new RegExp('\\' + extname(path) + '$') + let lrcPath = path.replace(filePath, '.lrc') + let stats = await getFileStats(lrcPath) + // console.log(lrcPath, stats) + if (stats && stats.size < 1024 * 1024 * 10) { + const lrcBuf = await readFile(lrcPath) + const { detect } = await import('jschardet') + const { confidence, encoding } = detect(lrcBuf) + console.log('lrc file encoding', confidence, encoding) + if (confidence > 0.8) { + const iconv = (await import('iconv-lite')).default + if (iconv.encodingExists(encoding)) { + const lrc = iconv.decode(lrcBuf, encoding) + if (lrc) { + return { + lyric: lrc, + } + } + } + } + } + // 尝试读取同目录下的同名krc文件 + lrcPath = path.replace(filePath, '.krc') + stats = await getFileStats(lrcPath) + console.log(lrcPath, stats?.size) + if (stats && stats.size < 1024 * 1024 * 10) { + const lrcBuf = await readFile(lrcPath) + try { + return await decodeKrc(lrcBuf) + } catch (e) { + console.log(e) + } + } + + + // 尝试读取文件内歌词 + const metadata = await getFileMetadata(path) + // console.log(metadata?.common) + if (!metadata) return null + // let lyricInfo = metadata.common.lyrics?.[0] + // if (lyricInfo) { + // let lyric: string | undefined + // if (typeof lyricInfo == 'object') lyric = lyricInfo.text + // else if (typeof lyricInfo == 'string') lyric = lyricInfo + // if (lyric && lyric.length > 10) { + // return { lyric } + // } + // } + // console.log(metadata) + for (const info of Object.values(metadata.native)) { + for (const ust of info) { + switch (ust.id) { + case 'LYRICS': { + const value = typeof ust.value == 'string' ? ust.value : (ust as IComment).text + if (value && value.length > 10) return { lyric: value } + break + } + case 'USLT': { + const value = ust.value as IComment + if (value.text && value.text.length > 10) return { lyric: value.text } + break + } + } + } + } + return null +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/api-source-info.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/api-source-info.ts new file mode 100644 index 000000000..65cf20e25 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/api-source-info.ts @@ -0,0 +1,32 @@ +// Support qualitys: 128k 320k flac wav + +const sources: Array<{ + id: string + name: string + disabled: boolean + supportQualitys: Partial> +}> = [ + // { + // id: 'test', + // name: '测试接口', + // disabled: false, + // supportQualitys: { + // kw: ['128k'], + // kg: ['128k'], + // tx: ['128k'], + // wy: ['128k'], + // mg: ['128k'], + // // bd: ['128k'], + // }, + // }, + // { + // id: 'temp', + // name: '临时接口', + // disabled: false, + // supportQualitys: { + // kw: ['128k'], + // }, + // }, +] + +export default sources diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/api-source.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/api-source.js new file mode 100644 index 000000000..da6d07d43 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/api-source.js @@ -0,0 +1,40 @@ +import apiSourceInfo from './api-source-info' +import { apiSource, userApi } from '@renderer/store' +// import api_temp_kw from './kw/api-temp' +// // import api_test_bd from './bd/api-test' +// import api_test_tx from './tx/api-test' +// import api_test_kg from './kg/api-test' +// import api_test_kw from './kw/api-test' +// import api_test_mg from './mg/api-test' +// import api_test_wy from './wy/api-test' + +const allApi = { + // temp_kw: api_temp_kw, + // // test_bd: api_test_bd, + // test_tx: api_test_tx, + // test_kg: api_test_kg, + // test_kw: api_test_kw, + // test_mg: api_test_mg, + // test_wy: api_test_wy, +} + +const apiList = {} +const supportQuality = {} + +for (const api of apiSourceInfo) { + supportQuality[api.id] = api.supportQualitys + for (const source of Object.keys(api.supportQualitys)) { + apiList[`${api.id}_api_${source}`] = allApi[`${api.id}_${source}`] + } +} + +const getAPI = source => apiList[`${apiSource.value}_api_${source}`] + +const apis = source => { + if (/^user_api/.test(apiSource.value)) return userApi.apis[source] + let api = getAPI(source) + if (api) return api + throw new Error('Api is not found') +} + +export { apis, supportQuality } diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/api-test.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/api-test.js new file mode 100644 index 000000000..dfeffdbb8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/api-test.js @@ -0,0 +1,26 @@ +import { httpFetch } from '../../request' +import { requestMsg } from '../../message' +import { headers, timeout } from '../options' +import { dnsLookup } from '../utils' + +const api_test = { + getMusicUrl(songInfo, type) { + const requestObj = httpFetch(`http://ts.tempmusics.tk/url/bd/${songInfo.songmid}/${type}`, { + method: 'get', + timeout, + headers, + lookup: dnsLookup, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { + if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) + switch (body.code) { + case 0: return Promise.resolve({ type, url: body.data }) + default: return Promise.reject(new Error(requestMsg.fail)) + } + }) + return requestObj + }, +} + +export default api_test diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/hotSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/hotSearch.js new file mode 100644 index 000000000..5a9cbf2c4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/hotSearch.js @@ -0,0 +1,23 @@ +import { httpFetch } from '../../request' + +export default { + _requestObj: null, + async getList(retryNum = 0) { + if (this._requestObj) this._requestObj.cancelHttp() + if (retryNum > 2) return Promise.reject(new Error('try max num')) + + const _requestObj = httpFetch('http://musicapi.qianqian.com/v1/restserver/ting?from=android&version=7.0.2.0&channel=ppzs&operator=0&method=baidu.ting.search.hot', { + method: 'get', + headers: { + 'User-Agent': 'android_7.0.2.0;baiduyinyue', + }, + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.error_code !== 22000) throw new Error('获取热搜词失败') + // console.log(body, statusCode) + return { source: 'bd', list: this.filterList(body.result) } + }, + filterList(rawList) { + return rawList.map(item => item.word) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/index.js new file mode 100644 index 000000000..adc4d6235 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/index.js @@ -0,0 +1,40 @@ +import leaderboard from './leaderboard' +import { apis } from '../api-source' +import musicInfo from './musicInfo' +import songList from './songList' +import { httpFetch } from '../../request' +import musicSearch from './musicSearch' +import hotSearch from './hotSearch' + +const bd = { + leaderboard, + songList, + musicSearch, + hotSearch, + getMusicUrl(songInfo, type) { + return apis('bd').getMusicUrl(songInfo, type) + }, + getPic(songInfo) { + const requestObj = this.getMusicInfo(songInfo) + return requestObj.promise.then(info => info.pic_premium) + }, + getLyric(songInfo) { + const requestObj = this.getMusicInfo(songInfo) + requestObj.promise = requestObj.promise.then(info => httpFetch(info.lrclink).promise.then(resp => ({ lyric: resp.body, tlyric: '' }))) + return requestObj + }, + // getLyric(songInfo) { + // return apis('bd').getLyric(songInfo) + // }, + // getPic(songInfo) { + // return apis('bd').getPic(songInfo) + // }, + getMusicInfo(songInfo) { + return musicInfo.getMusicInfo(songInfo.songmid) + }, + getMusicDetailPageUrl(songInfo) { + return `http://music.taihe.com/song/${songInfo.songmid}` + }, +} + +export default bd diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/leaderboard.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/leaderboard.js new file mode 100644 index 000000000..21f2c6406 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/leaderboard.js @@ -0,0 +1,153 @@ +import { httpFetch } from '../../request' +// import { formatPlayTime } from '../../index' + + +const boardList = [ + // { id: 'bd__601', name: '歌单榜', bangid: '601' }, + { id: 'bd__2', name: '热歌榜', bangid: '2' }, + { id: 'bd__20', name: '华语金曲榜', bangid: '20' }, + { id: 'bd__25', name: '网络歌曲榜', bangid: '25' }, + { id: 'bd__1', name: '新歌榜', bangid: '1' }, + { id: 'bd__21', name: '欧美金曲榜', bangid: '21' }, + { id: 'bd__200', name: '原创音乐榜', bangid: '200' }, + { id: 'bd__22', name: '经典老歌榜', bangid: '22' }, + { id: 'bd__24', name: '影视金曲榜', bangid: '24' }, + { id: 'bd__23', name: '情歌对唱榜', bangid: '23' }, + { id: 'bd__11', name: '摇滚榜', bangid: '11' }, + { id: 'bd__105', name: '好童星榜', bangid: '105' }, + { id: 'bd__106', name: '雅克•藏羌彝原创音乐榜', bangid: '106' }, +] + +export default { + limit: 20, + list: [ + { + id: 'bdrgb', + name: '热歌榜', + bangid: '2', + }, + { + id: 'bdxgb', + name: '新歌榜', + bangid: '1', + }, + { + id: 'bdycb', + name: '原创榜', + bangid: '200', + }, + { + id: 'bdhyjqb', + name: '华语榜', + bangid: '20', + }, + { + id: 'bdomjqb', + name: '欧美榜', + bangid: '21', + }, + { + id: 'bdwugqb', + name: '网络榜', + bangid: '25', + }, + { + id: 'bdjdlgb', + name: '老歌榜', + bangid: '22', + }, + { + id: 'bdysjqb', + name: '影视金曲榜', + bangid: '24', + }, + { + id: 'bdqgdcb', + name: '情歌对唱榜', + bangid: '23', + }, + { + id: 'bdygb', + name: '摇滚榜', + bangid: '11', + }, + ], + getUrl(id, p) { + return `http://musicmini.qianqian.com/2018/static/bangdan/bangdanList_${id}_${p}.html` + }, + regExps: { + item: /data-song="({.+?})"/g, + info: /{total[\s:]+"(\d+)", size[\s:]+"(\d+)", page[\s:]+"(\d+)"}/, + }, + getData(url) { + const requestObj = httpFetch(url) + return requestObj.promise + }, + filterData(rawList) { + // console.log(rawList) + return rawList.map(item => { + const types = [] + const _types = {} + let size = null + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + if (item.biaoshi) { + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + // types.reverse() + + return { + singer: item.song_artist.replace(',', '、'), + name: item.song_title, + albumName: item.album_title, + albumId: item.album_id, + source: 'bd', + interval: '', + songmid: item.song_id, + img: null, + lrc: null, + types, + _types, + typeUrl: {}, + } + }) + }, + parseData(rawData) { + // return rawData.map(item => JSON.parse(item.replace(this.regExps.item, '$1').replace(/"/g, '"').replace(/\\\//g, '/').replace(/(@s_1,w_)\d+(,h_)\d+/, '$1500$2500'))) + return rawData.map(item => JSON.parse(item.replace(this.regExps.item, '$1').replace(/"/g, '"').replace(/\\\//g, '/'))) + }, + async getBoards(retryNum = 0) { + this.list = boardList + return { + list: boardList, + source: 'bd', + } + }, + getList(bangid, page, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + return this.getData(this.getUrl(bangid, page)).then(({ body }) => { + let result = body.match(this.regExps.item) + if (!result) return this.getList(bangid, page, retryNum) + let info = body.match(this.regExps.info) + if (!info) return this.getList(bangid, page, retryNum) + const list = this.filterData(this.parseData(result)) + this.limit = parseInt(info[2]) + return { + total: parseInt(info[1]), + list, + limit: this.limit, + page: parseInt(info[3]), + source: 'bd', + } + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/musicInfo.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/musicInfo.js new file mode 100644 index 000000000..7c6c0b0bf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/musicInfo.js @@ -0,0 +1,20 @@ +import { httpFetch } from '../../request' + +export default { + cache: {}, + getMusicInfo(songmid) { + if (this.cache[songmid]) { + return { promise: Promise.resolve(this.cache[songmid]) } + } + const requestObj = httpFetch(`https://musicapi.qianqian.com/v1/restserver/ting?method=baidu.ting.song.getSongLink&format=json&from=bmpc&version=1.0.0&version_d=11.1.6.0&songid=${songmid}&type=1&res=1&s_protocol=1&aac=2&project=tpass`) + requestObj.promise = requestObj.promise.then(({ body }) => { + // console.log(body) + if (body.error_code == 22000) { + this.cache[songmid] = body.result.songinfo + return body.result.songinfo + } + return Promise.reject(new Error('获取音乐信息失败')) + }) + return requestObj + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/musicSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/musicSearch.js new file mode 100644 index 000000000..5ec25890a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/musicSearch.js @@ -0,0 +1,88 @@ +// import '../../polyfill/array.find' + +import { httpFetch } from '../../request' +import { formatPlayTime } from '../../index' +// import { debug } from '../../utils/env' +// import { formatSinger } from './util' + +export default { + limit: 30, + total: 0, + page: 0, + allPage: 1, + musicSearch(str, page, limit) { + const searchRequest = httpFetch(`http://tingapi.ting.baidu.com/v1/restserver/ting?from=android&version=5.6.5.6&method=baidu.ting.search.merge&format=json&query=${encodeURIComponent(str)}&page_no=${page}&page_size=${limit}&type=0&data_source=0&use_cluster=1`) + return searchRequest.promise.then(({ body }) => body) + }, + handleResult(rawData) { + let ids = new Set() + const list = [] + if (!rawData) return list + rawData.forEach(item => { + if (ids.has(item.song_id)) return + ids.add(item.song_id) + const types = [] + const _types = {} + let size = null + let itemTypes = item.all_rate.split(',') + if (itemTypes.includes('128')) { + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (itemTypes.includes('320')) { + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (itemTypes.includes('flac')) { + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + // types.reverse() + + list.push({ + singer: item.author.replace(',', '、'), + name: item.title, + albumName: item.album_title, + albumId: item.album_id, + source: 'bd', + interval: formatPlayTime(parseInt(item.file_duration)), + songmid: item.song_id, + img: null, + lrc: null, + types, + _types, + typeUrl: {}, + }) + }) + return list + }, + search(str, page = 1, limit, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + if (limit == null) limit = this.limit + + return this.musicSearch(str, page, limit).then(result => { + if (!result || result.error_code !== 22000) return this.search(str, page, limit, retryNum) + let list = this.handleResult(result.result.song_info.song_list) + + if (list == null) return this.search(str, page, limit, retryNum) + + this.total = result.result.song_info.total + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + limit, + total: this.total, + source: 'bd', + }) + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/songList.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/songList.js new file mode 100644 index 000000000..90fddb12b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/bd/songList.js @@ -0,0 +1,262 @@ +import { httpFetch } from '../../request' +import { formatPlayTime, toMD5 } from '../../index' +import CryptoJS from 'crypto-js' + +export default { + _requestObj_tags: null, + _requestObj_list: null, + _requestObj_listRecommend: null, + limit_list: 30, + limit_song: 10000, + successCode: 22000, + sortList: [ + { + name: '最热', + id: '1', + }, + { + name: '最新', + id: '0', + }, + ], + regExps: { + // http://music.taihe.com/songlist/566347741 + listDetailLink: /^.+\/songlist\/(\d+)(?:\?.*|&.*$|#.*$|$)/, + }, + aesPassEncod(jsonData) { + let timestamp = Math.floor(Date.now() / 1000) + let privateKey = toMD5('baidu_taihe_music_secret_key' + timestamp).substr(8, 16) + let key = CryptoJS.enc.Utf8.parse(privateKey) + let iv = CryptoJS.enc.Utf8.parse(privateKey) + let arrData = [] + let strData = '' + for (let key in jsonData) arrData.push(key) + arrData.sort() + for (let i = 0; i < arrData.length; i++) { + let key = arrData[i] + strData += + (i === 0 ? '' : '&') + key + '=' + encodeURIComponent(jsonData[key]) + } + let JsonFormatter = { + stringify(cipherParams) { + let jsonObj = { + ct: cipherParams.ciphertext.toString(CryptoJS.enc.Base64), + } + if (cipherParams.iv) { + jsonObj.iv = cipherParams.iv.toString() + } + if (cipherParams.salt) { + jsonObj.s = cipherParams.salt.toString() + } + return jsonObj + }, + parse(jsonStr) { + let jsonObj = JSON.parse(jsonStr) + let cipherParams = CryptoJS.lib.CipherParams.create({ + ciphertext: CryptoJS.enc.Base64.parse(jsonObj.ct), + }) + if (jsonObj.iv) { + cipherParams.iv = CryptoJS.enc.Hex.parse(jsonObj.iv) + } + if (jsonObj.s) { + cipherParams.salt = CryptoJS.enc.Hex.parse(jsonObj.s) + } + return cipherParams + }, + } + let encrypted = CryptoJS.AES.encrypt(strData, key, { + iv, + blockSize: 16, + mode: CryptoJS.mode.CBC, + format: JsonFormatter, + }) + let ciphertext = encrypted.toString().ct + let sign = toMD5('baidu_taihe_music' + ciphertext + timestamp) + let jsonRet = { + timestamp, + param: ciphertext, + sign, + } + return jsonRet + }, + createUrl(param, method) { + let data = this.aesPassEncod(param) + return `http://musicmini.qianqian.com/v1/restserver/ting?method=${method}&time=${Date.now()}×tamp=${data.timestamp}¶m=${data.param}&sign=${data.sign}` + }, + getTagsUrl() { + return this.createUrl({ + from: 'qianqianmini', + type: 'diy', + version: '10.1.8', + }, 'baidu.ting.ugcdiy.getChannels') + }, + getListUrl(sortType, tagName, page) { + return this.createUrl({ + channelname: tagName || '全部', + from: 'qianqianmini', + offset: (page - 1) * this.limit_list, + order_type: sortType, + size: this.limit_list, + version: '10.1.8', + }, 'baidu.ting.ugcdiy.getChanneldiy') + }, + getListDetailUrl(list_id, page) { + return this.createUrl({ + list_id, + offset: (page - 1) * this.limit_song, + size: this.limit_song, + withcount: '1', + withsong: '1', + }, 'baidu.ting.ugcdiy.getBaseInfo') + }, + + // 获取标签 + getTags(tryNum = 0) { + if (this._requestObj_tags) this._requestObj_tags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_tags = httpFetch(this.getTagsUrl()) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.error_code !== this.successCode) return this.getTags(++tryNum) + return { + hotTag: this.filterInfoHotTag(body.result.hot), + tags: this.filterTagInfo(body.result.tags), + source: 'bd', + } + }) + }, + filterInfoHotTag(rawList) { + return rawList.map(item => ({ + name: item, + id: item, + source: 'bd', + })) + }, + filterTagInfo(rawList) { + return rawList.map(type => ({ + name: type.first, + list: type.second.map(item => ({ + parent_id: type.first, + parent_name: type.first, + id: item, + name: item, + source: 'bd', + })), + })) + }, + + // 获取列表数据 + getList(sortId, tagId, page, tryNum = 0) { + if (this._requestObj_list) this._requestObj_list.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_list = httpFetch(this.getListUrl(sortId, tagId, page)) + return this._requestObj_list.promise.then(({ body }) => { + if (body.error_code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) + return { + list: this.filterList(body.diyInfo), + total: body.nums, + page, + limit: this.limit_list, + source: 'bd', + } + }) + }, + + + /** + * 格式化播放数量 + * @param {*} num + */ + formatPlayCount(num) { + if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿' + if (num > 10000) return parseInt(num / 1000) / 10 + '万' + return num + }, + filterList(rawData) { + return rawData.map(item => ({ + play_count: this.formatPlayCount(item.listen_num), + id: String(item.list_id), + author: item.username, + name: item.title, + // time: item.publish_time, + img: item.list_pic_large || item.list_pic, + grade: item.grade, + desc: item.desc || item.tag, + source: 'bd', + })) + }, + + // 获取歌曲列表内的音乐 + getListDetail(id, page, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + + if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + + const requestObj_listDetail = httpFetch(this.getListDetailUrl(id, page)) + return requestObj_listDetail.promise.then(({ body }) => { + if (body.error_code !== this.successCode) return this.getListDetail(id, page, ++tryNum) + let listData = this.filterData(body.result.songlist) + return { + list: listData, + page, + limit: this.limit_song, + total: body.result.song_num, + source: 'bd', + info: { + name: body.result.info.list_title, + img: body.result.info.list_pic, + desc: body.result.info.list_desc, + author: body.result.info.userinfo.username, + play_count: this.formatPlayCount(body.result.listen_num), + }, + } + }) + }, + filterData(rawList) { + // console.log(rawList) + return rawList.map(item => { + const types = [] + const _types = {} + let size = null + let itemTypes = item.all_rate.split(',') + if (itemTypes.includes('128')) { + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (itemTypes.includes('320')) { + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (itemTypes.includes('flac')) { + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + // types.reverse() + + return { + singer: item.author.replace(',', '、'), + name: item.title, + albumName: item.album_title, + albumId: item.album_id, + source: 'bd', + interval: formatPlayTime(parseInt(item.file_duration)), + songmid: item.song_id, + img: item.pic_s500, + lrc: null, + types, + _types, + typeUrl: {}, + } + }) + }, + +} + +// getList +// getTags +// getListDetail diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/index.js new file mode 100644 index 000000000..e0b53d191 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/index.js @@ -0,0 +1,176 @@ +import kw from './kw/index' +import kg from './kg/index' +import tx from './tx/index' +import wy from './wy/index' +import mg from './mg/index' +import bd from './bd/index' +import xm from './xm' +import { supportQuality } from './api-source' +import { versionChars } from './versionChars' + + +const sources = { + sources: [ + { + name: '酷我音乐', + id: 'kw', + }, + { + name: '酷狗音乐', + id: 'kg', + }, + { + name: 'QQ音乐', + id: 'tx', + }, + { + name: '网易音乐', + id: 'wy', + }, + { + name: '咪咕音乐', + id: 'mg', + }, + { + name: '虾米音乐', + id: 'xm', + }, + // { + // name: '百度音乐', + // id: 'bd', + // }, + ], + kw, + kg, + tx, + wy, + mg, + bd, + xm, +} +export default { + ...sources, + init() { + const tasks = [] + for (let source of sources.sources) { + let sm = sources[source.id] + sm && sm.init && tasks.push(sm.init()) + } + return Promise.all(tasks) + }, + supportQuality, + + async searchMusic({ name, singer, source: s, limit = 25 }) { + const trimStr = str => typeof str == 'string' ? str.trim() : str + const musicName = trimStr(name) + const tasks = [] + const excludeSource = ['xm'] + for (const source of sources.sources) { + if (!sources[source.id].musicSearch || source.id == s || excludeSource.includes(source.id)) continue + tasks.push(sources[source.id].musicSearch.search(`${musicName} ${singer || ''}`.trim(), 1, limit).catch(_ => null)) + } + return (await Promise.all(tasks)).filter(s => s) + }, + + async findMusic({ name, singer, albumName, interval, source: s }) { + const lists = await this.searchMusic({ name, singer, source: s, limit: 25 }) + // console.log(lists) + // console.log({ name, singer, albumName, interval, source: s }) + + const singersRxp = /、|&|;|;|\/|,|,|\|/ + const sortSingle = singer => singersRxp.test(singer) + ? singer.split(singersRxp).sort((a, b) => a.localeCompare(b)).join('、') + : (singer || '') + const sortMusic = (arr, callback) => { + const tempResult = [] + for (let i = arr.length - 1; i > -1; i--) { + const item = arr[i] + if (callback(item)) { + delete item.fSinger + delete item.fMusicName + delete item.fAlbumName + delete item.fInterval + tempResult.push(item) + arr.splice(i, 1) + } + } + tempResult.reverse() + return tempResult + } + const getIntv = (interval) => { + if (!interval) return 0 + // if (musicInfo._interval) return musicInfo._interval + let intvArr = interval.split(':') + let intv = 0 + let unit = 1 + while (intvArr.length) { + intv += parseInt(intvArr.pop()) * unit + unit *= 60 + } + return intv + } + const trimStr = str => typeof str == 'string' ? str.trim() : (str || '') + const filterStr = str => typeof str == 'string' ? str.replace(/\s|'|\.|,|,|&|"|、|\(|\)|(|)|`|~|-|<|>|\||\/|\]|\[|!|!/g, '') : String(str || '') + const fMusicName = filterStr(name).toLowerCase() + const fSinger = filterStr(sortSingle(singer)).toLowerCase() + const fAlbumName = filterStr(albumName).toLowerCase() + const fInterval = getIntv(interval) + const isEqualsInterval = (intv) => Math.abs((fInterval || intv) - (intv || fInterval)) <= 5 + const isEqualsVersionMusicNameChar = (name) => { + for (const char of versionChars) { + if (name.includes(char) != fMusicName.includes(char)) return false + } + return true + } + const isIncludesName = (name) => (fMusicName.includes(name) || name.includes(fMusicName)) && isEqualsVersionMusicNameChar(name) + const isIncludesSinger = (singer) => fSinger ? (fSinger.includes(singer) || singer.includes(fSinger)) : true + const isEqualsAlbum = (album) => fAlbumName ? fAlbumName == album : true + + const result = lists.map(source => { + for (const item of source.list) { + item.name = trimStr(item.name) + item.singer = trimStr(item.singer) + item.fSinger = filterStr(sortSingle(item.singer).toLowerCase()) + item.fMusicName = filterStr(String(item.name ?? '').toLowerCase()) + item.fAlbumName = filterStr(String(item.albumName ?? '').toLowerCase()) + item.fInterval = getIntv(item.interval) + // console.log(fMusicName, item.fMusicName, item.source) + if (!isEqualsInterval(item.fInterval)) { + item.name = null + continue + } + if (item.fMusicName == fMusicName && isIncludesSinger(item.fSinger)) return item + } + for (const item of source.list) { + if (item.name == null) continue + if (item.fSinger == fSinger && isIncludesName(item.fMusicName)) return item + } + for (const item of source.list) { + if (item.name == null) continue + if (isEqualsAlbum(item.fAlbumName) && isIncludesSinger(item.fSinger) && isIncludesName(item.fMusicName)) return item + } + return null + }).filter(s => s) + const newResult = [] + if (result.length) { + newResult.push(...sortMusic(result, item => item.fSinger == fSinger && item.fMusicName == fMusicName && item.interval == interval)) + newResult.push(...sortMusic(result, item => item.fMusicName == fMusicName && item.fSinger == fSinger && item.fAlbumName == fAlbumName)) + newResult.push(...sortMusic(result, item => item.fSinger == fSinger && item.fMusicName == fMusicName)) + newResult.push(...sortMusic(result, item => item.fMusicName == fMusicName && item.interval == interval)) + newResult.push(...sortMusic(result, item => item.fSinger == fSinger && item.interval == interval)) + newResult.push(...sortMusic(result, item => item.interval == interval)) + newResult.push(...sortMusic(result, item => item.fMusicName == fMusicName)) + newResult.push(...sortMusic(result, item => item.fSinger == fSinger)) + newResult.push(...sortMusic(result, item => item.fAlbumName == fAlbumName)) + for (const item of result) { + delete item.fSinger + delete item.fMusicName + delete item.fAlbumName + delete item.fInterval + } + newResult.push(...result) + } + // console.log(newResult) + return newResult + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/album.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/album.js new file mode 100644 index 000000000..1d9642f8e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/album.js @@ -0,0 +1,63 @@ +import { getMusicInfosByList } from './musicInfo' +import { createHttpFetch } from './util' + +export default { + /** + * 通过AlbumId获取专辑信息 + * @param {*} id + */ + async getAlbumInfo(id) { + const albumInfoRequest = await createHttpFetch('http://kmrserviceretry.kugou.com/container/v1/album?dfid=1tT5He3kxrNC4D29ad1MMb6F&mid=22945702112173152889429073101964063697&userid=0&appid=1005&clientver=11589', { + method: 'POST', + body: { + appid: 1005, + clienttime: 1681833686, + clientver: 11589, + data: [{ album_id: id }], + fields: 'language,grade_count,intro,mix_intro,heat,category,sizable_cover,cover,album_name,type,quality,publish_company,grade,special_tag,author_name,publish_date,language_id,album_id,exclusive,is_publish,trans_param,authors,album_tag', + isBuy: 0, + key: 'e6f3306ff7e2afb494e89fbbda0becbf', + mid: '22945702112173152889429073101964063697', + show_album_tag: 0, + }, + }) + if (!albumInfoRequest) return Promise.reject(new Error('get album info failed.')) + const albumInfo = albumInfoRequest[0] + + return { + name: albumInfo.album_name, + image: albumInfo.sizable_cover.replace('{size}', 240), + desc: albumInfo.intro, + authorName: albumInfo.author_name, + // play_count: this.formatPlayCount(info.count), + } + }, + /** + * 通过AlbumId获取专辑 + * @param {*} id + * @param {*} page + */ + async getAlbumDetail(id, page = 1, limit = 200) { + const albumList = await createHttpFetch(`http://mobiles.kugou.com/api/v3/album/song?version=9108&albumid=${id}&plat=0&pagesize=${limit}&area_code=0&page=${page}&with_res_tag=0`) + if (!albumList.info) return Promise.reject(new Error('Get album list failed.')) + + let result = await getMusicInfosByList(albumList.info) + + const info = await this.getAlbumInfo(id) + + return { + list: result || [], + page, + limit, + total: albumList.total, + source: 'kg', + info: { + name: info.name, + img: info.image, + desc: info.desc, + author: info.authorName, + // play_count: this.formatPlayCount(info.count), + }, + } + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/api-test.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/api-test.js new file mode 100644 index 000000000..5587dbf8d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/api-test.js @@ -0,0 +1,50 @@ +import { httpFetch } from '../../request' +import { requestMsg } from '../../message' +import { headers, timeout } from '../options' +import { dnsLookup } from '../utils' + +const api_test = { + getMusicUrl(songInfo, type) { + const requestObj = httpFetch(`http://ts.tempmusics.tk/url/kg/${songInfo._types[type].hash}/${type}`, { + method: 'get', + timeout, + headers, + lookup: dnsLookup, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { + if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) + switch (body.code) { + case 0: return Promise.resolve({ type, url: body.data }) + default: return Promise.reject(new Error(requestMsg.fail)) + } + }) + return requestObj + }, + getPic(songInfo) { + const requestObj = httpFetch(`http://ts.tempmusics.tk/pic/kg/${songInfo.hash}`, { + method: 'get', + timeout, + headers, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ body }) => { + return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) + }) + return requestObj + }, + getLyric(songInfo) { + const requestObj = httpFetch(`http://ts.tempmusics.tk/lrc/kg/${songInfo.hash}`, { + method: 'get', + timeout, + headers, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ body }) => { + return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) + }) + return requestObj + }, +} + +export default api_test diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/comment.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/comment.js new file mode 100644 index 000000000..9c1f6df7e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/comment.js @@ -0,0 +1,101 @@ +import { httpFetch } from '../../request' +import { decodeName, dateFormat2 } from '../../index' +import { signatureParams } from './util' +// import { getMusicInfoRaw } from './musicInfo' + +export default { + _requestObj: null, + _requestObj2: null, + async getComment({ hash }, page = 1, limit = 20) { + if (this._requestObj) this._requestObj.cancelHttp() + + // const res_id = (await getMusicInfoRaw(hash)).classification?.[0]?.res_id + // if (!res_id) throw new Error('获取评论失败') + + let timestamp = Date.now() + const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0` + // const params = `appid=1005&clienttime=${timestamp}&clienttoken=0&clientver=11409&code=fc4be23b4e972707f36b8a828a93ba8a&dfid=0&extdata=${hash}&kugouid=0&mid=16249512204336365674023395779019&mixsongid=${res_id}&p=${page}&pagesize=${limit}&uuid=0&ver=10` + const _requestObj = httpFetch(`http://m.comment.service.kugou.com/r/v1/rank/newest?${params}&signature=${signatureParams(params)}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.24', + }, + }) + const { body, statusCode } = await _requestObj.promise + // console.log(body) + if (statusCode != 200 || body.err_code !== 0) throw new Error('获取评论失败') + const total = body.count ?? 0 + return { source: 'kg', comments: this.filterComment(body.list || []), total, page, limit, maxPage: Math.ceil(total / limit) || 1 } + }, + async getHotComment({ hash }, page = 1, limit = 20) { + // console.log(songmid) + if (this._requestObj2) this._requestObj2.cancelHttp() + let timestamp = Date.now() + const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0` + // https://github.com/GitHub-ZC/wp_MusicApi/blob/bf9307dd138dc8ac6c4f7de29361209d4f5b665f/routes/v1/kugou/comment.js#L53 + const _requestObj2 = httpFetch(`http://m.comment.service.kugou.com/r/v1/rank/topliked?${params}&signature=${signatureParams(params)}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.24', + }, + }) + const { body, statusCode } = await _requestObj2.promise + // console.log(body) + if (statusCode != 200 || body.err_code !== 0) throw new Error('获取热门评论失败') + const total = body.count ?? 0 + return { source: 'kg', comments: this.filterComment(body.list || []), total, page, limit, maxPage: Math.ceil(total / limit) || 1 } + }, + async getReplyComment({ songmid, audioId }, replyId, page = 1, limit = 100) { + if (this._requestObj2) this._requestObj2.cancelHttp() + + songmid = songmid.length == 32 // 修复歌曲ID存储变更导致图片获取失败的问题 + ? audioId.split('_')[0] + : songmid + + const _requestObj2 = httpFetch(`http://comment.service.kugou.com/index.php?r=commentsv2/getReplyWithLike&code=fc4be23b4e972707f36b8a828a93ba8a&p=${page}&pagesize=${limit}&ver=1.01&clientver=8373&kugouid=687373022&need_show_image=1&appid=1001&childrenid=${songmid}&tid=${replyId}`, { + headers: { + 'User-Agent': 'Android712-AndroidPhone-8983-18-0-COMMENT-wifi', + }, + }) + const { body, statusCode } = await _requestObj2.promise + // console.log(body) + if (statusCode != 200 || body.err_code !== 0) throw new Error('获取回复评论失败') + return { source: 'kg', comments: this.filterComment(body.list || []) } + }, + replaceAt(raw, atList) { + atList.forEach((atobj) => { + raw = raw.replaceAll(`[at=${atobj.id}]`, `@${atobj.name} `) + }) + return raw + }, + filterComment(rawList) { + return rawList.map(item => { + let data = { + id: item.id, + text: decodeName((item.atlist ? this.replaceAt(item.content, item.atlist) : item.content) || ''), + images: item.images ? item.images.map(i => i.url) : [], + location: item.location, + time: item.addtime, + timeStr: dateFormat2(new Date(item.addtime).getTime()), + userName: item.user_name, + avatar: item.user_pic, + userId: item.user_id, + likedCount: item.like.likenum, + replyNum: item.reply_num, + reply: [], + } + + return item.pcontent + ? { + id: item.id, + text: decodeName(item.pcontent), + time: null, + userName: item.puser, + avatar: null, + userId: item.puser_id, + likedCount: null, + replyNum: null, + reply: [data], + } + : data + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/hotSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/hotSearch.js new file mode 100644 index 000000000..0d9c3694f --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/hotSearch.js @@ -0,0 +1,33 @@ +import { httpFetch } from '../../request' +import { decodeName } from '../../index' + +export default { + _requestObj: null, + async getList(retryNum = 0) { + if (this._requestObj) this._requestObj.cancelHttp() + if (retryNum > 2) return Promise.reject(new Error('try max num')) + + const _requestObj = httpFetch('http://gateway.kugou.com/api/v3/search/hot_tab?signature=ee44edb9d7155821412d220bcaf509dd&appid=1005&clientver=10026&plat=0', { + method: 'get', + headers: { + dfid: '1ssiv93oVqMp27cirf2CvoF1', + mid: '156798703528610303473757548878786007104', + clienttime: 1584257267, + 'x-router': 'msearch.kugou.com', + 'user-agent': 'Android9-AndroidPhone-10020-130-0-searchrecommendprotocol-wifi', + 'kg-rc': 1, + }, + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.errcode !== 0) throw new Error('获取热搜词失败') + // console.log(body, statusCode) + return { source: 'kg', list: this.filterList(body.data.list) } + }, + filterList(rawList) { + const list = [] + rawList.forEach(item => { + item.keywords.map(k => list.push(decodeName(k.keyword))) + }) + return list + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/index.js new file mode 100644 index 000000000..5d41846c9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/index.js @@ -0,0 +1,38 @@ +import leaderboard from './leaderboard' +import { apis } from '../api-source' +import songList from './songList' +import musicSearch from './musicSearch' +import pic from './pic' +import lyric from './lyric' +import hotSearch from './hotSearch' +import comment from './comment' +// import tipSearch from './tipSearch' + +const kg = { + // tipSearch, + leaderboard, + songList, + musicSearch, + hotSearch, + comment, + getMusicUrl(songInfo, type) { + return apis('kg').getMusicUrl(songInfo, type) + }, + getLyric(songInfo) { + return lyric.getLyric(songInfo) + }, + // getLyric(songInfo) { + // return apis('kg').getLyric(songInfo) + // }, + getPic(songInfo) { + return pic.getPic(songInfo) + }, + getMusicDetailPageUrl(songInfo) { + return `https://www.kugou.com/song/#hash=${songInfo.hash}&album_id=${songInfo.albumId}` + }, + // getPic(songInfo) { + // return apis('kg').getPic(songInfo) + // }, +} + +export default kg diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/leaderboard.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/leaderboard.js new file mode 100644 index 000000000..155ae62f9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/leaderboard.js @@ -0,0 +1,209 @@ +import { httpFetch } from '../../request' +import { decodeName, formatPlayTime, sizeFormate } from '../../index' +import { formatSingerName } from '../utils' + +let boardList = [{ id: 'kg__8888', name: 'TOP500', bangid: '8888' }, { id: 'kg__6666', name: '飙升榜', bangid: '6666' }, { id: 'kg__59703', name: '蜂鸟流行音乐榜', bangid: '59703' }, { id: 'kg__52144', name: '抖音热歌榜', bangid: '52144' }, { id: 'kg__52767', name: '快手热歌榜', bangid: '52767' }, { id: 'kg__24971', name: 'DJ热歌榜', bangid: '24971' }, { id: 'kg__23784', name: '网络红歌榜', bangid: '23784' }, { id: 'kg__44412', name: '说唱先锋榜', bangid: '44412' }, { id: 'kg__31308', name: '内地榜', bangid: '31308' }, { id: 'kg__33160', name: '电音榜', bangid: '33160' }, { id: 'kg__31313', name: '香港地区榜', bangid: '31313' }, { id: 'kg__51341', name: '民谣榜', bangid: '51341' }, { id: 'kg__54848', name: '台湾地区榜', bangid: '54848' }, { id: 'kg__31310', name: '欧美榜', bangid: '31310' }, { id: 'kg__33162', name: 'ACG新歌榜', bangid: '33162' }, { id: 'kg__31311', name: '韩国榜', bangid: '31311' }, { id: 'kg__31312', name: '日本榜', bangid: '31312' }, { id: 'kg__49225', name: '80后热歌榜', bangid: '49225' }, { id: 'kg__49223', name: '90后热歌榜', bangid: '49223' }, { id: 'kg__49224', name: '00后热歌榜', bangid: '49224' }, { id: 'kg__33165', name: '粤语金曲榜', bangid: '33165' }, { id: 'kg__33166', name: '欧美金曲榜', bangid: '33166' }, { id: 'kg__33163', name: '影视金曲榜', bangid: '33163' }, { id: 'kg__51340', name: '伤感榜', bangid: '51340' }, { id: 'kg__35811', name: '会员专享榜', bangid: '35811' }, { id: 'kg__37361', name: '雷达榜', bangid: '37361' }, { id: 'kg__21101', name: '分享榜', bangid: '21101' }, { id: 'kg__46910', name: '综艺新歌榜', bangid: '46910' }, { id: 'kg__30972', name: '酷狗音乐人原创榜', bangid: '30972' }, { id: 'kg__60170', name: '闽南语榜', bangid: '60170' }, { id: 'kg__65234', name: '儿歌榜', bangid: '65234' }, { id: 'kg__4681', name: '美国BillBoard榜', bangid: '4681' }, { id: 'kg__25028', name: 'Beatport电子舞曲榜', bangid: '25028' }, { id: 'kg__4680', name: '英国单曲榜', bangid: '4680' }, { id: 'kg__38623', name: '韩国Melon音乐榜', bangid: '38623' }, { id: 'kg__42807', name: 'joox本地热歌榜', bangid: '42807' }, { id: 'kg__36107', name: '小语种热歌榜', bangid: '36107' }, { id: 'kg__4673', name: '日本公信榜', bangid: '4673' }, { id: 'kg__46868', name: '日本SPACE SHOWER榜', bangid: '46868' }, { id: 'kg__42808', name: 'KKBOX风云榜', bangid: '42808' }, { id: 'kg__60171', name: '越南语榜', bangid: '60171' }, { id: 'kg__60172', name: '泰语榜', bangid: '60172' }, { id: 'kg__59895', name: 'R&B榜', bangid: '59895' }, { id: 'kg__59896', name: '摇滚榜', bangid: '59896' }, { id: 'kg__59897', name: '爵士榜', bangid: '59897' }, { id: 'kg__59898', name: '乡村音乐榜', bangid: '59898' }, { id: 'kg__59900', name: '纯音乐榜', bangid: '59900' }, { id: 'kg__59899', name: '古典榜', bangid: '59899' }, { id: 'kg__22603', name: '5sing音乐榜', bangid: '22603' }, { id: 'kg__21335', name: '繁星音乐榜', bangid: '21335' }, { id: 'kg__33161', name: '古风新歌榜', bangid: '33161' }] + +export default { + listDetailLimit: 100, + list: [ + { + id: 'kgtop500', + name: 'TOP500', + bangid: '8888', + }, + { + id: 'kgwlhgb', + name: '网络榜', + bangid: '23784', + }, + { + id: 'kgbsb', + name: '飙升榜', + bangid: '6666', + }, + { + id: 'kgfxb', + name: '分享榜', + bangid: '21101', + }, + { + id: 'kgcyyb', + name: '纯音乐榜', + bangid: '33164', + }, + { + id: 'kggfjqb', + name: '古风榜', + bangid: '33161', + }, + { + id: 'kgyyjqb', + name: '粤语榜', + bangid: '33165', + }, + { + id: 'kgomjqb', + name: '欧美榜', + bangid: '33166', + }, + { + id: 'kgdyrgb', + name: '电音榜', + bangid: '33160', + }, + { + id: 'kgjdrgb', + name: 'DJ热歌榜', + bangid: '24971', + }, + { + id: 'kghyxgb', + name: '华语新歌榜', + bangid: '31308', + }, + ], + getUrl(p, id, limit) { + return `http://mobilecdnbj.kugou.com/api/v3/rank/song?version=9108&ranktype=1&plat=0&pagesize=${limit}&area_code=1&page=${p}&rankid=${id}&with_res_tag=0&show_portrait_mv=1` + }, + regExps: { + total: /total: '(\d+)',/, + page: /page: '(\d+)',/, + limit: /pagesize: '(\d+)',/, + listData: /global\.features = (\[.+\]);/, + }, + _requestBoardsObj: null, + getBoardsData() { + if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp() + this._requestBoardsObj = httpFetch('http://mobilecdnbj.kugou.com/api/v5/rank/list?version=9108&plat=0&showtype=2&parentid=0&apiver=6&area_code=1&withsong=1') + return this._requestBoardsObj.promise + }, + getData(url) { + const requestDataObj = httpFetch(url) + return requestDataObj.promise + }, + getSinger(singers) { + let arr = [] + singers.forEach(singer => { + arr.push(singer.author_name) + }) + return arr.join('、') + }, + filterData(rawList) { + // console.log(rawList) + return rawList.map(item => { + const types = [] + const _types = {} + if (item.filesize !== 0) { + let size = sizeFormate(item.filesize) + types.push({ type: '128k', size, hash: item.hash }) + _types['128k'] = { + size, + hash: item.hash, + } + } + if (item['320filesize'] !== 0) { + let size = sizeFormate(item['320filesize']) + types.push({ type: '320k', size, hash: item['320hash'] }) + _types['320k'] = { + size, + hash: item['320hash'], + } + } + if (item.sqfilesize !== 0) { + let size = sizeFormate(item.sqfilesize) + types.push({ type: 'flac', size, hash: item.sqhash }) + _types.flac = { + size, + hash: item.sqhash, + } + } + if (item.filesize_high !== 0) { + let size = sizeFormate(item.filesize_high) + types.push({ type: 'flac24bit', size, hash: item.hash_high }) + _types.flac24bit = { + size, + hash: item.hash_high, + } + } + return { + singer: formatSingerName(item.authors, 'author_name'), + name: decodeName(item.songname), + albumName: decodeName(item.remark), + albumId: item.album_id, + songmid: item.audio_id, + albumAudioId: item.album_audio_id, + source: 'kg', + interval: formatPlayTime(item.duration), + img: null, + lrc: null, + hash: item.hash, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + + filterBoardsData(rawList) { + // console.log(rawList) + let list = [] + for (const board of rawList) { + if (board.isvol != 1) continue + list.push({ + id: 'kg__' + board.rankid, + name: board.rankname, + bangid: String(board.rankid), + }) + } + return list + }, + async getBoards(retryNum = 0) { + // if (++retryNum > 3) return Promise.reject(new Error('try max num')) + // let response + // try { + // response = await this.getBoardsData() + // } catch (error) { + // return this.getBoards(retryNum) + // } + // // console.log(response.body) + // if (response.statusCode !== 200 || response.body.errcode !== 0) return this.getBoards(retryNum) + // const list = this.filterBoardsData(response.body.data.info) + // console.log(list) + // // console.log(JSON.stringify(list)) + // this.list = list + // return { + // list, + // source: 'kg', + // } + this.list = boardList + return { + list: boardList, + source: 'kg', + } + }, + async getList(bangid, page, retryNum = 0) { + if (++retryNum > 3) throw new Error('try max num') + const { body } = await this.getData(this.getUrl(page, bangid, this.listDetailLimit)) + + if (body.errcode != 0) return this.getList(bangid, page, retryNum) + + // console.log(body) + let total = body.data.total + let limit = 100 + let listData = this.filterData(body.data.info) + // console.log(listData) + return { + total, + list: listData, + limit, + page, + source: 'kg', + } + }, + getDetailPageUrl(id) { + if (typeof id == 'string') id = id.replace('kg__', '') + return `https://www.kugou.com/yy/rank/home/1-${id}.html` + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/lyric.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/lyric.js new file mode 100644 index 000000000..2d6f7a08c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/lyric.js @@ -0,0 +1,108 @@ +import { httpFetch } from '../../request' +import { decodeKrc } from '@common/utils/lyricUtils/kg' + +export default { + getIntv(interval) { + if (!interval) return 0 + let intvArr = interval.split(':') + let intv = 0 + let unit = 1 + while (intvArr.length) { + intv += (intvArr.pop()) * unit + unit *= 60 + } + return parseInt(intv) + }, + // getLyric(songInfo, tryNum = 0) { + // let requestObj = httpFetch(`http://m.kugou.com/app/i/krc.php?cmd=100&keyword=${encodeURIComponent(songInfo.name)}&hash=${songInfo.hash}&timelength=${songInfo._interval || this.getIntv(songInfo.interval)}&d=0.38664927426725626`, { + // headers: { + // 'KG-RC': 1, + // 'KG-THash': 'expand_search_manager.cpp:852736169:451', + // 'User-Agent': 'KuGou2012-9020-ExpandSearchManager', + // }, + // }) + // requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { + // if (statusCode !== 200) { + // if (tryNum > 5) return Promise.reject(new Error('歌词获取失败')) + // let tryRequestObj = this.getLyric(songInfo, ++tryNum) + // requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) + // return tryRequestObj.promise + // } + // return { + // lyric: body, + // tlyric: '', + // } + // }) + // return requestObj + // }, + searchLyric(name, hash, time, tryNum = 0) { + let requestObj = httpFetch(`http://lyrics.kugou.com/search?ver=1&man=yes&client=pc&keyword=${encodeURIComponent(name)}&hash=${hash}&timelength=${time}&lrctxt=1`, { + headers: { + 'KG-RC': 1, + 'KG-THash': 'expand_search_manager.cpp:852736169:451', + 'User-Agent': 'KuGou2012-9020-ExpandSearchManager', + }, + }) + requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { + if (statusCode !== 200) { + if (tryNum > 5) return Promise.reject(new Error('歌词获取失败')) + let tryRequestObj = this.searchLyric(name, hash, time, ++tryNum) + requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) + return tryRequestObj.promise + } + if (body.candidates.length) { + let info = body.candidates[0] + return { id: info.id, accessKey: info.accesskey, fmt: (info.krctype == 1 && info.contenttype != 1) ? 'krc' : 'lrc' } + } + return null + }) + return requestObj + }, + getLyricDownload(id, accessKey, fmt, tryNum = 0) { + let requestObj = httpFetch(`http://lyrics.kugou.com/download?ver=1&client=pc&id=${id}&accesskey=${accessKey}&fmt=${fmt}&charset=utf8`, { + headers: { + 'KG-RC': 1, + 'KG-THash': 'expand_search_manager.cpp:852736169:451', + 'User-Agent': 'KuGou2012-9020-ExpandSearchManager', + }, + }) + requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { + if (statusCode !== 200) { + if (tryNum > 5) return Promise.reject(new Error('歌词获取失败')) + let tryRequestObj = this.getLyric(id, accessKey, fmt, ++tryNum) + requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) + return tryRequestObj.promise + } + + switch (body.fmt) { + case 'krc': + return decodeKrc(body.content) + case 'lrc': + return { + lyric: Buffer.from(body.content, 'base64').toString('utf-8'), + tlyric: '', + rlyric: '', + lxlyric: '', + } + default: + return Promise.reject(new Error(`未知歌词格式: ${body.fmt}`)) + } + }) + + return requestObj + }, + getLyric(songInfo, tryNum = 0) { + let requestObj = this.searchLyric(songInfo.name, songInfo.hash, songInfo._interval || this.getIntv(songInfo.interval)) + + requestObj.promise = requestObj.promise.then(result => { + if (!result) return Promise.reject(new Error('Get lyric failed')) + + let requestObj2 = this.getLyricDownload(result.id, result.accessKey, result.fmt) + + requestObj.cancelHttp = requestObj2.cancelHttp.bind(requestObj2) + + return requestObj2.promise + }) + return requestObj + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/musicInfo.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/musicInfo.js new file mode 100644 index 000000000..e05513d32 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/musicInfo.js @@ -0,0 +1,117 @@ +import { decodeName, formatPlayTime, sizeFormate } from '../../index' +import { createHttpFetch } from './util' + +const createGetMusicInfosTask = (hashs) => { + let data = { + area_code: '1', + show_privilege: 1, + show_album_info: '1', + is_publish: '', + appid: 1005, + clientver: 11451, + mid: '1', + dfid: '-', + clienttime: Date.now(), + key: 'OIlwieks28dk2k092lksi2UIkp', + fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname,classification', + } + let list = hashs + let tasks = [] + while (list.length) { + tasks.push(Object.assign({ data: list.slice(0, 100) }, data)) + if (list.length < 100) break + list = list.slice(100) + } + let url = 'http://gateway.kugou.com/v3/album_audio/audio' + return tasks.map(task => createHttpFetch(url, { + method: 'POST', + body: task, + headers: { + 'KG-THash': '13a3164', + 'KG-RC': '1', + 'KG-Fake': '0', + 'KG-RF': '00869891', + 'User-Agent': 'Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi', + 'x-router': 'kmr.service.kugou.com', + }, + }).then(data => data.map(s => s[0]))) +} + +export const filterMusicInfoList = (rawList) => { + // console.log(rawList) + let ids = new Set() + let list = [] + rawList.forEach(item => { + if (!item) return + if (ids.has(item.audio_info.audio_id)) return + ids.add(item.audio_info.audio_id) + const types = [] + const _types = {} + if (item.audio_info.filesize !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize)) + types.push({ type: '128k', size, hash: item.audio_info.hash }) + _types['128k'] = { + size, + hash: item.audio_info.hash, + } + } + if (item.audio_info.filesize_320 !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_320)) + types.push({ type: '320k', size, hash: item.audio_info.hash_320 }) + _types['320k'] = { + size, + hash: item.audio_info.hash_320, + } + } + if (item.audio_info.filesize_flac !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_flac)) + types.push({ type: 'flac', size, hash: item.audio_info.hash_flac }) + _types.flac = { + size, + hash: item.audio_info.hash_flac, + } + } + if (item.audio_info.filesize_high !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_high)) + types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high }) + _types.flac24bit = { + size, + hash: item.audio_info.hash_high, + } + } + list.push({ + singer: decodeName(item.author_name), + name: decodeName(item.songname), + albumName: decodeName(item.album_info.album_name), + albumId: item.album_info.album_id, + songmid: item.audio_info.audio_id, + albumAudioId: item.audio_info.audio_group_id, + source: 'kg', + interval: formatPlayTime(parseInt(item.audio_info.timelength) / 1000), + img: null, + lrc: null, + hash: item.audio_info.hash, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + }) + return list +} + +export const getMusicInfos = async(hashs) => { + return filterMusicInfoList(await Promise.all(createGetMusicInfosTask(hashs)).then(data => data.flat())) +} + +export const getMusicInfoRaw = async(hash) => { + return Promise.all(createGetMusicInfosTask([{ hash }])).then(data => data.flat()[0]) +} + +export const getMusicInfo = async(hash) => { + return getMusicInfos([{ hash }]).then(data => data[0]) +} + +export const getMusicInfosByList = (list) => { + return getMusicInfos(list.map(item => ({ hash: item.hash }))) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/musicSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/musicSearch.js new file mode 100644 index 000000000..75bac6cec --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/musicSearch.js @@ -0,0 +1,109 @@ +import { httpFetch } from '../../request' +import { decodeName, formatPlayTime, sizeFormate } from '../../index' +import { formatSingerName } from '../utils' + + +export default { + limit: 30, + total: 0, + page: 0, + allPage: 1, + musicSearch(str, page, limit) { + const searchRequest = httpFetch(`http://songsearch.kugou.com/song_search_v2?platform=AndroidFilter&iscorrection=1&keyword=${encodeURIComponent(str)}&hifiquality=0&pagesize=${limit}&PrivilegeFilter=0&page=${page}`) + return searchRequest.promise.then(({ body }) => body) + }, + filterData(rawData) { + const types = [] + const _types = {} + if (rawData.FileSize !== 0) { + let size = sizeFormate(rawData.FileSize) + types.push({ type: '128k', size, hash: rawData.FileHash }) + _types['128k'] = { + size, + hash: rawData.FileHash, + } + } + if (rawData.HQFileSize !== 0) { + let size = sizeFormate(rawData.HQFileSize) + types.push({ type: '320k', size, hash: rawData.HQFileHash }) + _types['320k'] = { + size, + hash: rawData.HQFileHash, + } + } + if (rawData.SQFileSize !== 0) { + let size = sizeFormate(rawData.SQFileSize) + types.push({ type: 'flac', size, hash: rawData.SQFileHash }) + _types.flac = { + size, + hash: rawData.SQFileHash, + } + } + if (rawData.ResFileSize !== 0) { + let size = sizeFormate(rawData.ResFileSize) + types.push({ type: 'flac24bit', size, hash: rawData.ResFileHash }) + _types.flac24bit = { + size, + hash: rawData.ResFileHash, + } + } + return { + singer: decodeName(formatSingerName(rawData.Singers, 'name')), + name: decodeName(`${rawData.OriSongName}${rawData.Suffix ? ` ${rawData.Suffix}` : ''}`), + albumName: decodeName(rawData.AlbumName), + albumId: rawData.AlbumID, + songmid: rawData.Audioid, + albumAudioId: rawData.MixSongID, + source: 'kg', + interval: formatPlayTime(rawData.Duration), + _interval: rawData.Duration, + img: null, + lrc: null, + otherSource: null, + hash: rawData.FileHash, + types, + _types, + typeUrl: {}, + } + }, + handleResult(rawData) { + let ids = new Set() + const list = [] + rawData.forEach(item => { + const key = item.Audioid + item.FileHash + if (ids.has(key)) return + ids.add(key) + list.push(this.filterData(item)) + for (const childItem of item.Grp) { + const key = item.Audioid + item.FileHash + if (ids.has(key)) continue + ids.add(key) + list.push(this.filterData(childItem)) + } + }) + return list + }, + search(str, page = 1, limit, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + if (limit == null) limit = this.limit + // http://newlyric.kuwo.cn/newlyric.lrc?62355680 + return this.musicSearch(str, page, limit).then(result => { + if (!result || result.error_code !== 0) return this.search(str, page, limit, retryNum) + let list = this.handleResult(result.data.lists) + + if (list == null) return this.search(str, page, limit, retryNum) + + this.total = result.data.total + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + limit, + total: this.total, + source: 'kg', + }) + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/pic.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/pic.js new file mode 100644 index 000000000..9b28b9354 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/pic.js @@ -0,0 +1,48 @@ +import { httpFetch } from '../../request' + +export default { + getPic(songInfo) { + const requestObj = httpFetch( + 'http://media.store.kugou.com/v1/get_res_privilege', + { + method: 'POST', + headers: { + 'KG-RC': 1, + 'KG-THash': 'expand_search_manager.cpp:852736169:451', + 'User-Agent': 'KuGou2012-9020-ExpandSearchManager', + }, + body: { + appid: 1001, + area_code: '1', + behavior: 'play', + clientver: '9020', + need_hash_offset: 1, + relate: 1, + resource: [ + { + album_audio_id: + songInfo.songmid.length == 32 // 修复歌曲ID存储变更导致图片获取失败的问题 + ? songInfo.audioId.split('_')[0] + : songInfo.songmid, + album_id: songInfo.albumId, + hash: songInfo.hash, + id: 0, + name: `${songInfo.singer} - ${songInfo.name}.mp3`, + type: 'audio', + }, + ], + token: '', + userid: 2626431536, + vip: 1, + }, + }, + ) + return requestObj.promise.then(({ body }) => { + if (body.error_code !== 0) return Promise.reject(new Error('图片获取失败')) + let info = body.data[0].info + const img = info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image + if (!img) return Promise.reject(new Error('Pic get failed')) + return img + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/singer.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/singer.js new file mode 100644 index 000000000..d0ec7c296 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/singer.js @@ -0,0 +1,85 @@ +import { getMusicInfosByList } from './musicInfo' +import { createHttpFetch } from './util' + +export default { + /** + * 获取歌手信息 + * @param {*} id + */ + getInfo(id) { + if (id == 0) throw new Error('歌手不存在') // kg源某些歌曲在歌手没被kg收录时返回的歌手id为0 + return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/info?singerid=${id}`).then(body => { + if (!body) throw new Error('get singer info faild.') + + return { + source: 'kg', + id: body.singerid, + info: { + name: body.singername, + desc: body.intro, + avatar: body.imgurl.replace('{size}', 480), + gender: body.grade === 1 ? 'man' : 'woman', + }, + count: { + music: body.songcount, + album: body.albumcount, + }, + } + }) + }, + /** + * 获取歌手专辑列表 + * @param {*} id + * @param {*} page + * @param {*} limit + */ + getAlbumList(id, page = 1, limit = 10) { + if (id == 0) throw new Error('歌手不存在') + return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/album?singerid=${id}&page=${page}&pagesize=${limit}`).then(body => { + if (!body.info) throw new Error('get singer album list faild.') + + const list = this.filterAlbumList(body.info) + return { + source: 'kg', + list, + limit, + page, + total: body.total, + } + }) + }, + /** + * 获取歌手歌曲列表 + * @param {*} id + * @param {*} page + * @param {*} limit + */ + async getSongList(id, page = 1, limit = 100) { + if (id == 0) throw new Error('歌手不存在') + const body = await createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/song?singerid=${id}&page=${page}&pagesize=${limit}`) + if (!body.info) throw new Error('get singer song list faild.') + + const list = await getMusicInfosByList(body.info) + return { + source: 'kg', + list, + limit, + page, + total: body.total, + } + }, + filterAlbumList(raw) { + return raw.map(item => { + return { + id: item.albumid, + count: item.songcount, + info: { + name: item.albumname, + author: item.singername, + img: item.replaceAll('{size}', '480'), + desc: item.intro, + }, + } + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/songList.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/songList.js new file mode 100644 index 000000000..fa3ff1d08 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/songList.js @@ -0,0 +1,1030 @@ +import { httpFetch } from '../../request' +import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../index' +import infSign from '@renderer/utils/musicSdk/kg/vendors/infSign.min' +import { signatureParams } from './util' + +const handleSignature = (id, page, limit) => new Promise((resolve, reject) => { + infSign({ appid: 1058, type: 0, module: 'playlist', page, pagesize: limit, specialid: id }, null, { + useH5: !0, + isCDN: !0, + callback(i) { + resolve(i.signature) + }, + }) +}) + +export default { + _requestObj_tags: null, + _requestObj_listInfo: null, + _requestObj_list: null, + _requestObj_listRecommend: null, + listDetailLimit: 10000, + currentTagInfo: { + id: undefined, + info: undefined, + }, + sortList: [ + { + name: '推荐', + id: '5', + }, + { + name: '最热', + id: '6', + }, + { + name: '最新', + id: '7', + }, + { + name: '热藏', + id: '3', + }, + { + name: '飙升', + id: '8', + }, + ], + cache: new Map(), + regExps: { + listData: /global\.data = (\[.+\]);/, + listInfo: /global = {[\s\S]+?name: "(.+)"[\s\S]+?pic: "(.+)"[\s\S]+?};/, + // https://www.kugou.com/yy/special/single/1067062.html + listDetailLink: /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/, + }, + // async getGlobalSpecialId(specialId) { + // return httpFetch(`http://mobilecdnbj.kugou.com/api/v5/special/info?specialid=${specialId}`, { + // headers: { + // 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HLK-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Mobile Safari/537.36 EdgA/104.0.1293.70', + // }, + // }).promise.then(({ body }) => { + // // console.log(body) + // if (!body.data.global_specialid) Promise.reject(new Error('Failed to get global collection id.')) + // return body.data.global_specialid + // }) + // }, + // async getListInfoBySpecialId(special_id, retry = 0) { + // if (++retry > 2) throw new Error('failed') + // return httpFetch(`https://m.kugou.com/plist/list/${special_id}/?json=true`, { + // headers: { + // 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HLK-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Mobile Safari/537.36 EdgA/104.0.1293.70', + // }, + // follow_max: 2, + // }).promise.then(({ body }) => { + // // console.log(body) + // if (!body.info.list) return this.getListInfoBySpecialId(special_id, retry) + // let listinfo = body.info.list + // return { + // listInfo: { + // name: listinfo.specialname, + // image: listinfo.imgurl.replace('{size}', '150'), + // intro: listinfo.intro, + // author: listinfo.nickname, + // playcount: listinfo.playcount, + // total: listinfo.songcount, + // }, + // globalSpecialId: listinfo.global_specialid, + // } + // }) + // }, + // async getSongListDetailByGlobalSpecialId(id, page, limit = 100, retry = 0) { + // if (++retry > 2) throw new Error('failed') + // console.log(id) + // const params = `specialid=0&need_sort=1&module=CloudMusic&clientver=11409&pagesize=${limit}&global_collection_id=${id}&userid=0&page=${page}&type=1&area_code=1&appid=1005` + // return httpFetch(`http://pubsongscdn.tx.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params)}`).promise.then(({ body }) => { + // // console.log(body) + // if (body.data?.info == null) return this.getSongListDetailByGlobalSpecialId(id, page, limit, retry) + // return body.data.info + // }) + // }, + parseHtmlDesc(html) { + const prefix = '
    ' + let index = html.indexOf(prefix) + if (index < 0) return null + const afterStr = html.substring(index + prefix.length) + index = afterStr.indexOf('
    ') + if (index < 0) return null + return decodeName(afterStr.substring(0, index)) + }, + async getListDetailBySpecialId(id, page, tryNum = 0) { + if (tryNum > 2) throw new Error('try max num') + + const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise + let listData = body.match(this.regExps.listData) + let listInfo = body.match(this.regExps.listInfo) + if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum) + let list = await this.getMusicInfos(JSON.parse(listData[1])) + // listData = this.filterData(JSON.parse(listData[1])) + let name + let pic + if (listInfo) { + name = listInfo[1] + pic = listInfo[2] + } + let desc = this.parseHtmlDesc(body) + + + return { + list, + page: 1, + limit: 10000, + total: list.length, + source: 'kg', + info: { + name, + img: pic, + desc, + // author: body.result.info.userinfo.username, + // play_count: formatPlayCount(body.result.listen_num), + }, + } + + // const globalSpecialId = await this.getGlobalSpecialId(id) + // const limit = 100 + // const listData = await this.getSongListDetailByGlobalSpecialId(globalSpecialId, page, limit) + // if (!Array.isArray(listData)) + // return this.getUserListDetail2(globalSpecialId) + // return { + // list: this.filterDatav9(listData), + // page, + // limit, + // total: listInfo.total, + // source: 'kg', + // info: { + // name: listInfo.name, + // img: listInfo.image, + // desc: listInfo.intro, + // author: listInfo.author, + // play_count: formatPlayCount(listInfo.playcount), + // }, + // } + }, + getInfoUrl(tagId) { + return tagId + ? `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&cdn=cdn&t=5&c=${tagId}` + : 'http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&' + }, + getSongListUrl(sortId, tagId, page) { + if (tagId == null) tagId = '' + return `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_ajax=1&cdn=cdn&t=${sortId}&c=${tagId}&p=${page}` + }, + getSongListDetailUrl(id) { + return `http://www2.kugou.kugou.com/yueku/v9/special/single/${id}-5-9999.html` + }, + + filterInfoHotTag(rawData) { + const result = [] + if (rawData.status !== 1) return result + for (const key of Object.keys(rawData.data)) { + let tag = rawData.data[key] + result.push({ + id: tag.special_id, + name: tag.special_name, + source: 'kg', + }) + } + return result + }, + filterTagInfo(rawData) { + const result = [] + for (const name of Object.keys(rawData)) { + result.push({ + name, + list: rawData[name].data.map(tag => ({ + parent_id: tag.parent_id, + parent_name: tag.pname, + id: tag.id, + name: tag.name, + source: 'kg', + })), + }) + } + return result + }, + + getSongList(sortId, tagId, page, tryNum = 0) { + if (this._requestObj_list) this._requestObj_list.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_list = httpFetch( + this.getSongListUrl(sortId, tagId, page), + ) + return this._requestObj_list.promise.then(({ body }) => { + if (!body || body.status !== 1) return this.getSongList(sortId, tagId, page, ++tryNum) + return this.filterList(body.special_db) + }) + }, + getSongListRecommend(tryNum = 0) { + if (this._requestObj_listRecommend) this._requestObj_listRecommend.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_listRecommend = httpFetch( + 'http://everydayrec.service.kugou.com/guess_special_recommend', + { + method: 'post', + headers: { + 'User-Agent': 'KuGou2012-8275-web_browser_event_handler', + }, + body: { + appid: 1001, + clienttime: 1566798337219, + clientver: 8275, + key: 'f1f93580115bb106680d2375f8032d96', + mid: '21511157a05844bd085308bc76ef3343', + platform: 'pc', + userid: '262643156', + return_min: 6, + return_max: 15, + }, + }, + ) + return this._requestObj_listRecommend.promise.then(({ body }) => { + if (body.status !== 1) return this.getSongListRecommend(++tryNum) + return this.filterList(body.data.special_list) + }) + }, + filterList(rawData) { + return rawData.map(item => ({ + play_count: item.total_play_count || formatPlayCount(item.play_count), + id: 'id_' + item.specialid, + author: item.nickname, + name: item.specialname, + time: dateFormat(item.publish_time || item.publishtime, 'Y-M-D'), + img: item.img || item.imgurl, + total: item.songcount, + grade: item.grade, + desc: item.intro, + source: 'kg', + })) + }, + + async createHttp(url, options, retryNum = 0) { + if (retryNum > 2) throw new Error('try max num') + let result + try { + result = await httpFetch(url, options).promise + } catch (err) { + console.log(err) + return this.createHttp(url, options, ++retryNum) + } + // console.log(result.statusCode, result.body) + if (result.statusCode !== 200 || + ( + (result.body.error_code !== undefined + ? result.body.error_code + : result.body.errcode !== undefined + ? result.body.errcode + : result.body.err_code + ) !== 0) + ) return this.createHttp(url, options, ++retryNum) + if (result.body.data) return result.body.data + if (Array.isArray(result.body.info)) return result.body + return result.body.info + }, + + createTask(hashs) { + let data = { + area_code: '1', + show_privilege: 1, + show_album_info: '1', + is_publish: '', + appid: 1005, + clientver: 11451, + mid: '1', + dfid: '-', + clienttime: Date.now(), + key: 'OIlwieks28dk2k092lksi2UIkp', + fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname', + } + let list = hashs + let tasks = [] + while (list.length) { + tasks.push(Object.assign({ data: list.slice(0, 100) }, data)) + if (list.length < 100) break + list = list.slice(100) + } + let url = 'http://gateway.kugou.com/v2/album_audio/audio' + return tasks.map(task => this.createHttp(url, { + method: 'POST', + body: task, + headers: { + 'KG-THash': '13a3164', + 'KG-RC': '1', + 'KG-Fake': '0', + 'KG-RF': '00869891', + 'User-Agent': 'Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi', + 'x-router': 'kmr.service.kugou.com', + }, + }).then(data => data.map(s => s[0]))) + }, + async getMusicInfos(list) { + return this.filterData2( + await Promise.all( + this.createTask( + this.deDuplication(list) + .map(item => ({ hash: item.hash })), + )) + .then(([...datas]) => datas.flat())) + }, + + async getUserListDetailByCode(id) { + const songInfo = await this.createHttp('http://t.kugou.com/command/', { + method: 'POST', + headers: { + 'KG-RC': 1, + 'KG-THash': 'network_super_call.cpp:3676261689:379', + 'User-Agent': '', + }, + body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: id }, + }) + // console.log(songInfo) + // type 1单曲,2歌单,3电台,4酷狗码,5别人的播放队列 + let songList + let info = songInfo.info + switch (info.type) { + case 2: + if (!info.global_collection_id) return this.getListDetailBySpecialId(info.id) + break + + default: + break + } + if (info.global_collection_id) return this.getUserListDetail2(info.global_collection_id) + if (info.userid != null) { + songList = await this.createHttp('http://www2.kugou.kugou.com/apps/kucodeAndShare/app/', { + method: 'POST', + headers: { + 'KG-RC': 1, + 'KG-THash': 'network_super_call.cpp:3676261689:379', + 'User-Agent': '', + }, + body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: { id: info.id, type: 3, userid: info.userid, collect_type: 0, page: 1, pagesize: info.count } }, + }) + // console.log(songList) + } + let list = await this.getMusicInfos(songList || songInfo.list) + return { + list, + page: 1, + limit: info.count, + total: list.length, + source: 'kg', + info: { + name: info.name, + img: (info.img_size && info.img_size.replace('{size}', 240)) || info.img, + // desc: body.result.info.list_desc, + author: info.username, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetail3(chain, page) { + const songInfo = await this.createHttp(`http://m.kugou.com/schain/transfer?pagesize=${this.listDetailLimit}&chain=${chain}&su=1&page=${page}&n=0.7928855356604456`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + }, + }) + if (!songInfo.list) { + if (songInfo.global_collection_id) { + return this.getListDetailByGcidChain(songInfo.global_collection_id, chain) + .catch(() => this.getUserListDetail2(songInfo.global_collection_id)) + } else return this.getUserListDetail4(songInfo, chain, page).catch(() => this.getUserListDetail5(chain)) + } + let list = await this.getMusicInfos(songInfo.list) + // console.log(info, songInfo) + return { + list, + page: 1, + limit: this.listDetailLimit, + total: list.length, + source: 'kg', + info: { + name: songInfo.info.name, + img: songInfo.info.img, + // desc: body.result.info.list_desc, + author: songInfo.info.username, + // play_count: formatPlayCount(info.count), + }, + } + }, + + deDuplication(datas) { + let ids = new Set() + return datas.filter(({ hash }) => { + if (ids.has(hash)) return false + ids.add(hash) + return true + }) + }, + + async decodeGcid(gcid) { + const params = 'dfid=-&appid=1005&mid=0&clientver=20109&clienttime=640612895&uuid=-' + const body = { + ret_info: 1, + data: [ + { + id: gcid, + id_type: 2, + }, + ], + } + const result = await this.createHttp(`https://t.kugou.com/v1/songlist/batch_decode?${params}&signature=${signatureParams(params, 'android', JSON.stringify(body))}`, { + method: 'POST', + headers: { + 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HUAWEI HMA-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36', + Referer: 'https://m.kugou.com/', + }, + body, + }) + return result.list[0].global_collection_id + }, + + async getUserListDetailByLink({ info }, link) { + let listInfo = info['0'] + let total = listInfo.count + let tasks = [] + let page = 0 + while (total) { + const limit = total > 90 ? 90 : total + total -= limit + page += 1 + tasks.push(this.createHttp(link.replace(/pagesize=\d+/, 'pagesize=' + limit).replace(/page=\d+/, 'page=' + page), { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + Referer: link, + }, + }).then(data => data.list.info)) + } + let result = await Promise.all(tasks).then(([...datas]) => datas.flat()) + result = await this.getMusicInfos(result) + // console.log(result) + return { + list: result, + page, + limit: this.listDetailLimit, + total: result.length, + source: 'kg', + info: { + name: listInfo.name, + img: listInfo.pic && listInfo.pic.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.list_create_username, + // play_count: formatPlayCount(listInfo.count), + }, + } + }, + createGetListDetail2Task(id, total) { + let tasks = [] + let page = 0 + while (total) { + const limit = total > 300 ? 300 : total + total -= limit + page += 1 + const params = 'appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&page=' + page + '&pagesize=' + limit + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-' + tasks.push(this.createHttp(`https://mobiles.kugou.com/api/v5/special/song_v2?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163263991', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163263991', + }, + }).then(data => data.info)) + } + return Promise.all(tasks).then(([...datas]) => datas.flat()) + }, + async getListDetailByGcidChain(id, chain) { + const params = 'srcappid=2919&clientver=20000&clienttime=1789566751657&mid=1789566751657&uuid=1789566751657&dfid=-&listid=15&type=0&pagesize=50&global_collection_id=' + id + '&page=1&share_type=collect&appid=1058&chain=' + chain + const data = await this.createHttp(`https://m3ws.kugou.com/v2/zlist/list?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163263991', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163263991', + }, + }) + const info = data.info[0] + if (!info) throw new Error('get list error') + let tasks = [Promise.resolve(data.list.info)] + let page = 1 + let totalPage = Math.ceil(info.count / 50) + while (page < totalPage) { + const limit = 50 + page += 1 + const params = 'srcappid=2919&clientver=20000&clienttime=1789566751657&mid=1789566751657&uuid=1789566751657&dfid=-&listid=15&type=0&pagesize=' + limit + '&global_collection_id=' + id + '&page=' + page + '&share_type=collect&appid=1058&chain=' + chain + tasks.push(this.createHttp(`https://m3ws.kugou.com/v2/zlist/list?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163263991', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163263991', + }, + }).then(data => data.list.info)) + } + const hashs = await Promise.all(tasks).then(([...datas]) => datas.flat()) + let list = await this.getMusicInfos(hashs) + return { + list, + page: 1, + limit: this.listDetailLimit, + total: info.count, + source: 'kg', + info: { + name: info.name, + img: info.pic && info.pic.replace('{size}', 240), + desc: info.intro, + author: info.list_create_username, + // play_count: formatPlayCount(info.playcount), + }, + } + }, + async getUserListDetail2(global_collection_id) { + let id = global_collection_id + if (id.length > 1000) throw new Error('get list error') + const params = 'appid=1058&specialid=0&global_specialid=' + id + '&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-' + let info = await this.createHttp(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163242519', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163242519', + }, + }) + const songInfo = await this.createGetListDetail2Task(id, info.songcount) + let list = await this.getMusicInfos(songInfo) + // console.log(info, songInfo, list) + return { + list, + page: 1, + limit: this.listDetailLimit, + total: list.length, + source: 'kg', + info: { + name: info.specialname, + img: info.imgurl && info.imgurl.replace('{size}', 240), + desc: info.intro, + author: info.nickname, + play_count: formatPlayCount(info.playcount), + }, + } + }, + + async getListInfoByChain(chain) { + if (this.cache.has(chain)) return this.cache.get(chain) + const { body } = await httpFetch(`https://m.kugou.com/share/?chain=${chain}&id=${chain}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', + }, + }).promise + let result = body.match(/var\sphpParam\s=\s({.+?});/) + if (result) result = JSON.parse(result[1]) + this.cache.set(chain, result) + return result + }, + + async getUserListDetailByPcChain(chain) { + let key = `${chain}_pc_list` + if (this.cache.has(key)) return this.cache.get(key) + const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + }, + }).promise + let result = body.match(/var\sdataFromSmarty\s=\s(\[.+?\])/) + if (result) result = JSON.parse(result[1]) + this.cache.set(chain, result) + result = await this.getMusicInfos(result) + // console.log(info, songInfo) + return result + }, + + async getUserListDetail4(songInfo, chain, page) { + const limit = 100 + const [listInfo, list] = await Promise.all([ + this.getListInfoByChain(chain), + this.getUserListDetailById(songInfo.id, page, limit), + ]) + return { + list: list || [], + page, + limit, + total: list.length ?? 0, + source: 'kg', + info: { + name: listInfo.specialname, + img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.nickname, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetail5(chain) { + const [listInfo, list] = await Promise.all([ + this.getListInfoByChain(chain), + this.getUserListDetailByPcChain(chain), + ]) + return { + list: list || [], + page: 1, + limit: this.listDetailLimit, + total: list.length ?? 0, + source: 'kg', + info: { + name: listInfo.specialname, + img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.nickname, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetailById(id, page, limit) { + const signature = await handleSignature(id, page, limit) + let info = await this.createHttp(`https://pubsongscdn.kugou.com/v2/get_other_list_file?srcappid=2919&clientver=20000&appid=1058&type=0&module=playlist&page=${page}&pagesize=${limit}&specialid=${id}&signature=${signature}`, { + headers: { + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + }, + }) + + // console.log(info) + let result = await this.getMusicInfos(info.info) + // console.log(info, songInfo) + return result + }, + + async getUserListDetail(link, page, retryNum = 0) { + if (retryNum > 3) return Promise.reject(new Error('link try max num')) + if (link.includes('#')) link = link.replace(/#.*$/, '') + if (link.includes('global_collection_id') && link.includes('chain')) { + return this.getListDetailByGcidChain(link.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'), link.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1')) + } + if (link.includes('chain=')) return this.getUserListDetail3(link.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (link.includes('gcid_')) { + let gcid = link.match(/gcid_\w+/)?.[0] + if (gcid) { + const global_collection_id = await this.decodeGcid(gcid) + if (global_collection_id) return this.getUserListDetail2(global_collection_id) + } + } + if (link.includes('.html')) { + if (link.includes('zlist.html')) { + link = link.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list') + if (link.includes('pagesize')) { + link = link.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page) + } else { + link += `&pagesize=${this.listDetailLimit}&page=${page}` + } + } else if (!link.includes('song.html')) return this.getUserListDetail3(link.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page) + } + if (link.includes('global_collection_id')) return this.getUserListDetail2(link.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')) + + const requestObj_listDetailLink = httpFetch(link, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + Referer: link, + }, + }) + const { headers: { location }, statusCode, body } = await requestObj_listDetailLink.promise + // console.log(body, location) + if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum) + if (location) { + if (location.includes('global_collection_id') && location.includes('chain')) { + return this.getListDetailByGcidChain(location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'), location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1')) + } + // console.log(location) + if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (location.includes('gcid_')) { + let gcid = location.match(/gcid_\w+/)?.[0] + if (gcid) { + const global_collection_id = await this.decodeGcid(gcid) + if (global_collection_id) return this.getUserListDetail2(global_collection_id) + } + } + if (location.includes('.html')) { + if (location.includes('zlist.html')) { + let link = location.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list') + if (link.includes('pagesize')) { + link = link.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page) + } else { + link += `&pagesize=${this.listDetailLimit}&page=${page}` + } + return this.getUserListDetail(link, page, ++retryNum) + } else return this.getUserListDetail3(location.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page) + } + if (location.includes('global_collection_id')) return this.getUserListDetail2(location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')) + // console.log('location', location) + return this.getUserListDetail(location, page, ++retryNum) + } + if (typeof body == 'string') { + let global_collection_id = body.match(/"global_collection_id":"(\w+)"/)?.[1] + if (!global_collection_id) { + let gcid = body.match(/"encode_gic":"(\w+)"/)?.[1] + if (!gcid) gcid = body.match(/"encode_src_gid":"(\w+)"/)?.[1] + if (gcid) global_collection_id = await this.decodeGcid(gcid) + } + if (!global_collection_id) throw new Error('get list error') + return this.getUserListDetail2(global_collection_id) + } + if (body.errcode !== 0) return this.getUserListDetail(link, page, ++retryNum) + return this.getUserListDetailByLink(body, link) + }, + + async getListDetail(id, page) { // 获取歌曲列表内的音乐 + id = id.toString() + if (id.includes('special/single/')) { + id = id.replace(this.regExps.listDetailLink, '$1') + } else if (/https?:/.test(id)) { + // fix https://www.kugou.com/songlist/xxx/?uid=xxx&chl=qq_client&cover=http%3A%2F%2Fimge.kugou.com%xxx.jpg&iszlist=1 + return this.getUserListDetail(id.replace(/^.*?http/, 'http'), page) + } else if (/^\d+$/.test(id)) { + return this.getUserListDetailByCode(id) + } else if (id.startsWith('id_')) { + id = id.replace('id_', '') + } + // if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + + return this.getListDetailBySpecialId(id, page) + }, + filterData(rawList) { + // console.log(rawList) + return rawList.map(item => { + const types = [] + const _types = {} + if (item.filesize !== 0) { + let size = sizeFormate(item.filesize) + types.push({ type: '128k', size, hash: item.hash }) + _types['128k'] = { + size, + hash: item.hash, + } + } + if (item.filesize_320 !== 0) { + let size = sizeFormate(item.filesize_320) + types.push({ type: '320k', size, hash: item.hash_320 }) + _types['320k'] = { + size, + hash: item.hash_320, + } + } + if (item.filesize_ape !== 0) { + let size = sizeFormate(item.filesize_ape) + types.push({ type: 'ape', size, hash: item.hash_ape }) + _types.ape = { + size, + hash: item.hash_ape, + } + } + if (item.filesize_flac !== 0) { + let size = sizeFormate(item.filesize_flac) + types.push({ type: 'flac', size, hash: item.hash_flac }) + _types.flac = { + size, + hash: item.hash_flac, + } + } + return { + singer: decodeName(item.singername), + name: decodeName(item.songname), + albumName: decodeName(item.album_name), + albumId: item.album_id, + songmid: item.audio_id, + albumAudioId: item.album_audio_id, + source: 'kg', + interval: formatPlayTime(item.duration / 1000), + img: null, + lrc: null, + hash: item.hash, + types, + _types, + typeUrl: {}, + } + }) + }, + // getSinger(singers) { + // let arr = [] + // singers?.forEach(singer => { + // arr.push(singer.name) + // }) + // return arr.join('、') + // }, + // v9 API + // filterDatav9(rawList) { + // console.log(rawList) + // return rawList.map(item => { + // const types = [] + // const _types = {} + // item.relate_goods.forEach(qualityObj => { + // if (qualityObj.level === 2) { + // let size = sizeFormate(qualityObj.size) + // types.push({ type: '128k', size, hash: qualityObj.hash }) + // _types['128k'] = { + // size, + // hash: qualityObj.hash, + // } + // } else if (qualityObj.level === 4) { + // let size = sizeFormate(qualityObj.size) + // types.push({ type: '320k', size, hash: qualityObj.hash }) + // _types['320k'] = { + // size, + // hash: qualityObj.hash, + // } + // } else if (qualityObj.level === 5) { + // let size = sizeFormate(qualityObj.size) + // types.push({ type: 'flac', size, hash: qualityObj.hash }) + // _types.flac = { + // size, + // hash: qualityObj.hash, + // } + // } else if (qualityObj.level === 6) { + // let size = sizeFormate(qualityObj.size) + // types.push({ type: 'flac24bit', size, hash: qualityObj.hash }) + // _types.flac24bit = { + // size, + // hash: qualityObj.hash, + // } + // } + // }) + // const nameInfo = item.name.split(' - ') + // return { + // singer: this.getSinger(item.singerinfo), + // name: decodeName((nameInfo[1] ?? nameInfo[0]).trim()), + // albumName: decodeName(item.albuminfo.name), + // albumId: item.albuminfo.id, + // songmid: item.audio_id, + // source: 'kg', + // interval: formatPlayTime(item.timelen / 1000), + // img: null, + // lrc: null, + // hash: item.hash, + // types, + // _types, + // typeUrl: {}, + // } + // }) + // }, + + // hash list filter + filterData2(rawList) { + // console.log(rawList) + let ids = new Set() + let list = [] + rawList.forEach(item => { + if (!item) return + if (ids.has(item.audio_info.audio_id)) return + ids.add(item.audio_info.audio_id) + const types = [] + const _types = {} + if (item.audio_info.filesize !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize)) + types.push({ type: '128k', size, hash: item.audio_info.hash }) + _types['128k'] = { + size, + hash: item.audio_info.hash, + } + } + if (item.audio_info.filesize_320 !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_320)) + types.push({ type: '320k', size, hash: item.audio_info.hash_320 }) + _types['320k'] = { + size, + hash: item.audio_info.hash_320, + } + } + if (item.audio_info.filesize_flac !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_flac)) + types.push({ type: 'flac', size, hash: item.audio_info.hash_flac }) + _types.flac = { + size, + hash: item.audio_info.hash_flac, + } + } + if (item.audio_info.filesize_high !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_high)) + types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high }) + _types.flac24bit = { + size, + hash: item.audio_info.hash_high, + } + } + list.push({ + singer: decodeName(item.author_name), + name: decodeName(item.songname), + albumName: decodeName(item.album_info.album_name), + albumId: item.album_info.album_id, + songmid: item.audio_info.audio_id, + albumAudioId: item.audio_info.audio_group_id, + source: 'kg', + interval: formatPlayTime(parseInt(item.audio_info.timelength) / 1000), + img: null, + lrc: null, + hash: item.audio_info.hash, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + }) + return list + }, + + // 获取列表信息 + getListInfo(tagId, tryNum = 0) { + if (this._requestObj_listInfo) this._requestObj_listInfo.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_listInfo = httpFetch(this.getInfoUrl(tagId)) + return this._requestObj_listInfo.promise.then(({ body }) => { + if (body.status !== 1) return this.getListInfo(tagId, ++tryNum) + return { + limit: body.data.params.pagesize, + page: body.data.params.p, + total: body.data.params.total, + source: 'kg', + } + }) + }, + + // 获取列表数据 + getList(sortId, tagId, page) { + let tasks = [this.getSongList(sortId, tagId, page)] + tasks.push( + this.currentTagInfo.id === tagId + ? Promise.resolve(this.currentTagInfo.info) + : this.getListInfo(tagId).then(info => { + this.currentTagInfo.id = tagId + this.currentTagInfo.info = Object.assign({}, info) + return info + }), + ) + if (!tagId && page === 1 && sortId === this.sortList[0].id) tasks.push(this.getSongListRecommend()) // 如果是所有类别,则顺便获取推荐列表 + return Promise.all(tasks).then(([list, info, recommendList]) => { + if (recommendList) list.unshift(...recommendList) + return { + list, + ...info, + } + }) + }, + + // 获取标签 + getTags(tryNum = 0) { + if (this._requestObj_tags) this._requestObj_tags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_tags = httpFetch(this.getInfoUrl()) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.status !== 1) return this.getTags(++tryNum) + return { + hotTag: this.filterInfoHotTag(body.data.hotTag), + tags: this.filterTagInfo(body.data.tagids), + source: 'kg', + } + }) + }, + + getDetailPageUrl(id) { + if (typeof id == 'string') { + if (/^https?:\/\//.test(id)) return id + id = id.replace('id_', '') + } + return `https://www.kugou.com/yy/special/single/${id}.html` + }, + + search(text, page, limit = 20) { + // http://msearchretry.kugou.com/api/v3/search/special?version=9209&keyword=%E5%91%A8%E6%9D%B0%E4%BC%A6&pagesize=20&filter=0&page=1&sver=2&with_res_tag=0 + // return httpFetch(`http://ioscdn.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&correct=1&sver=5`) + return httpFetch(`http://msearchretry.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&filter=0&version=7910&sver=2`) + .promise.then(({ body }) => { + if (body.errcode != 0) throw new Error('filed') + // console.log(body.data.info) + return { + list: body.data.info.map(item => { + return { + play_count: formatPlayCount(item.playcount), + id: 'id_' + item.specialid, + author: item.nickname, + name: item.specialname, + time: dateFormat(item.publishtime, 'Y-M-D'), + img: item.imgurl, + grade: item.grade, + desc: item.intro, + total: item.songcount, + source: 'kg', + } + }), + limit, + total: body.data.total, + source: 'kg', + } + }) + }, +} + +// getList +// getTags +// getListDetail diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/temp/musicSearch-new.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/temp/musicSearch-new.js new file mode 100644 index 000000000..d2e8a5810 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/temp/musicSearch-new.js @@ -0,0 +1,112 @@ +import { decodeName, formatPlayTime, sizeFormate } from '../../index' +import { signatureParams, createHttpFetch } from './util' +import { formatSingerName } from '../../utils' + +export default { + limit: 30, + total: 0, + page: 0, + allPage: 1, + musicSearch(str, page, limit) { + const sign = signatureParams(`userid=0&area_code=1&appid=1005&dopicfull=1&page=${page}&token=0&privilegefilter=0&requestid=0&pagesize=${limit}&user_labels=&clienttime=0&sec_aggre=1&iscorrection=1&uuid=0&mid=0&keyword=${str}&dfid=-&clientver=11409&platform=AndroidFilter&tag=`, 3) + return createHttpFetch(`https://gateway.kugou.com/complexsearch/v3/search/song?userid=0&area_code=1&appid=1005&dopicfull=1&page=${page}&token=0&privilegefilter=0&requestid=0&pagesize=${limit}&user_labels=&clienttime=0&sec_aggre=1&iscorrection=1&uuid=0&mid=0&dfid=-&clientver=11409&platform=AndroidFilter&tag=&keyword=${encodeURIComponent(str)}&signature=${sign}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + referer: 'https://kugou.com', + }, + }).then(body => body) + }, + filterList(raw) { + let ids = new Set() + const list = [] + + raw.forEach(item => { + if (ids.has(item.Audioid)) return + ids.add(item.Audioid) + + const types = [] + const _types = {} + if (item.FileSize !== 0) { + let size = sizeFormate(item.FileSize) + types.push({ type: '128k', size, hash: item.FileHash }) + _types['128k'] = { + size, + hash: item.FileHash, + } + } + if (item.HQ != undefined) { + let size = sizeFormate(item.HQ.FileSize) + types.push({ type: '320k', size, hash: item.HQ.Hash }) + _types['320k'] = { + size, + hash: item.HQ.Hash, + } + } + if (item.SQ != undefined) { + let size = sizeFormate(item.SQ.FileSize) + types.push({ type: 'flac', size, hash: item.SQ.Hash }) + _types.flac = { + size, + hash: item.SQ.Hash, + } + } + if (item.Res != undefined) { + let size = sizeFormate(item.Res.FileSize) + types.push({ type: 'flac24bit', size, hash: item.Res.Hash }) + _types.flac24bit = { + size, + hash: item.Res.Hash, + } + } + list.push({ + singer: decodeName(formatSingerName(item.Singers)), + name: decodeName(item.SongName), + albumName: decodeName(item.AlbumName), + albumId: item.AlbumID, + songmid: item.Audioid, + source: 'kg', + interval: formatPlayTime(item.Duration), + _interval: item.Duration, + img: null, + lrc: null, + otherSource: null, + hash: item.FileHash, + types, + _types, + typeUrl: {}, + }) + }) + + return list + }, + handleResult(rawData) { + const rawList = [] + rawData.forEach(item => { + rawList.push(item) + item.Grp.forEach(e => rawList.push(e)) + }) + + return this.filterList(rawList) + }, + search(str, page = 1, limit, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + if (limit == null) limit = this.limit + + return this.musicSearch(str, page, limit).then(data => { + let list = this.handleResult(data.lists) + if (!list) return this.search(str, page, limit, retryNum) + + this.total = data.total + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + limit, + total: this.total, + source: 'kg', + }) + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/temp/songList-new.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/temp/songList-new.js new file mode 100644 index 000000000..2ed7586e4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/temp/songList-new.js @@ -0,0 +1,794 @@ +import { httpFetch } from '../../../request' +import { formatSingerName } from '../../utils' +import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../../index' +import { signatureParams, createHttpFetch } from './../util' +import { getMusicInfosByList } from '../musicInfo' +import album from '../album' + +export default { + _requestObj_tags: null, + _requestObj_listInfo: null, + _requestObj_list: null, + _requestObj_listRecommend: null, + listDetailLimit: 10000, + currentTagInfo: { + id: undefined, + info: undefined, + }, + sortList: [ + { + name: '推荐', + id: '5', + }, + { + name: '最热', + id: '6', + }, + { + name: '最新', + id: '7', + }, + { + name: '热藏', + id: '3', + }, + { + name: '飙升', + id: '8', + }, + ], + cache: new Map(), + collectionIdListInfoCache: new Map(), + regExps: { + listData: /global\.data = (\[.+\]);/, + listInfo: /global = {[\s\S]+?name: "(.+)"[\s\S]+?pic: "(.+)"[\s\S]+?};/, + // https://www.kugou.com/yy/special/single/1067062.html + listDetailLink: /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/, + }, + + /** + * 获取歌曲列表内的音乐 + * @param {*} id + * @param {*} page + */ + async getListDetail(id, page) { + id = id.toString() + + if (id.includes('special/single/')) id = id.replace(this.regExps.listDetailLink, '$1') + // fix https://www.kugou.com/songlist/xxx/?uid=xxx&chl=qq_client&cover=http%3A%2F%2Fimge.kugou.com%xxx.jpg&iszlist=1 + if (/https?:/.test(id)) { + if (id.includes('#')) id = id.replace(/#.*$/, '') + if (id.includes('global_collection_id')) return this.getUserListDetailByCollectionId(id.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (id.includes('chain=')) return this.getUserListDetail3(id.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (id.includes('.html')) { + if (id.includes('zlist.html')) { + id = id.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list') + if (id.includes('pagesize')) { + id = id.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page) + } else { + id += `&pagesize=${this.listDetailLimit}&page=${page}` + } + } else if (!id.includes('song.html')) return this.getUserListDetail3(id.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page) + } + return this.getUserListDetail(id.replace(/^.*?http/, 'http'), page) + } + if (/^\d+$/.test(id)) return this.getUserListDetailByCode(id, page) + if (id.startsWith('gid_')) return this.getUserListDetailByCollectionId(id.replace('gid_', ''), page) + if (id.startsWith('id_')) return this.getUserListDetailBySpecialId(id.replace('id_', ''), page) + + return new Error('Failed.') + }, + + /** + * 获取SpecialId歌单 + * @param {*} id + */ + async getUserListDetailBySpecialId(id, page, tryNum = 0) { + if (tryNum > 2) throw new Error('try max num') + + const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise + let listData = body.match(this.regExps.listData) + let listInfo = body.match(this.regExps.listInfo) + if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum) + let list = await getMusicInfosByList(JSON.parse(listData[1])) + let name + let pic + if (listInfo) { + name = listInfo[1] + pic = listInfo[2] + } + let desc = this.parseHtmlDesc(body) + + + return { + list, + page: 1, + limit: 10000, + total: list.length, + source: 'kg', + info: { + name, + img: pic, + desc, + // author: body.result.info.userinfo.username, + // play_count: formatPlayCount(body.result.listen_num), + }, + } + }, + parseHtmlDesc(html) { + const prefix = '
    ' + let index = html.indexOf(prefix) + if (index < 0) return null + const afterStr = html.substring(index + prefix.length) + index = afterStr.indexOf('
    ') + if (index < 0) return null + return decodeName(afterStr.substring(0, index)) + }, + + /** + * 使用SpecialId获取CollectionId + * @param {*} specialId + */ + async getCollectionIdBySpecialId(specialId) { + return httpFetch(`http://mobilecdnbj.kugou.com/api/v5/special/info?specialid=${specialId}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HLK-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Mobile Safari/537.36 EdgA/104.0.1293.70', + }, + }).promise.then(({ body }) => { + // console.log('getCollectionIdBySpecialId', body) + if (!body.data.global_specialid) return Promise.reject(new Error('Failed to get global collection id.')) + return body.data.global_specialid + }) + }, + + /** + * 获取歌单URL + * @param {*} sortId + * @param {*} tagId + * @param {*} page + */ + getSongListUrl(sortId, tagId, page) { + if (tagId == null) tagId = '' + return `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_ajax=1&cdn=cdn&t=${sortId}&c=${tagId}&p=${page}` + }, + getInfoUrl(tagId) { + return tagId + ? `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&cdn=cdn&t=5&c=${tagId}` + : 'http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&' + }, + getSongListDetailUrl(id) { + return `http://www2.kugou.kugou.com/yueku/v9/special/single/${id}-5-9999.html` + }, + + filterInfoHotTag(rawData) { + const result = [] + if (rawData.status !== 1) return result + for (const key of Object.keys(rawData.data)) { + let tag = rawData.data[key] + result.push({ + id: tag.special_id, + name: tag.special_name, + source: 'kg', + }) + } + return result + }, + + filterTagInfo(rawData) { + const result = [] + for (const name of Object.keys(rawData)) { + result.push({ + name, + list: rawData[name].data.map(tag => ({ + parent_id: tag.parent_id, + parent_name: tag.pname, + id: tag.id, + name: tag.name, + source: 'kg', + })), + }) + } + return result + }, + filterSongList(rawData) { + return rawData.map(item => ({ + play_count: item.total_play_count || formatPlayCount(item.play_count), + id: 'id_' + item.specialid, + author: item.nickname, + name: item.specialname, + time: dateFormat(item.publish_time || item.publishtime, 'Y-M-D'), + img: item.img || item.imgurl, + total: item.songcount, + grade: item.grade, + desc: item.intro, + source: 'kg', + })) + }, + + getSongList(sortId, tagId, page, tryNum = 0) { + if (this._requestObj_list) this._requestObj_list.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_list = httpFetch( + this.getSongListUrl(sortId, tagId, page), + ) + return this._requestObj_list.promise.then(({ body }) => { + if (!body || body.status !== 1) return this.getSongList(sortId, tagId, page, ++tryNum) + return this.filterSongList(body.special_db) + }) + }, + getSongListRecommend(tryNum = 0) { + if (this._requestObj_listRecommend) this._requestObj_listRecommend.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_listRecommend = httpFetch( + 'http://everydayrec.service.kugou.com/guess_special_recommend', + { + method: 'post', + headers: { + 'User-Agent': 'KuGou2012-8275-web_browser_event_handler', + }, + body: { + appid: 1001, + clienttime: 1566798337219, + clientver: 8275, + key: 'f1f93580115bb106680d2375f8032d96', + mid: '21511157a05844bd085308bc76ef3343', + platform: 'pc', + userid: '262643156', + return_min: 6, + return_max: 15, + }, + }, + ) + return this._requestObj_listRecommend.promise.then(({ body }) => { + if (body.status !== 1) return this.getSongListRecommend(++tryNum) + return this.filterSongList(body.data.special_list) + }) + }, + + /** + * 通过CollectionId获取歌单详情 + * @param {*} id + */ + async getUserListInfoByCollectionId(id) { + if (!id || id.length > 1000) return Promise.reject(new Error('get list error')) + if (this.collectionIdListInfoCache.has(id)) return this.collectionIdListInfoCache.get(id) + + const params = `appid=1058&specialid=0&global_specialid=${id}&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-` + return createHttpFetch(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163242519', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163242519', + }, + }).then(body => { + let info = { + type: body.type, + userName: body.nickname, + userAvatar: body.user_avatar, + imageUrl: body.imgurl, + desc: body.intro, + name: body.specialname, + globalSpecialid: body.global_specialid, + total: body.songcount, + playCount: body.playcount, + } + + this.collectionIdListInfoCache.set(id, info) + return info + }) + }, + /** + * 通过SpecialId获取歌单 + * @param {*} id + */ + // async getUserListDetailBySpecialId(id, page = 1, limit = 300) { + // if (!id || id.length > 1000) return Promise.reject(new Error('get list error.')) + // const listInfo = await this.getListInfoBySpecialId(id) + + // const params = `specialid=${id}&need_sort=1&module=CloudMusic&clientver=11589&pagesize=${limit}&userid=0&page=${page}&type=0&area_code=1&appid=1005` + // return createHttpFetch(`http://pubsongs.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params, 2)}`, { + // headers: { + // 'User-Agent': 'Android10-AndroidPhone-11589-201-0-playlist-wifi', + // }, + // }).then(body => { + // if (!body.info) return Promise.reject(new Error('Get list failed.')) + // const songList = this.filterListByCollectionId(body.info) + + // return { + // list: songList || [], + // page, + // limit, + // total: body.count, + // source: 'kg', + // info: { + // name: listInfo.name, + // img: listInfo.image, + // desc: listInfo.desc, + // // author: listInfo.userName, + // // play_count: formatPlayCount(listInfo.playCount), + // }, + // } + // }) + // }, + /** + * 通过CollectionId获取歌单 + * @param {*} id + */ + async getUserListDetailByCollectionId(id, page = 1, limit = 300) { + if (!id || id.length > 1000) return Promise.reject(new Error('ID error.')) + const listInfo = await this.getUserListInfoByCollectionId(id) + + const params = `need_sort=1&module=CloudMusic&clientver=11589&pagesize=${limit}&global_collection_id=${id}&userid=0&page=${page}&type=0&area_code=1&appid=1005` + return createHttpFetch(`http://pubsongs.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params, 'android')}`, { + headers: { + 'User-Agent': 'Android10-AndroidPhone-11589-201-0-playlist-wifi', + }, + }).then(body => { + if (!body.info) return Promise.reject(new Error('Get list failed.')) + const songList = this.filterListByCollectionId(body.info) + + return { + list: songList || [], + page, + limit, + total: listInfo.total, + source: 'kg', + info: { + name: listInfo.name, + img: listInfo.imageUrl && listInfo.imageUrl.replace('{size}', 240), + desc: listInfo.desc, + author: listInfo.userName, + play_count: formatPlayCount(listInfo.playCount), + }, + } + }) + }, + /** + * 过滤GlobalSpecialId歌单数据 + * @param {*} rawData + */ + filterListByCollectionId(rawData) { + let ids = new Set() + let list = [] + rawData.forEach(item => { + if (!item) return + if (ids.has(item.hash)) return + ids.add(item.hash) + const types = [] + const _types = {} + + item.relate_goods.forEach(data => { + let size = sizeFormate(data.size) + switch (data.level) { + case 2: + types.push({ type: '128k', size, hash: data.hash }) + _types['128k'] = { + size, + hash: data.hash, + } + break + case 4: + types.push({ type: '320k', size, hash: data.hash }) + _types['320k'] = { + size, + hash: data.hash, + } + break + case 5: + types.push({ type: 'flac', size, hash: data.hash }) + _types.flac = { + size, + hash: data.hash, + } + break + case 6: + types.push({ type: 'flac24bit', size, hash: data.hash }) + _types.flac24bit = { + size, + hash: data.hash, + } + break + } + }) + + list.push({ + singer: formatSingerName(item.singerinfo, 'name') || decodeName(item.name).split(' - ')[0].replace(/&/g, '、'), + name: decodeName(item.name).split(' - ')[1], + albumName: decodeName(item.albuminfo.name), + albumId: item.albuminfo.id, + songmid: item.audio_id, + source: 'kg', + interval: formatPlayTime(parseInt(item.timelen) / 1000), + img: null, + lrc: null, + hash: item.hash, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + }) + return list + }, + /** + * 通过酷狗码获取歌单 + * @param {*} id + * @param {*} page + */ + async getUserListDetailByCode(id, page = 1) { + // type 1单曲,2歌单,3电台,4酷狗码,5别人的播放队列 + const codeData = await createHttpFetch('http://t.kugou.com/command/', { + method: 'POST', + headers: { + 'KG-RC': 1, + 'KG-THash': 'network_super_call.cpp:3676261689:379', + 'User-Agent': '', + }, + body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: id }, + }) + if (!codeData) return Promise.reject(new Error('Get list failed.')) + const codeInfo = codeData.info + + switch (codeInfo.type) { + case 2: + if (!codeInfo.global_collection_id) return this.getUserListDetailBySpecialId(codeInfo.id, page) + break + case 3: + return album.getAlbumDetail(codeInfo.id, page) + } + if (codeInfo.global_collection_id) return this.getUserListDetailByCollectionId(codeInfo.global_collection_id, page) + + if (codeInfo.userid != null) { + const songList = await createHttpFetch('http://www2.kugou.kugou.com/apps/kucodeAndShare/app/', { + method: 'POST', + headers: { + 'KG-RC': 1, + 'KG-THash': 'network_super_call.cpp:3676261689:379', + 'User-Agent': '', + }, + body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: { id: codeInfo.id, type: 3, userid: codeInfo.userid, collect_type: 0, page: 1, pagesize: codeInfo.count } }, + }) + // console.log(songList) + let list = await getMusicInfosByList(songList || codeInfo.list) + return { + list, + page: 1, + limit: codeInfo.count, + total: list.length, + source: 'kg', + info: { + name: codeInfo.name, + img: (codeInfo.img_size && codeInfo.img_size.replace('{size}', 240)) || codeInfo.img, + // desc: body.result.info.list_desc, + author: codeInfo.username, + // play_count: formatPlayCount(info.count), + }, + } + } + }, + + async getUserListDetail3(chain, page) { + const songInfo = await createHttpFetch(`http://m.kugou.com/schain/transfer?pagesize=${this.listDetailLimit}&chain=${chain}&su=1&page=${page}&n=0.7928855356604456`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + }, + }) + if (!songInfo.list) { + if (songInfo.global_collection_id) return this.getUserListDetailByCollectionId(songInfo.global_collection_id, page) + else return this.getUserListDetail4(songInfo, chain, page).catch(() => this.getUserListDetail5(chain)) + } + let list = await getMusicInfosByList(songInfo.list) + // console.log(info, songInfo) + return { + list, + page: 1, + limit: this.listDetailLimit, + total: list.length, + source: 'kg', + info: { + name: songInfo.info.name, + img: songInfo.info.img, + // desc: body.result.info.list_desc, + author: songInfo.info.username, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetailByLink({ info }, link) { + let listInfo = info['0'] + let total = listInfo.count + let tasks = [] + let page = 0 + while (total) { + const limit = total > 90 ? 90 : total + total -= limit + page += 1 + tasks.push(createHttpFetch(link.replace(/pagesize=\d+/, 'pagesize=' + limit).replace(/page=\d+/, 'page=' + page), { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + Referer: link, + }, + }).then(data => data.list.info)) + } + let result = await Promise.all(tasks).then(([...datas]) => datas.flat()) + result = await getMusicInfosByList(result) + // console.log(result) + return { + list: result, + page, + limit: this.listDetailLimit, + total: result.length, + source: 'kg', + info: { + name: listInfo.name, + img: listInfo.pic && listInfo.pic.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.list_create_username, + // play_count: formatPlayCount(listInfo.count), + }, + } + }, + createGetListDetail2Task(id, total) { + let tasks = [] + let page = 0 + while (total) { + const limit = total > 300 ? 300 : total + total -= limit + page += 1 + const params = 'appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&page=' + page + '&pagesize=' + limit + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-' + tasks.push(createHttpFetch(`https://mobiles.kugou.com/api/v5/special/song_v2?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163263991', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163263991', + }, + }).then(data => data.info)) + } + return Promise.all(tasks).then(([...datas]) => datas.flat()) + }, + async getUserListDetail2(global_collection_id) { + let id = global_collection_id + if (id.length > 1000) throw new Error('get list error') + const params = 'appid=1058&specialid=0&global_specialid=' + id + '&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-' + let info = await createHttpFetch(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163242519', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163242519', + }, + }) + const songInfo = await this.createGetListDetail2Task(id, info.songcount) + let list = await getMusicInfosByList(songInfo) + // console.log(info, songInfo, list) + return { + list, + page: 1, + limit: this.listDetailLimit, + total: list.length, + source: 'kg', + info: { + name: info.specialname, + img: info.imgurl && info.imgurl.replace('{size}', 240), + desc: info.intro, + author: info.nickname, + play_count: formatPlayCount(info.playcount), + }, + } + }, + + async getListInfoByChain(chain) { + if (this.cache.has(chain)) return this.cache.get(chain) + const { body } = await httpFetch(`https://m.kugou.com/share/?chain=${chain}&id=${chain}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', + }, + }).promise + // console.log(body) + let result = body.match(/var\sphpParam\s=\s({.+?});/) + if (result) result = JSON.parse(result[1]) + this.cache.set(chain, result) + return result + }, + + async getUserListDetailByPcChain(chain) { + let key = `${chain}_pc_list` + if (this.cache.has(key)) return this.cache.get(key) + const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + }, + }).promise + let result = body.match(/var\sdataFromSmarty\s=\s(\[.+?\])/) + if (result) result = JSON.parse(result[1]) + this.cache.set(chain, result) + result = await getMusicInfosByList(result) + // console.log(info, songInfo) + return result + }, + + async getUserListDetail4(songInfo, chain, page) { + const limit = 100 + const [listInfo, list] = await Promise.all([ + this.getListInfoByChain(chain), + this.getUserListDetailBySpecialId(songInfo.id, page, limit), + ]) + return { + list: list || [], + page, + limit, + total: list.length ?? 0, + source: 'kg', + info: { + name: listInfo.specialname, + img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.nickname, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetail5(chain) { + const [listInfo, list] = await Promise.all([ + this.getListInfoByChain(chain), + this.getUserListDetailByPcChain(chain), + ]) + return { + list: list || [], + page: 1, + limit: this.listDetailLimit, + total: list.length ?? 0, + source: 'kg', + info: { + name: listInfo.specialname, + img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.nickname, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetail(link, page, retryNum = 0) { + if (retryNum > 3) return Promise.reject(new Error('link try max num')) + + const requestLink = httpFetch(link, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + Referer: link, + }, + follow_max: 2, + }) + const { headers: { location }, statusCode, body } = await requestLink.promise + // console.log(body, location, statusCode) + if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum) + if (typeof body == 'string') { + if (body.includes('"global_collection_id":')) return this.getUserListDetailByCollectionId(body.replace(/^[\s\S]+?"global_collection_id":"(\w+)"[\s\S]+?$/, '$1'), page) + if (body.includes('"albumid":')) return album.getAlbumDetail(body.replace(/^[\s\S]+?"albumid":(\w+)[\s\S]+?$/, '$1'), page) + if (body.includes('"album_id":') && link.includes('album/info')) return album.getAlbumDetail(body.replace(/^[\s\S]+?"album_id":(\w+)[\s\S]+?$/, '$1'), page) + if (body.includes('list_id = "') && link.includes('album/info')) return album.getAlbumDetail(body.replace(/^[\s\S]+?list_id = "(\w+)"[\s\S]+?$/, '$1'), page) + } + if (location) { + // 概念版分享链接 https://t1.kugou.com/xxx + if (location.includes('global_specialid')) return this.getUserListDetailByCollectionId(location.replace(/^.*?global_specialid=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (location.includes('global_collection_id')) return this.getUserListDetailByCollectionId(location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (location.includes('.html')) { + if (location.includes('zlist.html')) { + let link = location.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list') + if (link.includes('pagesize')) { + link = link.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page) + } else { + link += `&pagesize=${this.listDetailLimit}&page=${page}` + } + return this.getUserListDetail(link, page, ++retryNum) + } else return this.getUserListDetail3(location.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page) + } + return this.getUserListDetail(location, page, ++retryNum) + } + if (body.errcode !== 0) return this.getUserListDetail(link, page, ++retryNum) + return this.getUserListDetailByLink(body, link) + }, + + // 获取列表信息 + getListInfo(tagId, tryNum = 0) { + if (this._requestObj_listInfo) this._requestObj_listInfo.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_listInfo = httpFetch(this.getInfoUrl(tagId)) + return this._requestObj_listInfo.promise.then(({ body }) => { + if (body.status !== 1) return this.getListInfo(tagId, ++tryNum) + return { + limit: body.data.params.pagesize, + page: body.data.params.p, + total: body.data.params.total, + source: 'kg', + } + }) + }, + + // 获取列表数据 + getList(sortId, tagId, page) { + let tasks = [this.getSongList(sortId, tagId, page)] + tasks.push( + this.currentTagInfo.id === tagId + ? Promise.resolve(this.currentTagInfo.info) + : this.getListInfo(tagId).then(info => { + this.currentTagInfo.id = tagId + this.currentTagInfo.info = Object.assign({}, info) + return info + }), + ) + if (!tagId && page === 1 && sortId === this.sortList[0].id) tasks.push(this.getSongListRecommend()) // 如果是所有类别,则顺便获取推荐列表 + return Promise.all(tasks).then(([list, info, recommendList]) => { + if (recommendList) list.unshift(...recommendList) + return { + list, + ...info, + } + }) + }, + + // 获取标签 + getTags(tryNum = 0) { + if (this._requestObj_tags) this._requestObj_tags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_tags = httpFetch(this.getInfoUrl()) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.status !== 1) return this.getTags(++tryNum) + return { + hotTag: this.filterInfoHotTag(body.data.hotTag), + tags: this.filterTagInfo(body.data.tagids), + source: 'kg', + } + }) + }, + + getDetailPageUrl(id) { + if (typeof id == 'string') { + if (/^https?:\/\//.test(id)) return id + id = id.replace('id_', '') + } + return `https://www.kugou.com/yy/special/single/${id}.html` + }, + + search(text, page, limit = 20) { + const params = `userid=1384394652&req_custom=1&appid=1005&req_multi=1&version=11589&page=${page}&filter=0&pagesize=${limit}&order=0&clienttime=1681779443&iscorrection=1&searchsong=0&keyword=${text}&mid=288799920684148686226285199951543865551&dfid=3eSBsO1u97EY1zeIZd40hH4p&clientver=11589&platform=AndroidFilter` + const url = encodeURI(`http://complexsearchretry.kugou.com/v1/search/special?${params}&signature=${signatureParams(params, 'android')}`) + return createHttpFetch(url).then(body => { + // console.log(body) + return { + list: body.lists.map(item => { + return { + play_count: formatPlayCount(item.total_play_count), + id: item.gid ? `gid_${item.gid}` : `id_${item.specialid}`, + author: item.nickname, + name: item.specialname, + time: dateFormat(item.publish_time, 'Y-M-D'), + img: item.img, + grade: item.grade, + desc: item.intro, + total: item.song_count, + source: 'kg', + } + }), + limit, + total: body.total, + source: 'kg', + } + }) + // http://msearchretry.kugou.com/api/v3/search/special?version=9209&keyword=%E5%91%A8%E6%9D%B0%E4%BC%A6&pagesize=20&filter=0&page=1&sver=2&with_res_tag=0 + // http://ioscdn.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&correct=1&sver=5 + // http://msearchretry.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&filter=0&version=7910&sver=2 + }, +} + +// getList +// getTags +// getListDetail diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/tipSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/tipSearch.js new file mode 100644 index 000000000..0268676df --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/tipSearch.js @@ -0,0 +1,25 @@ +import { createHttpFetch } from './util' + +export default { + requestObj: null, + cancelTipSearch() { + if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp() + }, + tipSearchBySong(str) { + this.cancelTipSearch() + this.requestObj = createHttpFetch(`https://searchtip.kugou.com/getSearchTip?MusicTipCount=10&keyword=${encodeURIComponent(str)}`, { + headers: { + referer: 'https://www.kugou.com/', + }, + }) + return this.requestObj.then(body => { + return body[0].RecordDatas + }) + }, + handleResult(rawData) { + return rawData.map(info => info.HintInfo) + }, + async search(str) { + return this.tipSearchBySong(str).then(result => this.handleResult(result)) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/util.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/util.js new file mode 100644 index 000000000..9c15bdeb4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/util.js @@ -0,0 +1,47 @@ +import { toMD5 } from '../utils' +import { httpFetch } from '../../request' + +// s.content[0].lyricContent.forEach(([str]) => { +// console.log(str) +// }) + +/** + * 签名 + * @param {*} params + * @param {*} apiver + */ +export const signatureParams = (params, platform = 'android', body = '') => { + let keyparam = 'OIlwieks28dk2k092lksi2UIkp' + if (platform === 'web') keyparam = 'NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt' + let param_list = params.split('&') + param_list.sort() + let sign_params = `${keyparam}${param_list.join('')}${body}${keyparam}` + return toMD5(sign_params) +} + +/** + * 创建一个适用于KG的Http请求 + * @param {*} url + * @param {*} options + * @param {*} retryNum + */ +export const createHttpFetch = async(url, options, retryNum = 0) => { + if (retryNum > 2) throw new Error('try max num') + let result + try { + result = await httpFetch(url, options).promise + } catch (err) { + console.log(err) + return createHttpFetch(url, options, ++retryNum) + } + // console.log(result.statusCode, result.body) + if (result.statusCode !== 200 || + ( + result.body.error_code ?? + result.body.errcode ?? + result.body.err_code) != 0 + ) return createHttpFetch(url, options, ++retryNum) + if (result.body.data) return result.body.data + if (Array.isArray(result.body.info)) return result.body + return result.body.info +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/vendors/infSign.min.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/vendors/infSign.min.js new file mode 100644 index 000000000..c1a9d43b1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kg/vendors/infSign.min.js @@ -0,0 +1,2 @@ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self,t.infSign=n())}(this,function(){"use strict";function t(t,n,r){return n in t?Object.defineProperty(t,n,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[n]=r,t}function n(t,n){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var e=Object.getOwnPropertySymbols(t);n&&(e=e.filter(function(n){return Object.getOwnPropertyDescriptor(t,n).enumerable})),r.push.apply(r,e)}return r}function r(r){for(var e=1;e0&&void 0!==arguments[0]?arguments[0]:{},e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=!1,c=!1,a="json",l=r({},n),u=s.isInClient();"function"==typeof o?t=o:(t=o.callback,i=o.useH5||!1,a=o.postType||"json",c=o.isCDN||!1),e&&("[object Object]"!=Object.prototype.toString.call(e)?u=!1:"urlencoded"==a&&(u=!1));var f=function(){var n=(new Date).getTime(),i=[],s=[],u="NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt",f={srcappid:"2919",clientver:"20000",clienttime:n,mid:n,uuid:n,dfid:"-"};c&&(delete f.clienttime,delete f.mid,delete f.uuid,delete f.dfid),l=r({},f,{},l);for(var g in l)i.push(g);if(i.sort(),i.forEach(function(t){s.push(t+"="+l[t])}),e)if("[object Object]"==Object.prototype.toString.call(e))if("json"==a)s.push(JSON.stringify(e));else{var b=[];for(var g in e)b.push(g+"="+e[g]);s.push(b.join("&"))}else s.push(e);s.unshift(u),s.push(u),l.signature=d(s.join("")),o.log&&(console.log("H5签名前参数",s),console.log("H5签名后返回",l)),e?t&&t(l,"[object Object]"==Object.prototype.toString.call(e)&&"json"==a?JSON.stringify(e):e):t&&t(l)};if(u&&!i){var g=!1;s.mobileCall(764,{get:l,post:e},function(n){return!g&&(g=!0,n&&n.status?(delete n.status,o.log&&(console.log("客户端签名前参数",{get:l,post:e}),console.log("客户端签名后返回",r({},l,{},n))),l=r({},l,{},n),e?t&&t(l,"[object Object]"==Object.prototype.toString.call(e)&&"json"==a?JSON.stringify(e):e):t&&t(l),!1):(u=!1,void f()))})}else u=!1,f()}"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;var s=e(function(t,n){!function(n,r){t.exports=function(){var t={str2Json:function(t){var n={};if("[object String]"===Object.prototype.toString.call(t))try{n=JSON.parse(t)}catch(t){n={}}return n},json2Str:function(t){var n=t;if("string"!=typeof t)try{n=JSON.stringify(t)}catch(t){n=""}return n},_extend:function(t,n){if(n)for(var r in t)n.hasOwnProperty(r)||(n[r]=t[r]);return n},formatURL:{browser:"",url:""},formatSong:{filename:"",filesize:"",hash:"",bitrate:"",extname:"",duration:"",mvhash:"",m4afilesize:"","320hash":"","320filesize":"",sqhash:"",sqfilesize:0,feetype:0,isfirst:0},formatMV:{filename:"",singername:"",hash:"",imgurl:""},formatShare:{shareName:"",topicName:"",hash:"",listID:"",type:"",suid:"",slid:"",imgUrl:"",filename:"",duration:"",shareData:{linkUrl:"",picUrl:"",content:"",title:""}},cbNum:0,isIOS:!!navigator.userAgent.match(/KGBrowser/gi),isKugouAndroid:!!navigator.userAgent.match(/kugouandroid/gi),isAndroid:"undefined"!=typeof external&&void 0!==external.superCall,loadUrl:function(t){var n=document.createElement("iframe");n.setAttribute("src",t),n.setAttribute("style","display:none;"),n.setAttribute("height","0px"),n.setAttribute("width","0px"),n.setAttribute("frameborder","0"),document.body.appendChild(n),n.parentNode.removeChild(n),n=null},callCmd:function(n){var r=t;if(r.isKugouAndroid){var e={},o="";if(n.cmd&&(e.cmd=n.cmd),n.jsonStr&&(e.jsonStr=n.jsonStr),n.callback&&(o="kgandroidmobilecall"+ ++r.cbNum+Math.random().toString().substr(2,9),e.callback=o,window[o]=function(t,e){void 0!==t&&("[object String]"===Object.prototype.toString.call(t)?(t="#"===e?decodeURIComponent(t):decodeURIComponent(decodeURIComponent(t)),n.callback(r.str2Json(t))):n.callback(t))}),n.AndroidCallback){var i=r.str2Json(n.jsonStr);i.AndroidCallback=o,n.jsonStr=r.json2Str(i),n.jsonStr&&(e.jsonStr=n.jsonStr)}var c=encodeURIComponent(JSON.stringify(e)),s="kugoujsbridge://start.kugou_jsbridge/?".concat(c);r.loadUrl(s)}else if(r.isAndroid){var a="",l="";if(n.jsonStr){if(n.callback&&""!==n.callback&&!0===n.AndroidCallback){l="kgmobilecall"+ ++r.cbNum+Math.random().toString().substr(2,9),window[l]=function(t,e){void 0!==t&&("[object String]"===Object.prototype.toString.call(t)?(t="#"===e?decodeURIComponent(t):decodeURIComponent(decodeURIComponent(t)),n.callback(r.str2Json(t))):n.callback(t))};var u=r.str2Json(n.jsonStr);u.AndroidCallback=l,n.jsonStr=r.json2Str(u)}try{a=external.superCall(n.cmd,n.jsonStr)}catch(t){}}else try{a=external.superCall(n.cmd)}catch(t){}n.callback&&""!==n.callback&&"AndroidCallback"!=a&&(a=r.str2Json(a),n.callback(a))}else{var f="",d="";n.callback&&(d="kgmobilecall"+ ++r.cbNum+Math.random().toString().substr(2,9),window[d]=function(t){void 0!==t&&n.callback&&("[object String]"===Object.prototype.toString.call(t)?n.callback(r.str2Json(t)):n.callback(t))}),d&&""!=d&&n.jsonStr&&(f='kugouurl://start.music/?{"cmd":'+n.cmd+', "jsonStr":'+n.jsonStr+', "callback":"'+d+'"}'),d&&""!=d&&!n.jsonStr&&(f='kugouurl://start.music/?{"cmd":'+n.cmd+', "callback":"'+d+'"}'),""==d&&n.jsonStr&&(f='kugouurl://start.music/?{"cmd":'+n.cmd+', "jsonStr":'+n.jsonStr+"}"),""!=d||n.jsonStr||(f='kugouurl://start.music/?{"cmd":'+n.cmd+"}"),r.loadUrl(f)}},formartData:function(n,r){n&&123==n&&r&&(r=t._extend(t.formatURL,r)),n&&123==n&&r&&(r=t._extend(t.formatURL,r))}};return{isIOS:t.isIOS,isKugouAndroid:t.isKugouAndroid,isAndroid:t.isAndroid,isInClient:function(){return!(!t.isAndroid&&!t.isKugouAndroid&&!t.isIOS)},mobileCall:function(n,r,e){var o="";if(r&&(o=t.json2Str(r)),!n)return console.error("请输入命令号!"),!1;var i={};n&&(i.cmd=n),""!=o&&(i.jsonStr=o),e&&(i.callback=e),n&&186==n&&e&&(i.AndroidCallback=!0),t.callCmd(i)},KgWebMobileCall:function(t,n){if(t)try{var r=t.split(".");r.reduce(function(e,o){if(e[o]){if(o===r[r.length-1]){var i=e[o];return"function"==typeof i?(e[o]=function(t){i&&i(t),n&&n(t)},e[o]):(console.error("请检查,当前环境变量已注册了对象:"+t+",且该对象不是方法"),null)}return e[o]}return o===r[r.length-1]?e[o]=function(t){n&&n(t)}:e[o]=new Object,e[o]},window)}catch(t){}}}}()}()}),a=e(function(t){!function(){var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r={rotl:function(t,n){return t<>>32-n},rotr:function(t,n){return t<<32-n|t>>>n},endian:function(t){if(t.constructor==Number)return 16711935&r.rotl(t,8)|4278255360&r.rotl(t,24);for(var n=0;n0;t--)n.push(Math.floor(256*Math.random()));return n},bytesToWords:function(t){for(var n=[],r=0,e=0;r>>5]|=t[r]<<24-e%32;return n},wordsToBytes:function(t){for(var n=[],r=0;r<32*t.length;r+=8)n.push(t[r>>>5]>>>24-r%32&255);return n},bytesToHex:function(t){for(var n=[],r=0;r>>4).toString(16)),n.push((15&t[r]).toString(16));return n.join("")},hexToBytes:function(t){for(var n=[],r=0;r>>6*(3-i)&63)):r.push("=");return r.join("")},base64ToBytes:function(t){t=t.replace(/[^A-Z0-9+\/]/gi,"");for(var r=[],e=0,o=0;e>>6-2*o);return r}};t.exports=r}()}),l={utf8:{stringToBytes:function(t){return l.bin.stringToBytes(unescape(encodeURIComponent(t)))},bytesToString:function(t){return decodeURIComponent(escape(l.bin.bytesToString(t)))}},bin:{stringToBytes:function(t){for(var n=[],r=0;r>>24)|4278255360&(s[g]<<24|s[g]>>>8);s[a>>>5]|=128<>>9<<4)]=a;for(var b=i._ff,p=i._gg,h=i._hh,m=i._ii,g=0;g>>0,u=u+j>>>0,f=f+S>>>0,d=d+v>>>0}return n.endian([l,u,f,d])};i._ff=function(t,n,r,e,o,i,c){var s=t+(n&r|~n&e)+(o>>>0)+c;return(s<>>32-i)+n},i._gg=function(t,n,r,e,o,i,c){var s=t+(n&e|r&~e)+(o>>>0)+c;return(s<>>32-i)+n},i._hh=function(t,n,r,e,o,i,c){var s=t+(n^r^e)+(o>>>0)+c;return(s<>>32-i)+n},i._ii=function(t,n,r,e,o,i,c){var s=t+(r^(n|~e))+(o>>>0)+c;return(s<>>32-i)+n},i._blocksize=16,i._digestsize=16,t.exports=function(t,r){if(void 0===t||null===t)throw new Error("Illegal argument "+t);var e=n.wordsToBytes(i(t,r));return r&&r.asBytes?e:r&&r.asString?o.bytesToString(e):n.bytesToHex(e)}}()});return c}); + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/album.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/album.js new file mode 100644 index 000000000..79e641fc8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/album.js @@ -0,0 +1,131 @@ +import { httpFetch } from '../../request' +import { decodeName } from '../../index' +import { formatSinger, objStr2JSON } from './util' + +// let requestObj_list +export default { + limit_list: 36, + limit_song: 1000, + filterListDetail(rawList, albumName, albumId) { + // console.log(rawList) + // console.log(rawList.length, rawList2.length) + return rawList.map((item, inedx) => { + let formats = item.formats.split('|') + let types = [] + let _types = {} + if (formats.includes('MP3128')) { + types.push({ type: '128k', size: null }) + _types['128k'] = { + size: null, + } + } + // if (formats.includes('MP3192')) { + // types.push({ type: '192k', size: null }) + // _types['192k'] = { + // size: null, + // } + // } + if (formats.includes('MP3H')) { + types.push({ type: '320k', size: null }) + _types['320k'] = { + size: null, + } + } + // if (formats.includes('AL')) { + // types.push({ type: 'ape', size: null }) + // _types.ape = { + // size: null, + // } + // } + if (formats.includes('ALFLAC')) { + types.push({ type: 'flac', size: null }) + _types.flac = { + size: null, + } + } + if (formats.includes('HIRFLAC')) { + types.push({ type: 'flac24bit', size: null }) + _types.flac24bit = { + size: null, + } + } + // types.reverse() + return { + singer: formatSinger(decodeName(item.artist)), + name: decodeName(item.name), + albumName, + albumId, + songmid: item.id, + source: 'kw', + interval: null, + img: item.pic, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + /** + * 格式化播放数量 + * @param {*} num + */ + formatPlayCount(num) { + if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿' + if (num > 10000) return parseInt(num / 1000) / 10 + '万' + return num + }, + getAlbumListDetail(id, page, retryNum = 0) { + if (retryNum > 2) return Promise.reject(new Error('try max num')) + const requestObj_listDetail = httpFetch(`http://search.kuwo.cn/r.s?pn=${page - 1}&rn=${this.limit_song}&stype=albuminfo&albumid=${id}&show_copyright_off=0&encoding=utf&vipver=MUSIC_9.1.0`) + return requestObj_listDetail.promise.then(({ statusCode, body }) => { + if (statusCode !== 200) return this.getAlbumListDetail(id, page, ++retryNum) + body = objStr2JSON(body) + // console.log(body) + if (!body.musiclist) return this.getAlbumListDetail(id, page, ++retryNum) + body.name = decodeName(body.name) + return { + list: this.filterListDetail(body.musiclist, body.name, body.albumid), + page, + limit: this.limit_song, + total: parseInt(body.songnum), + source: 'kw', + info: { + name: body.name, + img: body.img || body.hts_img, + desc: decodeName(body.info), + author: decodeName(body.artist), + // play_count: this.formatPlayCount(body.playnum), + }, + } + }) + }, + // getAlbumListDetail(id, page, retryNum = 0) { + // if (retryNum > 2) return Promise.reject(new Error('try max num')) + // return tokenRequest(`http://www.kuwo.cn/api/www/album/albumInfo?albumId=${id}&pn=${page}&rn=${this.limit_song}&httpsStatus=1`).then((resp) => { + // return resp.promise.then(({ statusCode, body }) => { + // console.log(body) + // return Promise.reject(new Error('failed')) + // // if (statusCode !== 200) return this.getAlbumListDetail(id, page, ++retryNum) + // // const data = body.data + // // console.log(data) + // // if (!data.musicList) return this.getAlbumListDetail(id, page, ++retryNum) + // // return { + // // list: this.filterListDetail(data.musiclist), + // // page, + // // limit: this.limit_song, + // // total: data.total, + // // source: 'kw', + // // info: { + // // name: data.album, + // // img: data.pic, + // // desc: data.albuminfo, + // // author: data.artist, + // // play_count: this.formatPlayCount(data.playCnt), + // // }, + // // } + // }) + // }) + // }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/api-temp.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/api-temp.js new file mode 100644 index 000000000..4343941fa --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/api-temp.js @@ -0,0 +1,26 @@ +import { httpFetch } from '../../request' +import { requestMsg } from '../../message' +import { headers, timeout } from '../options' +import { dnsLookup } from '../utils' + +const api_temp = { + getMusicUrl(songInfo, type) { + const requestObj = httpFetch(`http://tm.tempmusics.tk/url/kw/${songInfo.songmid}/${type}`, { + method: 'get', + headers, + timeout, + lookup: dnsLookup, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { + if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) + switch (body.code) { + case 0: return Promise.resolve({ type, url: body.data }) + default: return Promise.reject(new Error(body.msg)) + } + }) + return requestObj + }, +} + +export default api_temp diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/api-test.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/api-test.js new file mode 100644 index 000000000..2b65a1f2b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/api-test.js @@ -0,0 +1,37 @@ +import { httpFetch } from '../../request' +import { requestMsg } from '../../message' +import { headers, timeout } from '../options' +import { dnsLookup } from '../utils' + +const api_test = { + // getMusicUrl(songInfo, type) { + // const requestObj = httpFetch(`http://45.32.53.128:3002/m/kw/u/${songInfo.songmid}/${type}`, { + // method: 'get', + // headers, + // timeout, + // }) + // requestObj.promise = requestObj.promise.then(({ body }) => { + // return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(body.msg)) + // }) + // return requestObj + // }, + getMusicUrl(songInfo, type) { + const requestObj = httpFetch(`http://ts.tempmusics.tk/url/kw/${songInfo.songmid}/${type}`, { + method: 'get', + timeout, + headers, + lookup: dnsLookup, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { + if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) + switch (body.code) { + case 0: return Promise.resolve({ type, url: body.data }) + default: return Promise.reject(new Error(requestMsg.fail)) + } + }) + return requestObj + }, +} + +export default api_test diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/comment.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/comment.js new file mode 100644 index 000000000..de821ccf1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/comment.js @@ -0,0 +1,82 @@ +import { httpFetch } from '../../request' +import { dateFormat2 } from '../../index' + +export default { + _requestObj: null, + _requestObj2: null, + async getComment({ songmid }, page = 1, limit = 20) { + if (this._requestObj) this._requestObj.cancelHttp() + + const _requestObj = httpFetch(`http://ncomment.kuwo.cn/com.s?f=web&type=get_comment&aapiver=1&prod=kwplayer_ar_10.5.2.0&digest=15&sid=${songmid}&start=${limit * (page - 1)}&msgflag=1&count=${limit}&newver=3&uid=0`, { + headers: { + 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)', + }, + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.code != '200') throw new Error('获取评论失败') + // console.log(body) + + const total = body.comments_counts + return { + source: 'kw', + comments: this.filterComment(body.comments), + total, + page, + limit, + maxPage: Math.ceil(total / limit) || 1, + } + }, + async getHotComment({ songmid }, page = 1, limit = 100) { + if (this._requestObj2) this._requestObj2.cancelHttp() + + const _requestObj2 = httpFetch(`http://ncomment.kuwo.cn/com.s?f=web&type=get_rec_comment&aapiver=1&prod=kwplayer_ar_10.5.2.0&digest=15&sid=${songmid}&start=${limit * (page - 1)}&msgflag=1&count=${limit}&newver=3&uid=0`, { + headers: { + 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)', + }, + }) + const { body, statusCode } = await _requestObj2.promise + if (statusCode != 200 || body.code != '200') throw new Error('获取热门评论失败') + // console.log(body) + + const total = body.hot_comments_counts + return { + source: 'kw', + comments: this.filterComment(body.hot_comments), + total, + page, + limit, + maxPage: Math.ceil(total / limit) || 1, + } + }, + filterComment(rawList) { + if (!rawList) return [] + return rawList.map(item => { + return { + id: item.id, + text: item.msg, + time: item.time, + timeStr: dateFormat2(Number(item.time) * 1000), + userName: item.u_name, + avatar: item.u_pic, + userId: item.u_id, + likedCount: item.like_num, + images: item.mpic ? [decodeURIComponent(item.mpic)] : [], + reply: item.child_comments + ? item.child_comments.map(i => { + return { + id: i.id, + text: i.msg, + time: i.time, + timeStr: dateFormat2(Number(i.time) * 1000), + userName: i.u_name, + avatar: i.u_pic, + userId: i.u_id, + likedCount: i.like_num, + images: i.mpic ? [i.mpic] : [], + } + }) + : [], + } + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/hotSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/hotSearch.js new file mode 100644 index 000000000..7f3fd7a97 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/hotSearch.js @@ -0,0 +1,22 @@ +import { httpFetch } from '../../request' + +export default { + _requestObj: null, + async getList(retryNum = 0) { + if (this._requestObj) this._requestObj.cancelHttp() + if (retryNum > 2) return Promise.reject(new Error('try max num')) + + const _requestObj = httpFetch('http://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1¬race=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1', { + headers: { + 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)', + }, + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.status !== 'ok') throw new Error('获取热搜词失败') + // console.log(body, statusCode) + return { source: 'kw', list: this.filterList(body.tagvalue) } + }, + filterList(rawList) { + return rawList.map(item => item.key) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/index.js new file mode 100644 index 000000000..bdc5e796b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/index.js @@ -0,0 +1,102 @@ +import { httpFetch } from '../../request' +import tipSearch from './tipSearch' +import musicSearch from './musicSearch' +import { formatSinger } from './util' +import leaderboard from './leaderboard' +import lyric from './lyric' +import pic from './pic' +import { apis } from '../api-source' +import songList from './songList' +import hotSearch from './hotSearch' +import comment from './comment' + +const kw = { + _musicInfoRequestObj: null, + _musicInfoPromiseCancelFn: null, + _musicPicRequestObj: null, + _musicPicPromiseCancelFn: null, + // context: null, + + + // init(context) { + // if (this.isInited) return + // this.isInited = true + // this.context = context + + // // this.musicSearch.search('我又想你了').then(res => { + // // console.log(res) + // // }) + + // // this.getMusicUrl('62355680', '320k').then(url => { + // // console.log(url) + // // }) + // }, + + tipSearch, + musicSearch, + leaderboard, + songList, + hotSearch, + comment, + getLyric(songInfo, isGetLyricx) { + // let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer + return lyric.getLyric(songInfo, isGetLyricx) + }, + handleMusicInfo(songInfo) { + return this.getMusicInfo(songInfo).then(info => { + // console.log(JSON.stringify(info)) + songInfo.name = info.name + songInfo.singer = formatSinger(info.artist) + songInfo.img = info.pic + songInfo.albumName = info.album + return songInfo + // return Object.assign({}, songInfo, { + // name: info.name, + // singer: formatSinger(info.artist), + // img: info.pic, + // albumName: info.album, + // }) + }) + }, + + getMusicUrl(songInfo, type) { + return apis('kw').getMusicUrl(songInfo, type) + }, + + getMusicInfo(songInfo) { + if (this._musicInfoRequestObj) this._musicInfoRequestObj.cancelHttp() + this._musicInfoRequestObj = httpFetch(`http://www.kuwo.cn/api/www/music/musicInfo?mid=${songInfo.songmid}`) + return this._musicInfoRequestObj.promise.then(({ body }) => { + return body.code === 200 ? body.data : Promise.reject(new Error(body.msg)) + }) + }, + + getMusicUrls(musicInfo, cb) { + let tasks = [] + let songId = musicInfo.songmid + musicInfo.types.forEach(type => { + tasks.push(kw.getMusicUrl(songId, type.type).promise) + }) + Promise.all(tasks).then(urlInfo => { + let typeUrl = {} + urlInfo.forEach(info => { + typeUrl[info.type] = info.url + }) + cb(typeUrl) + }) + }, + + getPic(songInfo) { + return pic.getPic(songInfo) + }, + + getMusicDetailPageUrl(songInfo) { + return `http://www.kuwo.cn/play_detail/${songInfo.songmid}` + }, + + // init() { + // return getToken() + // }, +} + +export default kw diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/leaderboard.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/leaderboard.js new file mode 100644 index 000000000..aaa71a4cb --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/leaderboard.js @@ -0,0 +1,229 @@ +import { httpFetch } from '../../request' +import { formatPlayTime, decodeName } from '../../index' +import { formatSinger, wbdCrypto } from './util' + + +const boardList = [{ id: 'kw__93', name: '飙升榜', bangid: '93' }, { id: 'kw__17', name: '新歌榜', bangid: '17' }, { id: 'kw__16', name: '热歌榜', bangid: '16' }, { id: 'kw__158', name: '抖音热歌榜', bangid: '158' }, { id: 'kw__292', name: '铃声榜', bangid: '292' }, { id: 'kw__284', name: '热评榜', bangid: '284' }, { id: 'kw__290', name: 'ACG新歌榜', bangid: '290' }, { id: 'kw__286', name: '台湾KKBOX榜', bangid: '286' }, { id: 'kw__279', name: '冬日暖心榜', bangid: '279' }, { id: 'kw__281', name: '巴士随身听榜', bangid: '281' }, { id: 'kw__255', name: 'KTV点唱榜', bangid: '255' }, { id: 'kw__280', name: '家务进行曲榜', bangid: '280' }, { id: 'kw__282', name: '熬夜修仙榜', bangid: '282' }, { id: 'kw__283', name: '枕边轻音乐榜', bangid: '283' }, { id: 'kw__278', name: '古风音乐榜', bangid: '278' }, { id: 'kw__264', name: 'Vlog音乐榜', bangid: '264' }, { id: 'kw__242', name: '电音榜', bangid: '242' }, { id: 'kw__187', name: '流行趋势榜', bangid: '187' }, { id: 'kw__204', name: '现场音乐榜', bangid: '204' }, { id: 'kw__186', name: 'ACG神曲榜', bangid: '186' }, { id: 'kw__185', name: '最强翻唱榜', bangid: '185' }, { id: 'kw__26', name: '经典怀旧榜', bangid: '26' }, { id: 'kw__104', name: '华语榜', bangid: '104' }, { id: 'kw__182', name: '粤语榜', bangid: '182' }, { id: 'kw__22', name: '欧美榜', bangid: '22' }, { id: 'kw__184', name: '韩语榜', bangid: '184' }, { id: 'kw__183', name: '日语榜', bangid: '183' }, { id: 'kw__145', name: '会员畅听榜', bangid: '145' }, { id: 'kw__153', name: '网红新歌榜', bangid: '153' }, { id: 'kw__64', name: '影视金曲榜', bangid: '64' }, { id: 'kw__176', name: 'DJ嗨歌榜', bangid: '176' }, { id: 'kw__106', name: '真声音', bangid: '106' }, { id: 'kw__12', name: 'Billboard榜', bangid: '12' }, { id: 'kw__49', name: 'iTunes音乐榜', bangid: '49' }, { id: 'kw__180', name: 'beatport电音榜', bangid: '180' }, { id: 'kw__13', name: '英国UK榜', bangid: '13' }, { id: 'kw__164', name: '百大DJ榜', bangid: '164' }, { id: 'kw__246', name: 'YouTube音乐排行榜', bangid: '246' }, { id: 'kw__265', name: '韩国Genie榜', bangid: '265' }, { id: 'kw__14', name: '韩国M-net榜', bangid: '14' }, { id: 'kw__8', name: '香港电台榜', bangid: '8' }, { id: 'kw__15', name: '日本公信榜', bangid: '15' }, { id: 'kw__151', name: '腾讯音乐人原创榜', bangid: '151' }] + +const sortQualityArray = array => { + const qualityMap = { + flac24bit: 4, + flac: 3, + '320k': 2, + '128k': 1, + } + const rawQualityArray = [] + const newQualityArray = [] + + array.forEach((item, index) => { + const type = qualityMap[item.type] + if (!type) return + rawQualityArray.push({ type, index }) + }) + + rawQualityArray.sort((a, b) => a.type - b.type) + + rawQualityArray.forEach(item => { + newQualityArray.push(array[item.index]) + }) + + return newQualityArray +} + +export default { + list: [ + { + id: 'kwbiaosb', + name: '飙升榜', + bangid: 93, + }, + { + id: 'kwregb', + name: '热歌榜', + bangid: 16, + }, + { + id: 'kwhuiyb', + name: '会员榜', + bangid: 145, + }, + { + id: 'kwdouyb', + name: '抖音榜', + bangid: 158, + }, + { + id: 'kwqsb', + name: '趋势榜', + bangid: 187, + }, + { + id: 'kwhuaijb', + name: '怀旧榜', + bangid: 26, + }, + { + id: 'kwhuayb', + name: '华语榜', + bangid: 104, + }, + { + id: 'kwyueyb', + name: '粤语榜', + bangid: 182, + }, + { + id: 'kwoumb', + name: '欧美榜', + bangid: 22, + }, + { + id: 'kwhanyb', + name: '韩语榜', + bangid: 184, + }, + { + id: 'kwriyb', + name: '日语榜', + bangid: 183, + }, + ], + // getUrl: (p, l, id) => `http://kbangserver.kuwo.cn/ksong.s?from=pc&fmt=json&pn=${p - 1}&rn=${l}&type=bang&data=content&id=${id}&show_copyright_off=0&pcmp4=1&isbang=1`, + regExps: { + mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/, + }, + limit: 100, + _requestBoardsObj: null, + + getBoardsData() { + if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp() + this._requestBoardsObj = httpFetch('http://qukudata.kuwo.cn/q.k?op=query&cont=tree&node=2&pn=0&rn=1000&fmt=json&level=2') + return this._requestBoardsObj.promise + }, + getData(url) { + const requestDataObj = httpFetch(url) + return requestDataObj.promise + }, + filterData(rawList) { + return rawList.map(item => { + let types = [] + const _types = {} + const qualitys = new Set() + + item.n_minfo.split(';').forEach(i => { + const info = i.match(this.regExps.mInfo) + if (!info) return + + const quality = info[2] + const size = info[4].toLocaleUpperCase() + + if (qualitys.has(quality)) return + qualitys.add(quality) + + switch (quality) { + case '4000': + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { size } + break + case '2000': + types.push({ type: 'flac', size }) + _types.flac = { size } + break + case '320': + types.push({ type: '320k', size }) + _types['320k'] = { size } + break + case '128': + types.push({ type: '128k', size }) + _types['128k'] = { size } + break + } + }) + types = sortQualityArray(types) + + return { + singer: formatSinger(decodeName(item.artist)), + name: decodeName(item.name), + albumName: decodeName(item.album), + albumId: item.albumId, + songmid: item.id, + source: 'kw', + interval: formatPlayTime(parseInt(item.duration)), + img: item.pic, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + + filterBoardsData(rawList) { + // console.log(rawList) + let list = [] + for (const board of rawList) { + if (board.source != '1') continue + list.push({ + id: 'kw__' + board.sourceid, + name: board.name, + bangid: String(board.sourceid), + }) + } + return list + }, + async getBoards(retryNum = 0) { + // if (++retryNum > 3) return Promise.reject(new Error('try max num')) + // let response + // try { + // response = await this.getBoardsData() + // } catch (error) { + // return this.getBoards(retryNum) + // } + // console.log(response.body) + // if (response.statusCode !== 200 || !response.body.child) return this.getBoards(retryNum) + // const list = this.filterBoardsData(response.body.child) + // // console.log(list) + // console.log(JSON.stringify(list)) + // this.list = list + // return { + // list, + // source: 'kw', + // } + this.list = boardList + return { + list: boardList, + source: 'kw', + } + }, + + getList(id, page, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + + const requestBody = { uid: '', devId: '', sFrom: 'kuwo_sdk', user_type: 'AP', carSource: 'kwplayercar_ar_6.0.1.0_apk_keluze.apk', id, pn: page - 1, rn: this.limit } + const requestUrl = `https://wbd.kuwo.cn/api/bd/bang/bang_info?${wbdCrypto.buildParam(requestBody)}` + const request = httpFetch(requestUrl).promise + + return request.then(({ statusCode, body }) => { + const rawData = wbdCrypto.decodeData(body) + // console.log(rawData) + const data = rawData.data + if (statusCode !== 200 || rawData.code != 200 || !data.musiclist) return this.getList(id, page, retryNum) + + const total = parseInt(data.total) + const list = this.filterData(data.musiclist) + + return { + total, + list, + limit: this.limit, + page, + source: 'kw', + } + }) + }, + + // getDetailPageUrl(id) { + // return `http://www.kuwo.cn/rankList/${id}` + // }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/lyric.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/lyric.js new file mode 100644 index 000000000..69c224d3b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/lyric.js @@ -0,0 +1,262 @@ +import { httpFetch } from '../../request' +import { decodeLyric, lrcTools } from './util' +import { decodeName } from '../../index' + +/* +export default { + formatTime(time) { + let m = parseInt(time / 60) + let s = (time % 60).toFixed(2) + return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s) + }, + sortLrcArr(arr) { + const lrcSet = new Set() + let lrc = [] + let lrcT = [] + + for (const item of arr) { + if (lrcSet.has(item.time)) { + const tItem = lrc.pop() + tItem.time = lrc[lrc.length - 1].time + lrcT.push(tItem) + lrc.push(item) + } else { + lrc.push(item) + lrcSet.add(item.time) + } + } + + if (lrcT.length && lrc.length > lrcT.length) { + const tItem = lrc.pop() + tItem.time = lrc[lrc.length - 1].time + lrcT.push(tItem) + } + + return { + lrc, + lrcT, + } + }, + transformLrc(songinfo, lrclist) { + return `[ti:${songinfo.songName}]\n[ar:${songinfo.artist}]\n[al:${songinfo.album}]\n[by:]\n[offset:0]\n${lrclist ? lrclist.map(l => `[${this.formatTime(l.time)}]${l.lineLyric}\n`).join('') : '暂无歌词'}` + }, + getLyric(songId) { + const requestObj = httpFetch(`http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=${songId}`) + requestObj.promise = requestObj.promise.then(({ body }) => { + // console.log(body) + if (!body.data?.lrclist?.length) return Promise.reject(new Error('Get lyric failed')) + let lrcInfo + try { + lrcInfo = this.sortLrcArr(body.data.lrclist) + } catch { + return Promise.reject(new Error('Get lyric failed')) + } + // console.log(body.data.lrclist) + // console.log(lrcInfo.lrc, lrcInfo.lrcT) + // console.log({ + // lyric: decodeName(this.transformLrc(body.data.songinfo, lrc)), + // tlyric: decodeName(this.transformLrc(body.data.songinfo, lrcT)), + // }) + return { + lyric: decodeName(this.transformLrc(body.data.songinfo, lrcInfo.lrc)), + tlyric: lrcInfo.lrcT.length ? decodeName(this.transformLrc(body.data.songinfo, lrcInfo.lrcT)) : '', + } + }) + return requestObj + }, +} + */ + +// const buf_key = Buffer.from('yeelion') +// const buf_key_len = buf_key.length +// const buildParams = (id, isGetLyricx) => { +// let params = `user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_${id}` +// if (isGetLyricx) params += '&lrcx=1' +// const buf_str = Buffer.from(params) +// const buf_str_len = buf_str.length +// const output = new Uint16Array(buf_str_len) +// let i = 0 +// while (i < buf_str_len) { +// let j = 0 +// while (j < buf_key_len && i < buf_str_len) { +// output[i] = buf_key[j] ^ buf_str[i] +// i++ +// j++ +// } +// } +// return Buffer.from(output).toString('base64') +// } + +// console.log(buildParams('207527604', false)) +// console.log(buildParams('207527604', true)) + +const timeExp = /^\[([\d:.]*)\]{1}/g +const existTimeExp = /\[\d{1,2}:.*\d{1,4}]/ +const lyricxTag = /^<-?\d+,-?\d+>/ +export default { + /* sortLrcArr(arr) { + const lrcSet = new Set() + let lrc = [] + let lrcT = [] + let markIndex = [] + for (const item of arr) { + if (lrcSet.has(item.time)) { + if (lrc.length < 2) continue + const index = lrc.findIndex(l => l.time == item.time) + markIndex.push(index) + if (index == lrc.length - 1) { + lrcT.push({ ...lrc[index], time: item.time }) + lrc.push(item) + } else { + lrcT.push({ ...lrc[index], time: lrc[index + 1].time }) + if (item.text) { + // const lastIndex = lrc.length - 1 + // markIndex.push(lastIndex) + // lrcT.push({ ...lrc[lastIndex], time: lrc[lastIndex - 1].time }) + lrc.push(item) + } + } + } else { + lrc.push(item) + lrcSet.add(item.time) + } + } + + // console.log(markIndex) + markIndex = Array.from(new Set(markIndex)) + for (let index = markIndex.length - 1; index >= 0; index--) { + lrc.splice(markIndex[index], 1) + } + + // if (lrcT.length) { + // if (lrc.length * 0.4 < lrcT.length) { // 翻译数量需大于歌词数量的0.4倍,否则认为没有翻译 + // const tItem = lrc.pop() + // tItem.time = lrc[lrc.length - 1].time + // lrcT.push(tItem) + // } else { + // lrc = arr + // lrcT = [] + // } + // } + + console.log(lrc, lrcT) + + return { + lrc, + lrcT, + } + }, */ + sortLrcArr(arr) { + const lrcSet = new Set() + let lrc = [] + let lrcT = [] + + let isLyricx = false + for (const item of arr) { + if (lrcSet.has(item.time)) { + if (lrc.length < 2) continue + const tItem = lrc.pop() + tItem.time = lrc[lrc.length - 1].time + lrcT.push(tItem) + lrc.push(item) + } else { + lrc.push(item) + lrcSet.add(item.time) + } + if (!isLyricx && lyricxTag.test(item.text)) isLyricx = true + } + + if (!isLyricx && lrcT.length > lrc.length * 0.3 && lrc.length - lrcT.length > 6) { + throw new Error('failed') + } + + return { + lrc, + lrcT, + } + }, + transformLrc(tags, lrclist) { + return `${tags.join('\n')}\n${lrclist ? lrclist.map(l => `[${l.time}]${l.text}\n`).join('') : '暂无歌词'}` + }, + parseLrc(lrc) { + const lines = lrc.split(/\r\n|\r|\n/) + let tags = [] + let lrcArr = [] + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim() + let result = timeExp.exec(line) + if (result) { + const text = line.replace(timeExp, '').trim() + let time = RegExp.$1 + if (/\.\d\d$/.test(time)) time += '0' + lrcArr.push({ + time, + text, + }) + } else if (lrcTools.rxps.tagLine.test(line)) { + tags.push(line) + } + } + const lrcInfo = this.sortLrcArr(lrcArr) + return { + lyric: decodeName(this.transformLrc(tags, lrcInfo.lrc)), + tlyric: lrcInfo.lrcT.length ? decodeName(this.transformLrc(tags, lrcInfo.lrcT)) : '', + } + }, + // getLyric2(musicInfo, isGetLyricx = true) { + // const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`) + // requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => { + // if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body))) + // return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => { + // let lrcInfo + // console.log(Buffer.from(base64Data, 'base64').toString()) + // try { + // lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString()) + // } catch { + // return Promise.reject(new Error('Get lyric failed')) + // } + // if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '') + // lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric) + // // console.log(lrcInfo.lyric) + // // console.log(lrcInfo.tlyric) + // // console.log(lrcInfo.lxlyric) + // // console.log(JSON.stringify(lrcInfo)) + // }) + // }) + // return requestObj + // }, + getLyric(musicInfo, isGetLyricx = true) { + // this.getLyric2(musicInfo) + const requestObj = httpFetch(`http://mlyric.kuwo.cn/mobi.s?f=web&type=lyric&lrcx=${isGetLyricx ? 1 : 0}&rid=${musicInfo.songmid}&encode=utf8`) + requestObj.promise = requestObj.promise.then(async({ statusCode, body, raw }) => { + if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body))) + const lrcText = await decodeLyric(raw, isGetLyricx) + // console.log(lrcText) + // let lrcInfo + // try { + // lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString()) + // } catch { + // return Promise.reject(new Error('Get lyric failed')) + // } + let lrcInfo + // console.log(Buffer.from(base64Data, 'base64').toString()) + try { + lrcInfo = this.parseLrc(lrcText) + } catch (err) { + return Promise.reject(new Error('Get lyric failed')) + } + // console.log(lrcInfo) + if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '') + try { + lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric) + } catch { + lrcInfo.lxlyric = '' + } + lrcInfo.lyric = lrcInfo.lyric.replace(lrcTools.rxps.wordTimeAll, '') + if (!existTimeExp.test(lrcInfo.lyric)) return Promise.reject(new Error('Get lyric failed')) + // console.log(lrcInfo) + return lrcInfo + }) + return requestObj + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/musicSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/musicSearch.js new file mode 100644 index 000000000..db83c8109 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/musicSearch.js @@ -0,0 +1,126 @@ +// import '../../polyfill/array.find' + +import { httpFetch } from '../../request' +import { formatPlayTime, decodeName } from '../../index' +// import { debug } from '../../utils/env' +import { formatSinger } from './util' + +export default { + regExps: { + mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/, + }, + limit: 30, + total: 0, + page: 0, + allPage: 1, + // cancelFn: null, + musicSearch(str, page, limit) { + const musicSearchRequestObj = httpFetch(`http://search.kuwo.cn/r.s?client=kt&all=${encodeURIComponent(str)}&pn=${page - 1}&rn=${limit}&uid=794762570&ver=kwplayer_ar_9.2.2.1&vipver=1&show_copyright_off=1&newver=1&ft=music&cluster=0&strategy=2012&encoding=utf8&rformat=json&vermerge=1&mobi=1&issubtitle=1`) + return musicSearchRequestObj.promise + }, + // getImg(songId) { + // return httpGet(`http://player.kuwo.cn/webmusic/sj/dtflagdate?flag=6&rid=MUSIC_${songId}`) + // }, + // getLrc(songId) { + // return httpGet(`http://mobile.kuwo.cn/mpage/html5/songinfoandlrc?mid=${songId}&flag=0`) + // }, + handleResult(rawData) { + const result = [] + if (!rawData) return result + // console.log(rawData) + for (let i = 0; i < rawData.length; i++) { + const info = rawData[i] + let songId = info.MUSICRID.replace('MUSIC_', '') + // const format = (info.FORMATS || info.formats).split('|') + + if (!info.N_MINFO) { + console.log('N_MINFO is undefined') + return null + } + + const types = [] + const _types = {} + + let infoArr = info.N_MINFO.split(';') + for (let info of infoArr) { + info = info.match(this.regExps.mInfo) + if (info) { + switch (info[2]) { + case '4000': + types.push({ type: 'flac24bit', size: info[4] }) + _types.flac24bit = { + size: info[4].toLocaleUpperCase(), + } + break + case '2000': + types.push({ type: 'flac', size: info[4] }) + _types.flac = { + size: info[4].toLocaleUpperCase(), + } + break + case '320': + types.push({ type: '320k', size: info[4] }) + _types['320k'] = { + size: info[4].toLocaleUpperCase(), + } + break + case '128': + types.push({ type: '128k', size: info[4] }) + _types['128k'] = { + size: info[4].toLocaleUpperCase(), + } + break + } + } + } + types.reverse() + + let interval = parseInt(info.DURATION) + + result.push({ + name: decodeName(info.SONGNAME), + singer: formatSinger(decodeName(info.ARTIST)), + source: 'kw', + // img = (info.album.name === '' || info.album.name === '空') + // ? `http://player.kuwo.cn/webmusic/sj/dtflagdate?flag=6&rid=MUSIC_160911.jpg` + // : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${info.album.mid}.jpg` + songmid: songId, + albumId: decodeName(info.ALBUMID || ''), + interval: Number.isNaN(interval) ? 0 : formatPlayTime(interval), + albumName: info.ALBUM ? decodeName(info.ALBUM) : '', + lrc: null, + img: null, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + } + // console.log(result) + return result + }, + search(str, page = 1, limit, retryNum = 0) { + if (retryNum > 2) return Promise.reject(new Error('try max num')) + if (limit == null) limit = this.limit + // http://newlyric.kuwo.cn/newlyric.lrc?62355680 + return this.musicSearch(str, page, limit).then(({ body: result }) => { + // console.log(result) + if (!result || (result.TOTAL !== '0' && result.SHOW === '0')) return this.search(str, page, limit, ++retryNum) + let list = this.handleResult(result.abslist) + + if (list == null) return this.search(str, page, limit, ++retryNum) + + this.total = parseInt(result.TOTAL) + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + total: this.total, + limit, + source: 'kw', + }) + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/pic.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/pic.js new file mode 100644 index 000000000..9f1ea7a14 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/pic.js @@ -0,0 +1,9 @@ +import { httpFetch } from '../../request' + +export default { + getPic({ songmid }) { + const requestObj = httpFetch(`http://artistpicserver.kuwo.cn/pic.web?corp=kuwo&type=rid_pic&pictype=500&size=500&rid=${songmid}`) + requestObj.promise = requestObj.promise.then(({ body }) => /^http/.test(body) ? body : null) + return requestObj.promise + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/songList.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/songList.js new file mode 100644 index 000000000..6ab4727a7 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/songList.js @@ -0,0 +1,491 @@ +import { httpFetch } from '../../request' +import { formatPlayTime, decodeName } from '../../index' +import { formatSinger, objStr2JSON } from './util' +import album from './album' + +export default { + _requestObj_tags: null, + _requestObj_hotTags: null, + _requestObj_list: null, + limit_list: 36, + limit_song: 1000, + successCode: 200, + sortList: [ + { + name: '最新', + id: 'new', + }, + { + name: '最热', + id: 'hot', + }, + ], + regExps: { + mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/, + // http://www.kuwo.cn/playlist_detail/2886046289 + // https://m.kuwo.cn/h5app/playlist/2736267853?t=qqfriend + listDetailLink: /^.+\/playlist(?:_detail)?\/(\d+)(?:\?.*|&.*$|#.*$|$)/, + }, + tagsUrl: 'http://wapi.kuwo.cn/api/pc/classify/playlist/getTagList?cmd=rcm_keyword_playlist&user=0&prod=kwplayer_pc_9.0.5.0&vipver=9.0.5.0&source=kwplayer_pc_9.0.5.0&loginUid=0&loginSid=0&appUid=76039576', + hotTagUrl: 'http://wapi.kuwo.cn/api/pc/classify/playlist/getRcmTagList?loginUid=0&loginSid=0&appUid=76039576', + getListUrl({ sortId, id, type, page }) { + if (!id) return `http://wapi.kuwo.cn/api/pc/classify/playlist/getRcmPlayList?loginUid=0&loginSid=0&appUid=76039576&&pn=${page}&rn=${this.limit_list}&order=${sortId}` + switch (type) { + case '10000': return `http://wapi.kuwo.cn/api/pc/classify/playlist/getTagPlayList?loginUid=0&loginSid=0&appUid=76039576&pn=${page}&id=${id}&rn=${this.limit_list}` + case '43': return `http://mobileinterfaces.kuwo.cn/er.s?type=get_pc_qz_data&f=web&id=${id}&prod=pc` + } + // http://wapi.kuwo.cn/api/pc/classify/playlist/getTagPlayList?loginUid=0&loginSid=0&appUid=76039576&id=173&pn=1&rn=100 + }, + getListDetailUrl(id, page) { + // http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=2858093057&pn=0&rn=100&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1 + return `http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=${id}&pn=${page - 1}&rn=${this.limit_song}&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1` + // http://mobileinterfaces.kuwo.cn/er.s?type=get_pc_qz_data&f=web&id=140&prod=pc + }, + + // http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=2849349915&pn=0&rn=100&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1 + // 获取标签 + getTag(tryNum = 0) { + if (this._requestObj_tags) this._requestObj_tags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_tags = httpFetch(this.tagsUrl) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getTag(++tryNum) + return this.filterTagInfo(body.data) + }) + }, + // 获取标签 + getHotTag(tryNum = 0) { + if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_hotTags = httpFetch(this.hotTagUrl) + return this._requestObj_hotTags.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getHotTag(++tryNum) + return this.filterInfoHotTag(body.data[0].data) + }) + }, + filterInfoHotTag(rawList) { + return rawList.map(item => ({ + id: `${item.id}-${item.digest}`, + name: item.name, + source: 'kw', + })) + }, + filterTagInfo(rawList) { + return rawList.map(type => ({ + name: type.name, + list: type.data.map(item => ({ + parent_id: type.id, + parent_name: type.name, + id: `${item.id}-${item.digest}`, + name: item.name, + source: 'kw', + })), + })) + }, + + // 获取列表数据 + getList(sortId, tagId, page, tryNum = 0) { + if (this._requestObj_list) this._requestObj_list.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + let id + let type + if (tagId) { + let arr = tagId.split('-') + id = arr[0] + type = arr[1] + } else { + id = null + } + this._requestObj_list = httpFetch(this.getListUrl({ sortId, id, type, page })) + return this._requestObj_list.promise.then(({ body }) => { + if (!id || type == '10000') { + if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) + return { + list: this.filterList(body.data.data), + total: body.data.total, + page: body.data.pn, + limit: body.data.rn, + source: 'kw', + } + } else if (!body.length) { + return this.getList(sortId, tagId, page, ++tryNum) + } + return { + list: this.filterList2(body), + total: 1000, + page, + limit: 1000, + source: 'kw', + } + }) + }, + + + /** + * 格式化播放数量 + * @param {*} num + */ + formatPlayCount(num) { + if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿' + if (num > 10000) return parseInt(num / 1000) / 10 + '万' + return num + }, + filterList(rawData) { + return rawData.map(item => ({ + play_count: this.formatPlayCount(item.listencnt), + id: `digest-${item.digest}__${item.id}`, + author: item.uname, + name: item.name, + // time: item.publish_time, + total: item.total, + img: item.img, + grade: item.favorcnt / 10, + desc: item.desc, + source: 'kw', + })) + }, + filterList2(rawData) { + const list = [] + const allowedTypes = ['songlist', 'list', 'album'] + // digest: 5 = list, 8 = songlist, 13 = album, 4 = artist + rawData.forEach(item => { + item.list.forEach(item => { + if (!allowedTypes.includes(item.type)) return + list.push({ + play_count: item.play_count && this.formatPlayCount(item.listencnt), + id: `digest-${item.digest}__${item.id}`, + author: item.uname, + name: item.name, + total: item.total, + // time: item.publish_time, + img: item.img, + grade: item.favorcnt && item.favorcnt / 10, + desc: item.desc, + source: 'kw', + }) + }) + }) + return list + }, + + getListDetailDigest8(id, page, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + + const requestObj = httpFetch(this.getListDetailUrl(id, page)) + return requestObj.promise.then(({ body }) => { + if (body.result !== 'ok') return this.getListDetail(id, page, ++tryNum) + return { + list: this.filterListDetail(body.musiclist), + page, + limit: body.rn, + total: body.total, + source: 'kw', + info: { + name: body.title, + img: body.pic, + desc: body.info, + author: body.uname, + play_count: this.formatPlayCount(body.playnum), + }, + } + }) + }, + getListDetailDigest5Info(id, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + const requestObj = httpFetch(`http://qukudata.kuwo.cn/q.k?op=query&cont=ninfo&node=${id}&pn=0&rn=1&fmt=json&src=mbox&level=2`) + return requestObj.promise.then(({ statusCode, body }) => { + if (statusCode != 200 || !body.child) return this.getListDetail(id, ++tryNum) + // console.log(body) + return body.child.length ? body.child[0].sourceid : null + }) + }, + getListDetailDigest5Music(id, page, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + const requestObj = httpFetch(`http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=${id}&pn=${page - 1}}&rn=${this.limit_song}&encode=utf-8&keyset=pl2012&identity=kuwo&pcmp4=1`) + return requestObj.promise.then(({ body }) => { + // console.log(body) + if (body.result !== 'ok') return this.getListDetail(id, page, ++tryNum) + return { + list: this.filterListDetail(body.musiclist), + page, + limit: body.rn, + total: body.total, + source: 'kw', + info: { + name: body.title, + img: body.pic, + desc: body.info, + author: body.uname, + play_count: this.formatPlayCount(body.playnum), + }, + } + }) + }, + async getListDetailDigest5(id, page, retryNum) { + const detailId = await this.getListDetailDigest5Info(id, retryNum) + return this.getListDetailDigest5Music(detailId, page, retryNum) + }, + + filterBDListDetail(rawList) { + return rawList.map(item => { + let types = [] + let _types = {} + for (let info of item.audios) { + info.size = info.size?.toLocaleUpperCase() + switch (info.bitrate) { + case '4000': + types.push({ type: 'flac24bit', size: info.size }) + _types.flac24bit = { + size: info.size, + } + break + case '2000': + types.push({ type: 'flac', size: info.size }) + _types.flac = { + size: info.size, + } + break + case '320': + types.push({ type: '320k', size: info.size }) + _types['320k'] = { + size: info.size, + } + break + case '128': + types.push({ type: '128k', size: info.size }) + _types['128k'] = { + size: info.size, + } + break + } + } + types.reverse() + + return { + singer: item.artists.map(s => s.name).join('、'), + name: item.name, + albumName: item.album, + albumId: item.albumId, + songmid: item.id, + source: 'kw', + interval: formatPlayTime(item.duration), + img: item.albumPic, + releaseDate: item.releaseDate, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + getReqId() { + function t() { + return (65536 * (1 + Math.random()) | 0).toString(16).substring(1) + } + return t() + t() + t() + t() + t() + t() + t() + t() + }, + async getListDetailMusicListByBDListInfo(id, source) { + const { body: infoData } = await httpFetch(`https://bd-api.kuwo.cn/api/service/playlist/info/${id}?reqId=${this.getReqId()}&source=${source}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + plat: 'h5', + }, + }).promise.catch(() => ({ code: 0 })) + + if (infoData.code != 200) return null + + return { + name: infoData.data.name, + img: infoData.data.pic, + desc: infoData.data.description, + author: infoData.data.creatorName, + play_count: infoData.data.playNum, + } + }, + async getListDetailMusicListByBDUserPub(id) { + const { body: infoData } = await httpFetch(`https://bd-api.kuwo.cn/api/ucenter/users/pub/${id}?reqId=${this.getReqId()}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + plat: 'h5', + }, + }).promise.catch(() => ({ code: 0 })) + + if (infoData.code != 200) return null + + // console.log(infoData) + return { + name: infoData.data.userInfo.nickname + '喜欢的音乐', + img: infoData.data.userInfo.headImg, + desc: '', + author: infoData.data.userInfo.nickname, + play_count: '', + } + }, + async getListDetailMusicListByBDList(id, source, page, tryNum = 0) { + const { body: listData } = await httpFetch(`https://bd-api.kuwo.cn/api/service/playlist/${id}/musicList?reqId=${this.getReqId()}&source=${source}&pn=${page}&rn=${this.limit_song}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + plat: 'h5', + }, + }).promise.catch(() => { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + return this.getListDetailMusicListByBDList(id, source, page, ++tryNum) + }) + + if (listData.code !== 200) return Promise.reject(new Error('failed')) + + return { + list: this.filterBDListDetail(listData.data.list), + page, + limit: listData.data.pageSize, + total: listData.data.total, + source: 'kw', + } + }, + async getListDetailMusicListByBD(id, page) { + const uid = /uid=(\d+)/.exec(id)?.[1] + const listId = /playlistId=(\d+)/.exec(id)?.[1] + const source = /source=(\d+)/.exec(id)?.[1] + if (!listId) return Promise.reject(new Error('failed')) + + const task = [this.getListDetailMusicListByBDList(listId, source, page)] + switch (source) { + case '4': + task.push(this.getListDetailMusicListByBDListInfo(listId, source)) + break + case '5': + task.push(this.getListDetailMusicListByBDUserPub(uid ?? listId)) + break + } + const [listData, info] = await Promise.all(task) + listData.info = info ?? { + name: '', + img: '', + desc: '', + author: '', + play_count: '', + } + // console.log(listData) + return listData + }, + + // 获取歌曲列表内的音乐 + getListDetail(id, page, retryNum = 0) { + // console.log(id) + // https://h5app.kuwo.cn/m/bodian/collection.html?uid=000&playlistId=000&source=5&ownerId=000 + // https://h5app.kuwo.cn/m/bodian/collection.html?uid=000&playlistId=000&source=4&ownerId= + if (/\/bodian\//.test(id)) return this.getListDetailMusicListByBD(id, page) + if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + else if (/^digest-/.test(id)) { + let [digest, _id] = id.split('__') + digest = digest.replace('digest-', '') + id = _id + switch (digest) { + case '8': + break + case '13': return album.getAlbumListDetail(id, page, retryNum) + case '5': + default: return this.getListDetailDigest5(id, page, retryNum) + } + } + return this.getListDetailDigest8(id, page, retryNum) + }, + filterListDetail(rawData) { + // console.log(rawData) + return rawData.map(item => { + let infoArr = item.N_MINFO.split(';') + let types = [] + let _types = {} + for (let info of infoArr) { + info = info.match(this.regExps.mInfo) + if (info) { + switch (info[2]) { + case '4000': + types.push({ type: 'flac24bit', size: info[4] }) + _types.flac24bit = { + size: info[4].toLocaleUpperCase(), + } + break + case '2000': + types.push({ type: 'flac', size: info[4] }) + _types.flac = { + size: info[4].toLocaleUpperCase(), + } + break + case '320': + types.push({ type: '320k', size: info[4] }) + _types['320k'] = { + size: info[4].toLocaleUpperCase(), + } + break + case '128': + types.push({ type: '128k', size: info[4] }) + _types['128k'] = { + size: info[4].toLocaleUpperCase(), + } + break + } + } + } + types.reverse() + + return { + singer: formatSinger(decodeName(item.artist)), + name: decodeName(item.name), + albumName: decodeName(item.album), + albumId: item.albumid, + songmid: item.id, + source: 'kw', + interval: formatPlayTime(parseInt(item.duration)), + img: null, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + getTags() { + return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'kw' })) + }, + getDetailPageUrl(id) { + if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + else if (/^digest-/.test(id)) { + let result = id.split('__') + id = result[1] + } + return `http://www.kuwo.cn/playlist_detail/${id}` + }, + + search(text, page, limit = 20) { + return httpFetch(`http://search.kuwo.cn/r.s?all=${encodeURIComponent(text)}&pn=${page - 1}&rn=${limit}&rformat=json&encoding=utf8&ver=mbox&vipver=MUSIC_8.7.7.0_BCS37&plat=pc&devid=28156413&ft=playlist&pay=0&needliveshow=0`) + .promise.then(({ body }) => { + body = objStr2JSON(body) + // console.log(body) + return { + list: body.abslist.map(item => { + return { + play_count: this.formatPlayCount(item.playcnt), + id: String(item.playlistid), + author: decodeName(item.nickname), + name: decodeName(item.name), + total: item.songnum, + // time: item.publish_time, + img: item.pic, + desc: decodeName(item.intro), + source: 'kw', + } + }), + limit, + total: parseInt(body.TOTAL), + source: 'kw', + } + }) + }, +} + +// getList +// getTags +// getListDetail diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/tipSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/tipSearch.js new file mode 100644 index 000000000..338b60083 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/tipSearch.js @@ -0,0 +1,32 @@ +// import { decodeName } from '../../index' +// import { tokenRequest } from './util' +import { httpFetch } from '../../request' + +export default { + regExps: { + relWord: /RELWORD=(.+)/, + }, + requestObj: null, + async tipSearchBySong(str) { + // 报错403,加了referer还是有问题(直接换一个 + // this.requestObj = await tokenRequest(`http://www.kuwo.cn/api/www/search/searchKey?key=${encodeURIComponent(str)}`) + + this.cancelTipSearch() + this.requestObj = httpFetch(`https://tips.kuwo.cn/t.s?corp=kuwo&newver=3&p2p=1¬race=0&c=mbox&w=${encodeURIComponent(str)}&encoding=utf8&rformat=json`, { + Referer: 'http://www.kuwo.cn/', + }) + return this.requestObj.promise.then(({ body, statusCode }) => { + if (statusCode != 200 || !body.WORDITEMS) return Promise.reject(new Error('请求失败')) + return body.WORDITEMS + }) + }, + handleResult(rawData) { + return rawData.map(item => item.RELWORD) + }, + cancelTipSearch() { + if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp() + }, + async search(str) { + return this.tipSearchBySong(str).then(result => this.handleResult(result)) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/util.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/util.js new file mode 100644 index 000000000..df6d87ad6 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/kw/util.js @@ -0,0 +1,225 @@ +import { createCipheriv, createDecipheriv } from 'crypto' +import { toMD5 } from '../utils' +import { inflate } from 'zlib' + + +const handleInflate = async(data) => { + return new Promise((resolve, reject) => { + inflate(data, (err, result) => { + if (err) { + reject(err) + return + } + resolve(result) + }) + }) +} + +const buf_key = Buffer.from('yeelion') +const buf_key_len = buf_key.length + +export const decodeLyric = async(rawData, isGetLyricx) => { + const buf = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData) + if (buf.toString('utf8', 0, 10).toLowerCase() !== 'tp=content') return '' + const lrcData = await handleInflate(buf.subarray(buf.indexOf('\r\n\r\n') + 4)) + if (!isGetLyricx) return lrcData.toString('utf8') + const buf_str = Buffer.from(lrcData.toString(), 'base64') + const buf_str_len = buf_str.length + const output = new Uint8Array(buf_str_len) + let i = 0 + while (i < buf_str_len) { + let j = 0 + while (j < buf_key_len && i < buf_str_len) { + output[i] = buf_str[i] ^ buf_key[j] + i++ + j++ + } + } + + return Buffer.from(output).toString('utf8') +} + +export const objStr2JSON = str => { + return JSON.parse(str.replace(/('(?=(,\s*')))|('(?=:))|((?<=([:,]\s*))')|((?<={)')|('(?=}))/g, '"')) +} + + +export const formatSinger = rawData => rawData.replace(/&/g, '、') + +// export const matchToken = headers => { +// try { +// return headers['set-cookie'][0].match(/kw_token=(\w+)/)[1] +// } catch (err) { +// return null +// } +// } + +// const wait = time => new Promise(resolve => setTimeout(() => resolve(), time)) + + +// export const getToken = (retryNum = 0) => new Promise((resolve, reject) => { +// if (retryNum > 2) return Promise.reject(new Error('try max num')) + +// if (kw_token.isGetingToken) return wait(1000).then(() => getToken(retryNum).then(token => resolve(token))) +// if (kw_token.token) return resolve(kw_token.token) +// kw_token.isGetingToken = true +// httpGet('http://www.kuwo.cn/', (err, resp) => { +// kw_token.isGetingToken = false +// if (err) return getToken(++retryNum) +// if (resp.statusCode != 200) return reject(new Error('获取失败')) +// const token = kw_token.token = matchToken(resp.headers) +// resolve(token) +// }) +// }) + +// export const tokenRequest = async(url, options = {}) => { +// let token = kw_token.token +// if (!token) token = await getToken() +// if (!options.headers) { +// options.headers = { +// Referer: 'http://www.kuwo.cn/', +// csrf: token, +// cookie: 'kw_token=' + token, +// } +// } +// const requestObj = httpFetch(url, options) +// requestObj.promise = requestObj.promise.then(resp => { +// // console.log(resp) +// if (resp.statusCode == 200) { +// kw_token.token = matchToken(resp.headers) +// } +// return resp +// }) +// return requestObj +// } + +export const lrcTools = { + rxps: { + wordLine: /^(\[\d{1,2}:.*\d{1,4}])\s*(\S+(?:\s+\S+)*)?\s*/, + tagLine: /\[(ver|ti|ar|al|offset|by|kuwo):\s*(\S+(?:\s+\S+)*)\s*]/, + wordTimeAll: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/g, + wordTime: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/, + }, + offset: 1, + offset2: 1, + isOK: false, + lines: [], + tags: [], + getWordInfo(str, str2, prevWord) { + const offset = parseInt(str) + const offset2 = parseInt(str2) + let startTime = Math.trunc(Math.abs((offset + offset2) / (this.offset * 2))) + let endTime = Math.trunc(Math.abs((offset - offset2) / (this.offset2 * 2)) + startTime) + if (prevWord) { + if (startTime < prevWord.endTime) { + prevWord.endTime = startTime + if (prevWord.startTime > prevWord.endTime) { + prevWord.startTime = prevWord.endTime + } + + prevWord.newTimeStr = `<${prevWord.startTime},${Math.trunc(prevWord.endTime - prevWord.startTime)}>` + // console.log(prevWord) + } + } + return { + startTime, + endTime, + timeStr: `<${startTime},${Math.trunc(endTime - startTime)}>`, + } + }, + parseLine(line) { + if (line.length < 6) return + let result = this.rxps.wordLine.exec(line) + if (result) { + const time = result[1] + let words = result[2] + if (words == null) { + words = '' + } + const wordTimes = words.match(this.rxps.wordTimeAll) + if (!wordTimes) return + // console.log(wordTimes) + let preTimeInfo + for (const timeStr of wordTimes) { + const result = this.rxps.wordTime.exec(timeStr) + const wordInfo = this.getWordInfo(result[1], result[2], preTimeInfo) + words = words.replace(timeStr, wordInfo.timeStr) + if (preTimeInfo?.newTimeStr) words = words.replace(preTimeInfo.timeStr, preTimeInfo.newTimeStr) + preTimeInfo = wordInfo + } + this.lines.push(time + words) + return + } + result = this.rxps.tagLine.exec(line) + if (!result) return + if (result[1] == 'kuwo') { + let content = result[2] + if (content != null && content.includes('][')) { + content = content.substring(0, content.indexOf('][')) + } + const valueOf = parseInt(content, 8) + this.offset = Math.trunc(valueOf / 10) + this.offset2 = Math.trunc(valueOf % 10) + if (this.offset == 0 || Number.isNaN(this.offset) || this.offset2 == 0 || Number.isNaN(this.offset2)) { + this.isOK = false + } + } else { + this.tags.push(line) + } + }, + parse(lrc) { + // console.log(lrc) + const lines = lrc.split(/\r\n|\r|\n/) + const tools = Object.create(this) + tools.isOK = true + tools.offset = 1 + tools.offset2 = 1 + tools.lines = [] + tools.tags = [] + + for (const line of lines) { + if (!tools.isOK) throw new Error('failed') + tools.parseLine(line) + } + if (!tools.lines.length) return '' + let lrcs = tools.lines.join('\n') + if (tools.tags.length) lrcs = `${tools.tags.join('\n')}\n${lrcs}` + // console.log(lrcs) + return lrcs + }, +} + + +const createAesEncrypt = (buffer, mode, key, iv) => { + const cipher = createCipheriv(mode, key, iv) + return Buffer.concat([cipher.update(buffer), cipher.final()]) +} + +const createAesDecrypt = (buffer, mode, key, iv) => { + const cipher = createDecipheriv(mode, key, iv) + return Buffer.concat([cipher.update(buffer), cipher.final()]) +} + +export const wbdCrypto = { + aesMode: 'aes-128-ecb', + aesKey: Buffer.from([112, 87, 39, 61, 199, 250, 41, 191, 57, 68, 45, 114, 221, 94, 140, 228], 'binary'), + aesIv: '', + appId: 'y67sprxhhpws', + decodeData(base64Result) { + const data = Buffer.from(decodeURIComponent(base64Result), 'base64') + return JSON.parse(createAesDecrypt(data, this.aesMode, this.aesKey, this.aesIv).toString()) + }, + createSign(data, time) { + const str = `${this.appId}${data}${time}` + return toMD5(str).toUpperCase() + }, + buildParam(jsonData) { + const data = Buffer.from(JSON.stringify(jsonData)) + const time = Date.now() + + const encodeData = createAesEncrypt(data, this.aesMode, this.aesKey, this.aesIv).toString('base64') + const sign = this.createSign(encodeData, time) + + return `data=${encodeURIComponent(encodeData)}&time=${time}&appId=${this.appId}&sign=${sign}` + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/album.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/album.js new file mode 100644 index 000000000..b9b4435a4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/album.js @@ -0,0 +1,51 @@ +import { createHttpFetch } from './utils' +import { filterMusicInfoList } from './musicInfo' +import { formatPlayCount } from '../../index' + +export default { + /** + * 通过AlbumId获取专辑 + * @param {*} id + * @param {*} page + */ + async getAlbumDetail(id, page = 1) { + const list = await createHttpFetch(`http://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/queryAlbumSong?albumId=${id}&pageNo=${page}`) + if (!list.songList) return Promise.reject(new Error('Get album list error.')) + + const songList = filterMusicInfoList(list.songList) + const listInfo = await this.getAlbumInfo(id) + + return { + list: songList || [], + page, + limit: listInfo.total, + total: listInfo.total, + source: 'mg', + info: { + name: listInfo.name, + img: listInfo.image, + desc: listInfo.desc, + author: listInfo.author, + play_count: listInfo.play_count, + }, + } + }, + /** + * 通过AlbumId获取专辑信息 + * @param {*} id + * @param {*} page + */ + async getAlbumInfo(id) { + const info = await createHttpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/resource/album/v2.0?albumId=${id}`) + if (!info) return Promise.reject(new Error('Get album info error.')) + + return { + name: info.title, + image: info.imgItems.length ? info.imgItems[0].img : null, + desc: info.summary, + author: info.singer, + play_count: formatPlayCount(info.opNumItem.playNum), + total: info.totalCount, + } + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/api-test.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/api-test.js new file mode 100644 index 000000000..627ab4919 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/api-test.js @@ -0,0 +1,26 @@ +import { httpFetch } from '../../request' +import { requestMsg } from '../../message' +import { headers, timeout } from '../options' +import { dnsLookup } from '../utils' + +const api_test = { + getMusicUrl(songInfo, type) { + const requestObj = httpFetch(`http://ts.tempmusics.tk/url/mg/${songInfo.copyrightId}/${type}`, { + method: 'get', + timeout, + headers, + lookup: dnsLookup, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { + if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) + switch (body.code) { + case 0: return Promise.resolve({ type, url: body.data }) + default: return Promise.reject(new Error(requestMsg.fail)) + } + }) + return requestObj + }, +} + +export default api_test diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/comment.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/comment.js new file mode 100644 index 000000000..dfa1d4b0c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/comment.js @@ -0,0 +1,96 @@ +import { httpFetch } from '../../request' +import getSongId from './songId' +import { dateFormat2 } from '../../index' + +export default { + _requestObj: null, + _requestObj2: null, + _requestObj3: null, + lastCommentIds: new Map(), + async getComment(musicInfo, page = 1, limit = 20) { + if (this._requestObj) this._requestObj.cancelHttp() + if (!musicInfo.songId) { + let id = await getSongId(musicInfo) + if (!id) throw new Error('获取评论失败') + musicInfo.songId = id + } + if (page === 1) this.lastCommentIds.clear() + const lastCommentId = this.lastCommentIds.get(String(page)) || '' + if (!lastCommentId && page > 1) throw new Error('获取评论失败') + // const _requestObj = httpFetch(`https://music.migu.cn/v3/api/comment/listComments?targetId=${musicInfo.songId}&pageSize=${limit}&pageNo=${page}`, { + const _requestObj = httpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/user/comment/stack/v1.0?pageSize=${limit}&queryType=1&resourceId=${musicInfo.songId}&resourceType=2&commentId=${lastCommentId}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', + // Referer: 'https://music.migu.cn', + }, + }) + const { body, statusCode } = await _requestObj.promise + // console.log(body) + if (statusCode != 200 || body.code !== '000000') throw new Error('获取评论失败') + const total = parseInt(body.data.commentNums) + const list = this.filterComment(body.data.comments) + this.lastCommentIds.set(String(page + 1), list.length ? list[list.length - 1].id : '') + return { source: 'mg', comments: list, total, page, limit, maxPage: Math.ceil(total / limit) || 1 } + }, + async getHotComment(musicInfo, page = 1, limit = 20) { + if (this._requestObj2) this._requestObj2.cancelHttp() + + if (!musicInfo.songId) { + let id = await getSongId(musicInfo) + if (!id) throw new Error('获取评论失败') + musicInfo.songId = id + } + + // const _requestObj2 = httpFetch(`https://music.migu.cn/v3/api/comment/listTopComments?targetId=${musicInfo.songId}&pageSize=${limit}&pageNo=${page}`, { + const _requestObj2 = httpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/user/comment/stack/v1.0?pageSize=${limit}&queryType=2&resourceId=${musicInfo.songId}&resourceType=2&hotCommentStart=${(page - 1) * limit}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', + // Referer: 'https://music.migu.cn', + }, + }) + const { body, statusCode } = await _requestObj2.promise + // console.log(body) + if (statusCode != 200 || body.code !== '000000') throw new Error('获取热门评论失败') + const total = parseInt(body.data.cfgHotCount) + return { source: 'mg', comments: this.filterComment(body.data.hotComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 } + }, + async getReplyComment(musicInfo, replyId, page = 1, limit = 10) { + if (this._requestObj2) this._requestObj2.cancelHttp() + + // const _requestObj2 = httpFetch(`https://music.migu.cn/v3/api/comment/listCommentsById?commentId=${replyId}&pageSize=${limit}&pageNo=${page}`, { + const _requestObj2 = httpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/user/comment/stack/${replyId}/v1.0?pageSize=${limit}&queryType=2&resourceId=${musicInfo.songId}&resourceType=2&start=${(page - 1) * limit}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', + }, + }) + const { body, statusCode } = await _requestObj2.promise + // console.log(body) + if (statusCode != 200 || body.code !== '000000') throw new Error('获取回复评论失败') + const total = parseInt(body.data.replyTotalCount) + return { source: 'mg', comments: this.filterComment(body.data.mainCommentItem.replyComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 } + }, + filterComment(rawList) { + return rawList.map(item => ({ + id: item.commentId, + text: item.commentInfo, + time: item.commentTime, + timeStr: dateFormat2(new Date(item.commentTime).getTime()), + userName: item.user.nickName, + avatar: item.user.middleIcon || item.user.bigIcon || item.user.smallIcon, + userId: item.user.userId, + likedCount: item.opNumItem.thumbNum, + replyNum: item.replyTotalCount, + reply: item.replyComments.map(c => ({ + id: c.replyId, + text: c.replyInfo, + time: c.replyTime, + timeStr: dateFormat2(new Date(c.replyTime).getTime()), + userName: c.user.nickName, + avatar: c.user.middleIcon || c.user.bigIcon || c.user.smallIcon, + userId: c.user.userId, + likedCount: null, + replyNum: null, + })), + })) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/hotSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/hotSearch.js new file mode 100644 index 000000000..4f0557098 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/hotSearch.js @@ -0,0 +1,18 @@ +import { httpFetch } from '../../request' + +export default { + _requestObj: null, + async getList(retryNum = 0) { + if (this._requestObj) this._requestObj.cancelHttp() + if (retryNum > 2) return Promise.reject(new Error('try max num')) + + const _requestObj = httpFetch('http://jadeite.migu.cn:7090/music_search/v3/search/hotword') + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.code !== '000000') throw new Error('获取热搜词失败') + // console.log(body, statusCode) + return { source: 'mg', list: this.filterList(body.data.hotwords[0].hotwordList) } + }, + filterList(rawList) { + return rawList.filter(item => item.resourceType == 'song').map(item => item.word) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/index.js new file mode 100644 index 000000000..6fec79872 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/index.js @@ -0,0 +1,32 @@ +import { apis } from '../api-source' +import leaderboard from './leaderboard' +import songList from './songList' +import musicSearch from './musicSearch' +import pic from './pic' +import lyric from './lyric' +import hotSearch from './hotSearch' +import comment from './comment' +// import tipSearch from './tipSearch' + +const mg = { + // tipSearch, + songList, + musicSearch, + leaderboard, + hotSearch, + comment, + getMusicUrl(songInfo, type) { + return apis('mg').getMusicUrl(songInfo, type) + }, + getLyric(songInfo) { + return lyric.getLyric(songInfo) + }, + getPic(songInfo) { + return pic.getPic(songInfo) + }, + getMusicDetailPageUrl(songInfo) { + return `http://music.migu.cn/v3/music/song/${songInfo.copyrightId}` + }, +} + +export default mg diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/leaderboard.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/leaderboard.js new file mode 100644 index 000000000..bad2a3c0d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/leaderboard.js @@ -0,0 +1,243 @@ +import { httpFetch } from '../../request' +import { filterMusicInfoList } from './musicInfo' + +// const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }] + +// const boardList = [ +// { id: 'mg__27553319', name: '尖叫新歌榜', bangid: '27553319', webId: 'jianjiao_newsong' }, +// { id: 'mg__27186466', name: '尖叫热歌榜', bangid: '27186466', webId: 'jianjiao_hotsong' }, +// { id: 'mg__27553408', name: '尖叫原创榜', bangid: '27553408', webId: 'jianjiao_original' }, +// { id: 'mg__23189800', name: '港台榜', bangid: '23189800', webId: 'hktw' }, +// { id: 'mg__23189399', name: '内地榜', bangid: '23189399', webId: 'mainland' }, +// { id: 'mg__19190036', name: '欧美榜', bangid: '19190036', webId: 'eur_usa' }, +// { id: 'mg__23189813', name: '日韩榜', bangid: '23189813', webId: 'jpn_kor' }, +// { id: 'mg__23190126', name: '彩铃榜', bangid: '23190126', webId: 'coloring' }, +// { id: 'mg__15140045', name: 'KTV榜', bangid: '15140045', webId: 'ktv' }, +// { id: 'mg__15140034', name: '网络榜', bangid: '15140034', webId: 'network' }, +// // { id: 'mg__21958042', name: '美国iTunes榜', bangid: '21958042', webId: 'itunes' }, +// // { id: 'mg__21975570', name: '美国billboard榜', bangid: '21975570', webId: 'billboard' }, +// // { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815', webId: 'hito' }, +// // { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943', webId: 'mnet' }, +// // { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437', webId: 'uk' }, +// ] +const boardList = [ + { + id: 'mg__27553319', + name: '新歌榜', + bangid: '27553319', + source: 'mg', + }, + { + id: 'mg__27186466', + name: '热歌榜', + bangid: '27186466', + source: 'mg', + }, + { + id: 'mg__27553408', + name: '原创榜', + bangid: '27553408', + source: 'mg', + }, + { + id: 'mg__75959118', + name: '音乐风向榜', + bangid: '75959118', + source: 'mg', + }, + { + id: 'mg__76557036', + name: '彩铃分贝榜', + bangid: '76557036', + source: 'mg', + }, + { + id: 'mg__76557745', + name: '会员臻爱榜', + bangid: '76557745', + source: 'mg', + }, + { + id: 'mg__23189800', + name: '港台榜', + bangid: '23189800', + source: 'mg', + }, + { + id: 'mg__23189399', + name: '内地榜', + bangid: '23189399', + source: 'mg', + }, + { + id: 'mg__19190036', + name: '欧美榜', + bangid: '19190036', + source: 'mg', + }, + { + id: 'mg__83176390', + name: '国风金曲榜', + bangid: '83176390', + source: 'mg', + }, +] +export default { + limit: 200, + list: [ + { + id: 'mgyyb', + name: '音乐榜', + bangid: '27553319', + }, + { + id: 'mgysb', + name: '影视榜', + bangid: '23603721', + }, + { + id: 'mghybnd', + name: '华语内地榜', + bangid: '23603926', + }, + { + id: 'mghyjqbgt', + name: '华语港台榜', + bangid: '23603954', + }, + { + id: 'mgomb', + name: '欧美榜', + bangid: '23603974', + }, + { + id: 'mgrhb', + name: '日韩榜', + bangid: '23603982', + }, + { + id: 'mgwlb', + name: '网络榜', + bangid: '23604058', + }, + { + id: 'mgclb', + name: '彩铃榜', + bangid: '23604023', + }, + { + id: 'mgktvb', + name: 'KTV榜', + bangid: '23604040', + }, + { + id: 'mgrcb', + name: '原创榜', + bangid: '23604032', + }, + ], + getUrl(id, page) { + return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/querycontentbyId.do?columnId=${id}&needAll=0` + // return `http://m.music.migu.cn/migu/remoting/cms_list_tag?nid=${id}&pageSize=${this.limit}&pageNo=${page - 1}` + }, + successCode: '000000', + requestBoardsObj: null, + getBoardsData() { + if (this.requestBoardsObj) this._requestBoardsObj.cancelHttp() + this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/pc/bmw/rank/rank-index/v1.0', { + // this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/rank-list/release', { + // this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/indexrank.do?templateVersion=8', { + headers: { + Referer: 'https://app.c.nf.migu.cn/', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36', + channel: '0146921', + }, + }) + return this.requestBoardsObj.promise + }, + getData(url) { + const requestObj = httpFetch(url) + return requestObj.promise + }, + // filterBoardsData(listData, list = [], ids = new Set()) { + // for (const item of listData) { + // if (item.rankId && !ids.has(item.rankId)) { + // ids.add(item.rankId) + // list.push({ + // id: 'mg__' + item.rankId, + // name: item.rankName, + // bangid: String(item.rankId), + // source: 'mg', + // }) + // } else if (item.contents) this.filterBoardsData(item.contents, list, ids) + // } + // return list + // }, + // filterBoardsData(rawList) { + // // console.log(rawList) + // let list = [] + // for (const board of rawList) { + // if (board.template != 'group1') continue + // for (const item of board.itemList) { + // if ((item.template != 'row1' && item.template != 'grid1' && !item.actionUrl) || !item.actionUrl.includes('rank-info')) continue + + // let data = item.displayLogId.param + // list.push({ + // id: 'mg__' + data.rankId, + // name: data.rankName, + // bangid: String(data.rankId), + // }) + // } + // } + // return list + // }, + async getBoards(retryNum = 0) { + // if (++retryNum > 3) return Promise.reject(new Error('try max num')) + // let response + // try { + // response = await this.getBoardsData() + // } catch (error) { + // return this.getBoards(retryNum) + // } + // // console.log(response.body.data.contentItemList) + // if (response.statusCode !== 200 || response.body.code !== this.successCode) return this.getBoards(retryNum) + // const list = this.filterBoardsData(response.body.data.contents) + // console.log(list) + // // console.log(JSON.stringify(list)) + // this.list = list + // return { + // list, + // source: 'mg', + // } + this.list = boardList + return { + list: boardList, + source: 'mg', + } + }, + getList(bangid, page, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + return this.getData(this.getUrl(bangid, page)).then(({ statusCode, body }) => { + // console.log(body) + if (statusCode !== 200 || body.code !== this.successCode) return this.getList(bangid, page, retryNum) + const list = filterMusicInfoList(body.columnInfo.contents.map(m => m.objectInfo)) + return { + total: list.length, + list, + limit: this.limit, + page, + source: 'mg', + } + }) + }, + + getDetailPageUrl(id) { + if (typeof id == 'string') id = id.replace('mg__', '') + for (const item of boardList) { + if (item.bangid == id) { + return `https://music.migu.cn/v3/music/top/${item.webId}` + } + } + return null + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/lyric.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/lyric.js new file mode 100644 index 000000000..276397063 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/lyric.js @@ -0,0 +1,103 @@ +import { httpFetch } from '../../request' +import { getMusicInfo } from './musicInfo' +import { decrypt } from './utils/mrc' + +const mrcTools = { + rxps: { + lineTime: /^\s*\[(\d+),\d+\]/, + wordTime: /\(\d+,\d+\)/, + wordTimeAll: /(\(\d+,\d+\))/g, + }, + parseLyric(str) { + str = str.replace(/\r/g, '') + const lines = str.split('\n') + const lxlrcLines = [] + const lrcLines = [] + + for (const line of lines) { + if (line.length < 6) continue + let result = this.rxps.lineTime.exec(line) + if (!result) continue + + const startTime = parseInt(result[1]) + let time = startTime + let ms = (time % 1000).toString().padStart(3, '0') + time /= 1000 + let m = parseInt(time / 60).toString().padStart(2, '0') + time %= 60 + let s = parseInt(time).toString().padStart(2, '0') + time = `${m}:${s}.${ms}` + + let words = line.replace(this.rxps.lineTime, '') + + lrcLines.push(`[${time}]${words.replace(this.rxps.wordTimeAll, '')}`) + + let times = words.match(this.rxps.wordTimeAll) + if (!times) continue + times = times.map(time => { + const result = /\((\d+),(\d+)\)/.exec(time) + return `<${Math.trunc(parseInt(result[1]) - startTime)},${result[2]}>` + }) + const wordArr = words.split(this.rxps.wordTime) + const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('') + lxlrcLines.push(`[${time}]${newWords}`) + } + return { + lyric: lrcLines.join('\n'), + lxlyric: lxlrcLines.join('\n'), + } + }, + getText(url, tryNum = 0) { + const requestObj = httpFetch(url, { + headers: { + Referer: 'https://app.c.nf.migu.cn/', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36', + channel: '0146921', + }, + }) + return requestObj.promise.then(({ statusCode, body }) => { + if (statusCode == 200) return body + if (tryNum > 5 || statusCode == 404) return Promise.reject(new Error('歌词获取失败')) + return this.getText(url, ++tryNum) + }) + }, + getMrc(url) { + return this.getText(url).then(text => { + return this.parseLyric(decrypt(text)) + }) + }, + getLrc(url) { + return this.getText(url).then(text => ({ lxlyric: '', lyric: text })) + }, + getTrc(url) { + if (!url) return Promise.resolve('') + return this.getText(url) + }, + async getMusicInfo(songInfo) { + return songInfo.mrcUrl == null + ? getMusicInfo(songInfo.songmid) + : songInfo + }, + getLyric(songInfo) { + return { + promise: this.getMusicInfo(songInfo).then(info => { + let p + if (info.mrcUrl) p = this.getMrc(info.mrcUrl) + else if (info.lrcUrl) p = this.getLrc(info.lrcUrl) + if (p == null) return Promise.reject(new Error('获取歌词失败')) + return Promise.all([p, this.getTrc(info.trcUrl)]).then(([lrcInfo, tlyric]) => { + lrcInfo.tlyric = tlyric + return lrcInfo + }) + }), + cancelHttp() {}, + } + }, +} + +export default { + getLyric(songInfo) { + let requestObj = mrcTools.getLyric(songInfo) + return requestObj + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/musicInfo.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/musicInfo.js new file mode 100644 index 000000000..4c2d9284d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/musicInfo.js @@ -0,0 +1,162 @@ +import { sizeFormate, formatPlayTime } from '../../index' +import { createHttpFetch } from './utils' +import { formatSingerName } from '../utils' + +const createGetMusicInfosTask = (ids) => { + let list = ids + let tasks = [] + while (list.length) { + tasks.push(list.slice(0, 100)) + if (list.length < 100) break + list = list.slice(100) + } + let url = 'https://c.musicapp.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceType=2' + return Promise.all(tasks.map(task => createHttpFetch(url, { + method: 'POST', + form: { + resourceId: task.join('|'), + }, + }).then(data => data.resource))) +} + +export const filterMusicInfoList = (rawList) => { + // console.log(rawList) + let ids = new Set() + const list = [] + rawList.forEach(item => { + if (!item.songId || ids.has(item.songId)) return + ids.add(item.songId) + const types = [] + const _types = {} + item.newRateFormats?.forEach(type => { + let size + switch (type.formatType) { + case 'PQ': + size = sizeFormate(type.size ?? type.androidSize) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + break + case 'HQ': + size = sizeFormate(type.size ?? type.androidSize) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + break + case 'SQ': + size = sizeFormate(type.size ?? type.androidSize) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + break + case 'ZQ': + size = sizeFormate(type.size ?? type.androidSize) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + break + } + }) + + const intervalTest = /(\d\d:\d\d)$/.test(item.length) + + list.push({ + singer: formatSingerName(item.artists, 'name'), + name: item.songName, + albumName: item.album, + albumId: item.albumId, + songmid: item.songId, + copyrightId: item.copyrightId, + source: 'mg', + interval: intervalTest ? RegExp.$1 : null, + img: item.albumImgs?.length ? item.albumImgs[0].img : null, + lrc: null, + lrcUrl: item.lrcUrl, + mrcUrl: item.mrcUrl, + trcUrl: item.trcUrl, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + }) + return list +} + +export const filterMusicInfoListV5 = (rawList) => { + // console.log(rawList) + let ids = new Set() + const list = [] + rawList.forEach(item => { + if (!item.songId || ids.has(item.songId)) return + ids.add(item.songId) + const types = [] + const _types = {} + item.audioFormats?.forEach(type => { + let size + switch (type.formatType) { + case 'PQ': + size = sizeFormate(type.size ?? type.androidSize) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + break + case 'HQ': + size = sizeFormate(type.size ?? type.androidSize) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + break + case 'SQ': + size = sizeFormate(type.size ?? type.androidSize) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + break + case 'ZQ': + size = sizeFormate(type.size ?? type.androidSize) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + break + } + }) + + list.push({ + singer: formatSingerName(item.singerList, 'name'), + name: item.songName, + albumName: item.album, + albumId: item.albumId, + songmid: item.songId, + copyrightId: item.copyrightId, + source: 'mg', + interval: formatPlayTime(item.duration), + img: item.img3 || item.img2 || item.img1 || null, + lrc: null, + lrcUrl: item.lrcUrl, + mrcUrl: item.mrcUrl, + trcUrl: item.trcUrl, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + }) + return list +} + +export const getMusicInfo = async(copyrightId) => { + return getMusicInfos([copyrightId]).then(data => data[0]) +} + +export const getMusicInfos = async(copyrightIds) => { + return filterMusicInfoList(await Promise.all(createGetMusicInfosTask(copyrightIds)).then(data => data.flat())) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/musicSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/musicSearch.js new file mode 100644 index 000000000..a4247fcb1 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/musicSearch.js @@ -0,0 +1,223 @@ +import { httpFetch } from '../../request' +import { sizeFormate, formatPlayTime } from '../../index' +import { toMD5, formatSingerName } from '../utils' + +export const createSignature = (time, str) => { + const deviceId = '963B7AA0D21511ED807EE5846EC87D20' + const signatureMd5 = '6cdc72a439cef99a3418d2a78aa28c73' + const sign = toMD5(`${str}${signatureMd5}yyapp2d16148780a1dcc7408e06336b98cfd50${deviceId}${time}`) + return { sign, deviceId } +} + +export default { + limit: 20, + total: 0, + page: 0, + allPage: 1, + + // 旧版API + // musicSearch(str, page, limit) { + // const searchRequest = httpFetch(`http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=${encodeURIComponent(str)}&pageNo=${page}&pageSize=${limit}&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22songlist%22%3A0%2C%22bestShow%22%3A1%7D`, { + // searchRequest = httpFetch(`http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=${encodeURIComponent(str)}&pageNo=${page}&pageSize=${limit}&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22songlist%22%3A0%2C%22bestShow%22%3A1%7D`, { + // searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, { + // searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`) + // // searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, { + // headers: { + // // sign: 'c3b7ae985e2206e97f1b2de8f88691e2', + // // timestamp: 1578225871982, + // // appId: 'yyapp2', + // // mode: 'android', + // // ua: 'Android_migu', + // // version: '6.9.4', + // osVersion: 'android 7.0', + // 'User-Agent': 'okhttp/3.9.1', + // }, + // }) + // // searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`) + // return searchRequest.promise.then(({ body }) => body) + // }, + // handleResult(rawData) { + // // console.log(rawData) + // let ids = new Set() + // const list = [] + // rawData.forEach(item => { + // if (ids.has(item.id)) return + // ids.add(item.id) + // const types = [] + // const _types = {} + // item.newRateFormats && item.newRateFormats.forEach(type => { + // let size + // switch (type.formatType) { + // case 'PQ': + // size = sizeFormate(type.size ?? type.androidSize) + // types.push({ type: '128k', size }) + // _types['128k'] = { + // size, + // } + // break + // case 'HQ': + // size = sizeFormate(type.size ?? type.androidSize) + // types.push({ type: '320k', size }) + // _types['320k'] = { + // size, + // } + // break + // case 'SQ': + // size = sizeFormate(type.size ?? type.androidSize) + // types.push({ type: 'flac', size }) + // _types.flac = { + // size, + // } + // break + // case 'ZQ': + // size = sizeFormate(type.size ?? type.androidSize) + // types.push({ type: 'flac24bit', size }) + // _types.flac24bit = { + // size, + // } + // break + // } + // }) + + // const albumNInfo = item.albums && item.albums.length + // ? { + // id: item.albums[0].id, + // name: item.albums[0].name, + // } + // : {} + + // list.push({ + // singer: this.getSinger(item.singers), + // name: item.name, + // albumName: albumNInfo.name, + // albumId: albumNInfo.id, + // songmid: item.songId, + // copyrightId: item.copyrightId, + // source: 'mg', + // interval: null, + // img: item.imgItems && item.imgItems.length ? item.imgItems[0].img : null, + // lrc: null, + // lrcUrl: item.lyricUrl, + // mrcUrl: item.mrcurl, + // trcUrl: item.trcUrl, + // otherSource: null, + // types, + // _types, + // typeUrl: {}, + // }) + // }) + // return list + // }, + + musicSearch(str, page, limit) { + const time = Date.now().toString() + const signData = createSignature(time, str) + const searchRequest = httpFetch(`https://jadeite.migu.cn/music_search/v3/search/searchAll?isCorrect=0&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0&sid=USS`, { + headers: { + uiVersion: 'A_music_3.6.1', + deviceId: signData.deviceId, + timestamp: time, + sign: signData.sign, + channel: '0146921', + 'User-Agent': 'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + }, + }) + return searchRequest.promise.then(({ body }) => body) + }, + filterData(rawData) { + // console.log(rawData) + const list = [] + const ids = new Set() + + rawData.forEach(item => { + item.forEach(data => { + if (!data.songId || !data.copyrightId || ids.has(data.copyrightId)) return + ids.add(data.copyrightId) + + const types = [] + const _types = {} + data.audioFormats && data.audioFormats.forEach(type => { + let size + switch (type.formatType) { + case 'PQ': + size = sizeFormate(type.asize ?? type.isize) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + break + case 'HQ': + size = sizeFormate(type.asize ?? type.isize) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + break + case 'SQ': + size = sizeFormate(type.asize ?? type.isize) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + break + case 'ZQ24': + size = sizeFormate(type.asize ?? type.isize) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + break + } + }) + + let img = data.img3 || data.img2 || data.img1 || null + if (img && !/https?:/.test(data.img3)) img = 'http://d.musicapp.migu.cn' + img + + list.push({ + singer: formatSingerName(data.singerList), + name: data.name, + albumName: data.album, + albumId: data.albumId, + songmid: data.songId, + copyrightId: data.copyrightId, + source: 'mg', + interval: formatPlayTime(data.duration), + img, + lrc: null, + lrcUrl: data.lrcUrl, + mrcUrl: data.mrcurl, + trcUrl: data.trcUrl, + types, + _types, + typeUrl: {}, + }) + }) + }) + return list + }, + search(str, page = 1, limit, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + if (limit == null) limit = this.limit + // http://newlyric.kuwo.cn/newlyric.lrc?62355680 + return this.musicSearch(str, page, limit).then(result => { + // console.log(result) + if (!result || result.code !== '000000') return Promise.reject(new Error(result ? result.info : '搜索失败')) + const songResultData = result.songResultData || { resultList: [], totalCount: 0 } + + let list = this.filterData(songResultData.resultList) + if (list == null) return this.search(str, page, limit, retryNum) + + this.total = parseInt(songResultData.totalCount) + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return { + list, + allPage: this.allPage, + limit, + total: this.total, + source: 'mg', + } + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/pic.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/pic.js new file mode 100644 index 000000000..b47217d38 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/pic.js @@ -0,0 +1,8 @@ +import { getMusicInfo } from './musicInfo' + +export default { + async getPic(songInfo) { + const info = await getMusicInfo(songInfo.songmid) + return info.img + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/songId.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/songId.js new file mode 100644 index 000000000..747d50586 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/songId.js @@ -0,0 +1,31 @@ +// import { httpFetch } from '../../request' +import { getMusicInfo } from './musicInfo' + +const getSongId = async(mInfo) => { + if (mInfo.songmid != mInfo.copyrightId) return mInfo.songmid + const musicInfo = await getMusicInfo(mInfo.songmid) + return musicInfo.songmid +} + + +// export const getSongId = async(musicInfo, retry = 0) => { +// if (musicInfo.songmid != musicInfo.copyrightId) return musicInfo.songmid +// if (++retry > 2) return Promise.reject(new Error('max retry')) + +// const requestObj = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/listen-url?netType=00&resourceType=2&songId=${musicInfo.copyrightId}&toneFlag=PQ`, { +// 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', +// channel: '0146921', +// }, +// }) + +// return requestObj.promise.then(({ body }) => { +// console.log(body) +// if (!body || body.code !== '000000') return this.getSongId(musicInfo, retry) +// const id = body.data.songItem.songId +// if (!id) throw new Error('failed') +// return id +// }) +// } + +export default getSongId diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/songList.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/songList.js new file mode 100644 index 000000000..67928171e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/songList.js @@ -0,0 +1,362 @@ +import { httpFetch } from '../../request' +import { formatPlayCount } from '../../index' +import { filterMusicInfoListV5 } from './musicInfo' +import { createSignature } from './musicSearch' +import { createHttpFetch } from './utils/index' + +// const tagData = { code: '000000', info: 'SUCCESS', columnInfo: { columnTitle: '分类', columnId: '15244430', columnPid: '15031270', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 6, columnStatus: 1, columnCreateTime: '2016-11-10 10:53:05.077', columntype: 2011, contents: [{ contentId: '18464615', relationType: 2011, objectInfo: { columnTitle: '热门', columnId: '18464615', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 8, columnStatus: 1, columnCreateTime: '2017-02-20 16:09:13.400', columntype: 2011, contents: [{ contentId: '1000001672', relationType: 4034, objectInfo: { tagId: '1000001672', tagName: '流行', resourceType: '2034' }, relationSort: 9 }, { contentId: '1003449727', relationType: 4034, objectInfo: { tagId: '1003449727', tagName: '厂牌', resourceType: '2034' }, relationSort: 8 }, { contentId: '1000001795', relationType: 4034, objectInfo: { tagId: '1000001795', tagName: '伤感', resourceType: '2034' }, relationSort: 7 }, { contentId: '1001076080', relationType: 4034, objectInfo: { tagId: '1001076080', tagName: '电影', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001675', relationType: 4034, objectInfo: { tagId: '1000001675', tagName: '中国风', resourceType: '2034' }, relationSort: 5 }, { contentId: '1000001635', relationType: 4034, objectInfo: { tagId: '1000001635', tagName: '经典老歌', resourceType: '2034' }, relationSort: 4 }, { contentId: '1000001831', relationType: 4034, objectInfo: { tagId: '1000001831', tagName: '翻唱', resourceType: '2034' }, relationSort: 3 }, { contentId: '1000001762', relationType: 4034, objectInfo: { tagId: '1000001762', tagName: '国语', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620410266029', customizedPicUrls: [] }, relationSort: 6 }, { contentId: '15244503', relationType: 2011, objectInfo: { columnTitle: '主题', columnId: '15244503', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 23, columnStatus: 1, columnCreateTime: '2016-11-10 10:54:10.261', columntype: 2011, contents: [{ contentId: '1003449727', relationType: 4034, objectInfo: { tagId: '1003449727', tagName: '厂牌', resourceType: '2034' }, relationSort: 29 }, { contentId: '1001076080', relationType: 4034, objectInfo: { tagId: '1001076080', tagName: '电影', resourceType: '2034' }, relationSort: 28 }, { contentId: '1001076078', relationType: 4034, objectInfo: { tagId: '1001076078', tagName: '电视剧', resourceType: '2034' }, relationSort: 27 }, { contentId: '1001076083', relationType: 4034, objectInfo: { tagId: '1001076083', tagName: '综艺', resourceType: '2034' }, relationSort: 26 }, { contentId: '1000001827', relationType: 4034, objectInfo: { tagId: '1000001827', tagName: 'KTV', resourceType: '2034' }, relationSort: 23 }, { contentId: '1000001698', relationType: 4034, objectInfo: { tagId: '1000001698', tagName: '爱情', resourceType: '2034' }, relationSort: 22 }, { contentId: '1000001635', relationType: 4034, objectInfo: { tagId: '1000001635', tagName: '经典老歌', resourceType: '2034' }, relationSort: 21 }, { contentId: '1001076096', relationType: 4034, objectInfo: { tagId: '1001076096', tagName: '网络热歌', resourceType: '2034' }, relationSort: 20 }, { contentId: '1000001780', relationType: 4034, objectInfo: { tagId: '1000001780', tagName: '儿童歌曲', resourceType: '2034' }, relationSort: 19 }, { contentId: '1000587702', relationType: 4034, objectInfo: { tagId: '1000587702', tagName: '广场舞', resourceType: '2034' }, relationSort: 18 }, { contentId: '1000587717', relationType: 4034, objectInfo: { tagId: '1000587717', tagName: '70后', resourceType: '2034' }, relationSort: 17 }, { contentId: '1000587718', relationType: 4034, objectInfo: { tagId: '1000587718', tagName: '80后', resourceType: '2034' }, relationSort: 16 }, { contentId: '1000587726', relationType: 4034, objectInfo: { tagId: '1000587726', tagName: '90后', resourceType: '2034' }, relationSort: 15 }, { contentId: '1000001670', relationType: 4034, objectInfo: { tagId: '1000001670', tagName: '红歌', resourceType: '2034' }, relationSort: 14 }, { contentId: '1000587698', relationType: 4034, objectInfo: { tagId: '1000587698', tagName: '游戏', resourceType: '2034' }, relationSort: 13 }, { contentId: '1000587706', relationType: 4034, objectInfo: { tagId: '1000587706', tagName: '动漫', resourceType: '2034' }, relationSort: 12 }, { contentId: '1000001675', relationType: 4034, objectInfo: { tagId: '1000001675', tagName: '中国风', resourceType: '2034' }, relationSort: 11 }, { contentId: '1000587712', relationType: 4034, objectInfo: { tagId: '1000587712', tagName: '青春校园', resourceType: '2034' }, relationSort: 10 }, { contentId: '1000587673', relationType: 4034, objectInfo: { tagId: '1000587673', tagName: '小清新', resourceType: '2034' }, relationSort: 9 }, { contentId: '1000093902', relationType: 4034, objectInfo: { tagId: '1000093902', tagName: 'DJ舞曲', resourceType: '2034' }, relationSort: 7 }, { contentId: '1000093963', relationType: 4034, objectInfo: { tagId: '1000093963', tagName: '广告', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001831', relationType: 4034, objectInfo: { tagId: '1000001831', tagName: '翻唱', resourceType: '2034' }, relationSort: 2 }, { contentId: '1003449726', relationType: 4034, objectInfo: { tagId: '1003449726', tagName: '读书', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620410266055', customizedPicUrls: [] }, relationSort: 5 }, { contentId: '15244509', relationType: 2011, objectInfo: { columnTitle: '风格', columnId: '15244509', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 12, columnStatus: 1, columnCreateTime: '2016-11-10 10:54:57.257', columntype: 2011, contents: [{ contentId: '1000001672', relationType: 4034, objectInfo: { tagId: '1000001672', tagName: '流行', resourceType: '2034' }, relationSort: 14 }, { contentId: '1000001808', relationType: 4034, objectInfo: { tagId: '1000001808', tagName: 'R&B', resourceType: '2034' }, relationSort: 13 }, { contentId: '1000001809', relationType: 4034, objectInfo: { tagId: '1000001809', tagName: '嘻哈', resourceType: '2034' }, relationSort: 12 }, { contentId: '1000001674', relationType: 4034, objectInfo: { tagId: '1000001674', tagName: '摇滚', resourceType: '2034' }, relationSort: 11 }, { contentId: '1000001682', relationType: 4034, objectInfo: { tagId: '1000001682', tagName: '电子', resourceType: '2034' }, relationSort: 10 }, { contentId: '1000001852', relationType: 4034, objectInfo: { tagId: '1000001852', tagName: '电子舞曲', resourceType: '2034' }, relationSort: 9 }, { contentId: '1000001681', relationType: 4034, objectInfo: { tagId: '1000001681', tagName: '爵士', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001683', relationType: 4034, objectInfo: { tagId: '1000001683', tagName: '乡村', resourceType: '2034' }, relationSort: 5 }, { contentId: '1000001851', relationType: 4034, objectInfo: { tagId: '1000001851', tagName: '蓝调', resourceType: '2034' }, relationSort: 4 }, { contentId: '1000001775', relationType: 4034, objectInfo: { tagId: '1000001775', tagName: '民谣', resourceType: '2034' }, relationSort: 3 }, { contentId: '1000001807', relationType: 4034, objectInfo: { tagId: '1000001807', tagName: '纯音乐', resourceType: '2034' }, relationSort: 2 }, { contentId: '1000001783', relationType: 4034, objectInfo: { tagId: '1000001783', tagName: '古典', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620410266033', customizedPicUrls: [] }, relationSort: 4 }, { contentId: '18464665', relationType: 2011, objectInfo: { columnTitle: '语种', columnId: '18464665', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 6, columnStatus: 1, columnCreateTime: '2017-02-20 16:07:16.566', columntype: 2011, contents: [{ contentId: '1000001762', relationType: 4034, objectInfo: { tagId: '1000001762', tagName: '国语', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001763', relationType: 4034, objectInfo: { tagId: '1000001763', tagName: '粤语', resourceType: '2034' }, relationSort: 5 }, { contentId: '1000001766', relationType: 4034, objectInfo: { tagId: '1000001766', tagName: '英语', resourceType: '2034' }, relationSort: 4 }, { contentId: '1000001599', relationType: 4034, objectInfo: { tagId: '1000001599', tagName: '韩语', resourceType: '2034' }, relationSort: 3 }, { contentId: '1000001767', relationType: 4034, objectInfo: { tagId: '1000001767', tagName: '日语', resourceType: '2034' }, relationSort: 2 }, { contentId: '1003449724', relationType: 4034, objectInfo: { tagId: '1003449724', tagName: '小语种', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620410266036', customizedPicUrls: [] }, relationSort: 3 }, { contentId: '18464583', relationType: 2011, objectInfo: { columnTitle: '心情', columnId: '18464583', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 13, columnStatus: 1, columnCreateTime: '2017-02-20 15:59:03.412', columntype: 2011, contents: [{ contentId: '1000587677', relationType: 4034, objectInfo: { tagId: '1000587677', tagName: '幸福', resourceType: '2034' }, relationSort: 15 }, { contentId: '1000587710', relationType: 4034, objectInfo: { tagId: '1000587710', tagName: '治愈', resourceType: '2034' }, relationSort: 14 }, { contentId: '1000001703', relationType: 4034, objectInfo: { tagId: '1000001703', tagName: '思念', resourceType: '2034' }, relationSort: 13 }, { contentId: '1000587667', relationType: 4034, objectInfo: { tagId: '1000587667', tagName: '期待', resourceType: '2034' }, relationSort: 12 }, { contentId: '1000001700', relationType: 4034, objectInfo: { tagId: '1000001700', tagName: '励志', resourceType: '2034' }, relationSort: 11 }, { contentId: '1000001694', relationType: 4034, objectInfo: { tagId: '1000001694', tagName: '欢快', resourceType: '2034' }, relationSort: 10 }, { contentId: '1002600588', relationType: 4034, objectInfo: { tagId: '1002600588', tagName: '叛逆', resourceType: '2034' }, relationSort: 9 }, { contentId: '1002600585', relationType: 4034, objectInfo: { tagId: '1002600585', tagName: '宣泄', resourceType: '2034' }, relationSort: 8 }, { contentId: '1000001696', relationType: 4034, objectInfo: { tagId: '1000001696', tagName: '怀旧', resourceType: '2034' }, relationSort: 7 }, { contentId: '1000587679', relationType: 4034, objectInfo: { tagId: '1000587679', tagName: '减压', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001699', relationType: 4034, objectInfo: { tagId: '1000001699', tagName: '寂寞', resourceType: '2034' }, relationSort: 5 }, { contentId: '1002600579', relationType: 4034, objectInfo: { tagId: '1002600579', tagName: '忧郁', resourceType: '2034' }, relationSort: 4 }, { contentId: '1000001795', relationType: 4034, objectInfo: { tagId: '1000001795', tagName: '伤感', resourceType: '2034' }, relationSort: 3 }], dataVersion: '1620410266187', customizedPicUrls: [] }, relationSort: 2 }, { contentId: '18464638', relationType: 2011, objectInfo: { columnTitle: '场景', columnId: '18464638', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 13, columnStatus: 1, columnCreateTime: '2017-02-20 16:02:59.711', columntype: 2011, contents: [{ contentId: '1000587689', relationType: 4034, objectInfo: { tagId: '1000587689', tagName: '清晨', resourceType: '2034' }, relationSort: 21 }, { contentId: '1000587690', relationType: 4034, objectInfo: { tagId: '1000587690', tagName: '夜晚', resourceType: '2034' }, relationSort: 20 }, { contentId: '1000587688', relationType: 4034, objectInfo: { tagId: '1000587688', tagName: '睡前安眠', resourceType: '2034' }, relationSort: 19 }, { contentId: '1003449726', relationType: 4034, objectInfo: { tagId: '1003449726', tagName: '读书', resourceType: '2034' }, relationSort: 18 }, { contentId: '1003449723', relationType: 4034, objectInfo: { tagId: '1003449723', tagName: '下午·茶', resourceType: '2034' }, relationSort: 16 }, { contentId: '1000093923', relationType: 4034, objectInfo: { tagId: '1000093923', tagName: '驾车', resourceType: '2034' }, relationSort: 15 }, { contentId: '1003449615', relationType: 4034, objectInfo: { tagId: '1003449615', tagName: '运动', resourceType: '2034' }, relationSort: 13 }, { contentId: '1000587694', relationType: 4034, objectInfo: { tagId: '1000587694', tagName: '散步', resourceType: '2034' }, relationSort: 12 }, { contentId: '1000001749', relationType: 4034, objectInfo: { tagId: '1000001749', tagName: '旅行', resourceType: '2034' }, relationSort: 11 }, { contentId: '1000587686', relationType: 4034, objectInfo: { tagId: '1000587686', tagName: '夜店', resourceType: '2034' }, relationSort: 10 }, { contentId: '1002600606', relationType: 4034, objectInfo: { tagId: '1002600606', tagName: '派对', resourceType: '2034' }, relationSort: 9 }, { contentId: '1000001634', relationType: 4034, objectInfo: { tagId: '1000001634', tagName: '咖啡馆', resourceType: '2034' }, relationSort: 3 }, { contentId: '1000587692', relationType: 4034, objectInfo: { tagId: '1000587692', tagName: '瑜伽', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620846028994', customizedPicUrls: [] }, relationSort: 1 }], dataVersion: '1620846028941', customizedPicUrls: [] } } + +export default { + _requestObj_tags: null, + _requestObj_list: null, + limit_list: 30, + limit_song: 50, + successCode: '000000', + cachedDetailInfo: {}, + cachedUrl: {}, + sortList: [ + { + name: '推荐', + id: '15127315', + // id: '1', + }, + // { + // name: '最新', + // id: '15127272', + // // id: '2', + // }, + ], + regExps: { + list: /
  • .+?<\/li>/g, + listInfo: /.+data-original="(.+?)".*data-id="(\d+)".*
    (.+?)<\/a>.+<\/i>(.+?)<\/div>/, + + // https://music.migu.cn/v3/music/playlist/161044573?page=1 + listDetailLink: /^.+\/playlist\/(\d+)(?:\?.*|&.*$|#.*$|$)/, + }, + tagsUrl: 'https://app.c.nf.migu.cn/pc/v1.0/template/musiclistplaza-taglist/release', + // tagsUrl: 'https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/indexTagPage.do?needAll=0', + getSongListUrl(sortId, tagId, page) { + // if (tagId == null) { + // return sortId == 'recommend' + // ? `https://music.migu.cn/v3/music/playlist?page=${page}&from=migu` + // : `https://music.migu.cn/v3/music/playlist?sort=${sortId}&page=${page}&from=migu` + // } + // return `https://music.migu.cn/v3/music/playlist?tagId=${tagId}&page=${page}&from=migu` + if (!tagId) { + // return `https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?count=${this.limit_list}&start=${page}&templateVersion=5&type=1` + // return `https://c.musicapp.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?count=${this.limit_list}&start=${page}&templateVersion=5&type=${sortId}` + // https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?count=50&start=2&templateVersion=5&type=1 + // return `https://m.music.migu.cn/migu/remoting/playlist_bycolumnid_tag?playListType=2&type=1&columnId=${sortId}&startIndex=${(page - 1) * 10}` + return `https://app.c.nf.migu.cn/pc/bmw/page-data/playlist-square-recommend/v1.0?templateVersion=2&pageNo=${page}` + } + // return `https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?area=2&count=${this.limit_list}&start=${page}&tags=${tagId}&templateVersion=5&type=3` + return `https://app.c.nf.migu.cn/pc/v1.0/template/musiclistplaza-listbytag/release?pageNumber=${page}&templateVersion=2&tagId=${tagId}` + // return `https://m.music.migu.cn/migu/remoting/playlist_bycolumnid_tag?playListType=2&type=1&tagId=${tagId}&startIndex=${(page - 1) * 10}` + }, + getSongListDetailUrl(id, page) { + return `https://app.c.nf.migu.cn/MIGUM3.0/resource/playlist/song/v2.0?pageNo=${page}&pageSize=${this.limit_song}&playlistId=${id}` + // return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/user/queryMusicListSongs.do?musicListId=${id}&pageNo=${page}&pageSize=${this.limit_song}` + }, + defaultHeaders: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', + Referer: 'https://m.music.migu.cn/', + // language: 'Chinese', + // ua: 'Android_migu', + // mode: 'android', + // version: '6.8.5', + }, + + getListDetailList(id, page, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + // https://h5.nf.migu.cn/app/v4/p/share/playlist/index.html?id=184187437&channel=0146921 + + // if (/playlist\/index\.html\?/.test(id)) { + // id = id.replace(/.*(?:\?|&)id=(\d+)(?:&.*|$)/, '$1') + // } else if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + + const requestObj_listDetail = httpFetch(this.getSongListDetailUrl(id, page), { headers: this.defaultHeaders }) + return requestObj_listDetail.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getListDetailList(id, page, ++tryNum) + // console.log(JSON.stringify(body)) + // console.log(body) + return { + list: filterMusicInfoListV5(body.data.songList), + page, + limit: this.limit_song, + total: body.data.totalCount, + source: 'mg', + } + }) + }, + + getListDetailInfo(id, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + + if (this.cachedDetailInfo[id]) return Promise.resolve(this.cachedDetailInfo[id]) + const requestObj_listDetailInfo = httpFetch(`https://c.musicapp.migu.cn/MIGUM3.0/resource/playlist/v2.0?playlistId=${id}`, { + headers: this.defaultHeaders, + }) + return requestObj_listDetailInfo.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum) + // console.log(JSON.stringify(body)) + // console.log(body) + const cachedDetailInfo = this.cachedDetailInfo[id] = { + name: body.data.title, + img: body.data.imgItem.img, + desc: body.data.summary, + author: body.data.ownerName, + play_count: formatPlayCount(body.data.opNumItem.playNum), + } + return cachedDetailInfo + }) + }, + + async getDetailUrl(link, page, retryNum = 0) { + if (retryNum > 3) return Promise.reject(new Error('link try max num')) + + const requestObj_listDetailLink = httpFetch(link, { + headers: { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + Referer: link, + }, + }) + const { headers: { location }, statusCode } = await requestObj_listDetailLink.promise + // console.log(body, location) + if (statusCode > 400) return this.getDetailUrl(link, page, ++retryNum) + if (location) { + this.cachedUrl[link] = location + return this.getListDetail(location, page) + } + return Promise.reject(new Error('link get failed')) + }, + + getListDetail(id, page, retryNum = 0) { // 获取歌曲列表内的音乐 + // https://h5.nf.migu.cn/app/v4/p/share/playlist/index.html?id=184187437&channel=0146921 + // http://c.migu.cn/00bTY6?ifrom=babddaadfde4ebeda289d671ab62f236 + // https://music.migu.cn/v5/#/playlist?playlistId=221573417 + if (/\/playlist[/?]/.test(id)) { + id = /(?:playlistId|id)=(\d+)/.exec(id)?.[1] + if (!id) throw new Error('list detail id parse failed') + } else if (this.regExps.listDetailLink.test(id)) { + id = id.replace(this.regExps.listDetailLink, '$1') + } else if ((/[?&:/]/.test(id))) { + const url = this.cachedUrl[id] + return url ? this.getListDetail(url, page) : this.getDetailUrl(id, page) + } + + return Promise.all([ + this.getListDetailList(id, page, retryNum), + this.getListDetailInfo(id, retryNum), + ]).then(([listData, info]) => { + listData.info = info + return listData + }) + }, + + // 获取列表数据 + getList(sortId, tagId, page, tryNum = 0) { + if (this._requestObj_list) this._requestObj_list.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_list = httpFetch(this.getSongListUrl(sortId, tagId, page), { + headers: this.defaultHeaders, + // headers: { + // sign: 'c3b7ae985e2206e97f1b2de8f88691e2', + // timestamp: 1578225871982, + // appId: 'yyapp2', + // mode: 'android', + // ua: 'Android_migu', + // version: '6.9.4', + // osVersion: 'android 7.0', + // 'User-Agent': 'okhttp/3.9.1', + // }, + }) + // return this._requestObj_list.promise.then(({ statusCode, body }) => { + // if (statusCode !== 200) return this.getList(sortId, tagId, page) + // let list = body.replace(/[\r\n]/g, '').match(this.regExps.list) + // if (!list) return Promise.reject(new Error('获取列表失败')) + // return list.map(item => { + // let info = item.match(this.regExps.listInfo) + // return { + // play_count: info[4], + // id: info[2], + // author: '', + // name: info[3], + // time: '', + // img: info[1], + // grade: 0, + // desc: '', + // source: 'mg', + // } + // }) + // }) + // return this._requestObj_list.promise.then(({ body }) => { + // console.log(body) + // if (body.retCode !== '100000' || body.retMsg.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) + // return { + // list: this.filterList(body.retMsg.playlist), + // total: parseInt(body.retMsg.countSize), + // page, + // limit: this.limit_list, + // source: 'mg', + // } + // }) + return this._requestObj_list.promise.then(({ body }) => { + // console.log(body) + // if (body.retCode !== '000000') return this.getList(sortId, tagId, page, ++tryNum) + if (body.code !== '000000') return this.getList(sortId, tagId, page, ++tryNum) + const list = body.data.contents ? this.filterList2(body.data.contents) : this.filterList(body.data.contentItemList[1].itemList) + return { + list, + total: 99999, + page, + limit: this.limit_list, + source: 'mg', + } + }) + }, + filterList2(listData, list = [], ids = new Set()) { + for (const item of listData) { + if (item.contents) this.filterList2(item.contents, list, ids) + else if (item.resType == '2021' && !ids.has(item.resId)) { + ids.add(item.resId) + list.push({ + id: String(item.resId), + author: '', + name: item.txt, + // time: dateFormat(item.createTime, 'Y-M-D'), + img: item.img, + // grade: item.grade, + // total: item.contentCount, + desc: item.txt2, + source: 'mg', + }) + } + } + return list + }, + filterList(rawData) { + // console.log(rawData) + return rawData.map(item => ({ + play_count: item.barList[0]?.title, + id: String(item.logEvent.contentId), + author: '', + name: item.title, + // time: dateFormat(item.createTime, 'Y-M-D'), + img: item.imageUrl, + // grade: item.grade, + // total: item.contentCount, + desc: '', + source: 'mg', + })) + }, + + // 获取标签 + getTag(tryNum = 0) { + if (this._requestObj_tags) this._requestObj_tags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_tags = httpFetch(this.tagsUrl, { headers: this.defaultHeaders }) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getTag(++tryNum) + // console.log(body) + return this.filterTagInfo(body.data) + }) + // return Promise.resolve(this.filterTagInfo(tagData.columnInfo.contents)) + }, + filterTagInfo(rawList) { + return { + hotTag: rawList[0].content.map(({ texts: [name, id] }) => ({ + id, + name, + source: 'mg', + })), + tags: rawList.slice(1).map(({ header, content }) => ({ + name: header.title, + list: content.map(({ texts: [name, id] }) => ({ + // parent_id: objectInfo.columnId, + // parent_name: objectInfo.columnTitle, + id, + name, + source: 'mg', + })), + })), + source: 'mg', + } + // return { + // hotTag: rawList[0].objectInfo.contents.map(item => ({ + // id: item.objectInfo.tagId, + // name: item.objectInfo.tagName, + // source: 'mg', + // })), + // tags: rawList.slice(1).map(({ objectInfo }) => ({ + // name: objectInfo.columnTitle, + // list: objectInfo.contents.map(item => ({ + // parent_id: objectInfo.columnId, + // parent_name: objectInfo.columnTitle, + // id: item.objectInfo.tagId, + // name: item.objectInfo.tagName, + // source: 'mg', + // })), + // })), + // source: 'mg', + // } + }, + getTags() { + return this.getTag() + }, + + getDetailPageUrl(id) { + if (/playlist\/index\.html\?/.test(id)) { + id = id.replace(/.*(?:\?|&)id=(\d+)(?:&.*|$)/, '$1') + } else if (this.regExps.listDetailLink.test(id)) { + id = id.replace(this.regExps.listDetailLink, '$1') + } + return `https://music.migu.cn/v3/music/playlist/${id}` + }, + + filterSongListResult(raw) { + const list = [] + raw.forEach(item => { + if (!item.id) return + + const playCount = parseInt(item.playNum) + list.push({ + play_count: isNaN(playCount) ? 0 : formatPlayCount(playCount), + id: item.id, + author: item.userName, + name: item.name, + img: item.musicListPicUrl, + total: item.musicNum, + source: 'mg', + }) + }) + return list + }, + search(text, page, limit = 20) { + const timeStr = Date.now().toString() + const signResult = createSignature(timeStr, text) + return createHttpFetch(`https://jadeite.migu.cn/music_search/v3/search/searchAll?isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A0%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22bestShow%22%3A0%2C%22songlist%22%3A1%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(text)}&pageNo=${page}&sort=0&sid=USS`, { + headers: { + uiVersion: 'A_music_3.6.1', + deviceId: signResult.deviceId, + timestamp: timeStr, + sign: signResult.sign, + channel: '0146921', + 'User-Agent': 'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + }, + }).then(body => { + if (!body.songListResultData) throw new Error('get song list faild.') + + const list = this.filterSongListResult(body.songListResultData.result) + return { + list, + limit, + total: parseInt(body.songListResultData.totalCount), + source: 'mg', + } + }) + }, +} + +// getList +// getTags +// getListDetail diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/temp/leaderboard-old.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/temp/leaderboard-old.js new file mode 100644 index 000000000..674c97988 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/temp/leaderboard-old.js @@ -0,0 +1,195 @@ +import { httpFetch } from '../../../request' +import { formatPlayTime } from '../../../index' +// import { sizeFormate } from '../../index' + + +// const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }] +const boardList = [ + { id: 'mg__27553319', name: '尖叫新歌榜', bangid: '27553319', webId: 'jianjiao_newsong' }, + { id: 'mg__27186466', name: '尖叫热歌榜', bangid: '27186466', webId: 'jianjiao_hotsong' }, + { id: 'mg__27553408', name: '尖叫原创榜', bangid: '27553408', webId: 'jianjiao_original' }, + { id: 'mg__migumusic', name: '音乐榜', bangid: 'migumusic', webId: 'migumusic' }, + { id: 'mg__movies', name: '影视榜', bangid: 'movies', webId: 'movies' }, + { id: 'mg__23189800', name: '港台榜', bangid: '23189800', webId: 'hktw' }, + { id: 'mg__23189399', name: '内地榜', bangid: '23189399', webId: 'mainland' }, + { id: 'mg__19190036', name: '欧美榜', bangid: '19190036', webId: 'eur_usa' }, + { id: 'mg__23189813', name: '日韩榜', bangid: '23189813', webId: 'jpn_kor' }, + { id: 'mg__23190126', name: '彩铃榜', bangid: '23190126', webId: 'coloring' }, + { id: 'mg__15140045', name: 'KTV榜', bangid: '15140045', webId: 'ktv' }, + { id: 'mg__15140034', name: '网络榜', bangid: '15140034', webId: 'network' }, + { id: 'mg__23217754', name: 'MV榜', bangid: '23217754', webId: 'mv' }, + { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151', webId: 'newalbum' }, + { id: 'mg__21958042', name: '美国iTunes榜', bangid: '21958042', webId: 'itunes' }, + { id: 'mg__21975570', name: '美国billboard榜', bangid: '21975570', webId: 'billboard' }, + { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815', webId: 'hito' }, + { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, + { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943', webId: 'mnet' }, + { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437', webId: 'uk' }, +] +// const boardList = [ +// { id: 'mg__jianjiao_newsong', bangid: 'jianjiao_newsong', name: '尖叫新歌榜' }, +// { id: 'mg__jianjiao_hotsong', bangid: 'jianjiao_hotsong', name: '尖叫热歌榜' }, +// { id: 'mg__jianjiao_original', bangid: 'jianjiao_original', name: '尖叫原创榜' }, +// { id: 'mg__migumusic', bangid: 'migumusic', name: '音乐榜' }, +// { id: 'mg__movies', bangid: 'movies', name: '影视榜' }, +// { id: 'mg__mainland', bangid: 'mainland', name: '内地榜' }, +// { id: 'mg__hktw', bangid: 'hktw', name: '港台榜' }, +// { id: 'mg__eur_usa', bangid: 'eur_usa', name: '欧美榜' }, +// { id: 'mg__jpn_kor', bangid: 'jpn_kor', name: '日韩榜' }, +// { id: 'mg__coloring', bangid: 'coloring', name: '彩铃榜' }, +// { id: 'mg__ktv', bangid: 'ktv', name: 'KTV榜' }, +// { id: 'mg__network', bangid: 'network', name: '网络榜' }, +// { id: 'mg__newalbum', bangid: 'newalbum', name: '新专辑榜' }, +// { id: 'mg__mv', bangid: 'mv', name: 'MV榜' }, +// { id: 'mg__itunes', bangid: 'itunes', name: '美国iTunes榜' }, +// { id: 'mg__billboard', bangid: 'billboard', name: '美国billboard榜' }, +// { id: 'mg__hito', bangid: 'hito', name: 'Hito中文榜' }, +// { id: 'mg__mnet', bangid: 'mnet', name: '韩国Melon榜' }, +// { id: 'mg__uk', bangid: 'uk', name: '英国UK榜' }, +// ] + +export default { + limit: 10000, + getUrl(id, page) { + const targetBoard = boardList.find(board => board.bangid == id) + return `https://music.migu.cn/v3/music/top/${targetBoard.webId}` + // return `http://m.music.migu.cn/migu/remoting/cms_list_tag?nid=${id}&pageSize=${this.limit}&pageNo=${page - 1}` + }, + successCode: '000000', + requestBoardsObj: null, + regExps: { + listData: /var listData = (\{.+\})<\/script>/, + }, + getData(url) { + const requestObj = httpFetch(url) + return requestObj.promise + }, + getSinger(singers) { + let arr = [] + singers.forEach(singer => { + arr.push(singer.name) + }) + return arr.join('、') + }, + getIntv(interval) { + if (!interval) return 0 + let intvArr = interval.split(':') + let intv = 0 + let unit = 1 + while (intvArr.length) { + intv += (intvArr.pop()) * unit + unit *= 60 + } + return parseInt(intv) + }, + formateIntv() { + + }, + filterData(rawData) { + // console.log(JSON.stringify(rawData)) + // console.log(rawData) + let ids = new Set() + const list = [] + rawData.forEach(item => { + if (ids.has(item.copyrightId)) return + ids.add(item.copyrightId) + + const types = [] + const _types = {} + + const size = null + types.push({ type: '128k', size }) + _types['128k'] = { size } + + if (item.hq) { + const size = null + types.push({ type: '320k', size }) + _types['320k'] = { size } + } + if (item.sq) { + const size = null + types.push({ type: 'flac', size }) + _types.flac = { size } + } + + list.push({ + singer: this.getSinger(item.singers), + name: item.name, + albumName: item.album && item.album.albumName, + albumId: item.album && item.album.albumId, + songmid: item.id, + copyrightId: item.copyrightId, + source: 'mg', + interval: item.duration ? formatPlayTime(this.getIntv(item.duration)) : null, + img: item.mediumPic ? `https:${item.mediumPic}` : null, + lrc: null, + // lrcUrl: item.lrcUrl, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + }) + return list + }, + filterBoardsData(rawList) { + // console.log(rawList) + let list = [] + for (const board of rawList) { + if (board.template != 'group1') continue + for (const item of board.itemList) { + if ((item.template != 'row1' && item.template != 'grid1' && !item.actionUrl) || !item.actionUrl.includes('rank-info')) continue + + let data = item.displayLogId.param + list.push({ + id: 'mg__' + data.rankId, + name: data.rankName, + bangid: String(data.rankId), + }) + } + } + return list + }, + async getBoards(retryNum = 0) { + // if (++retryNum > 3) return Promise.reject(new Error('try max num')) + // let response + // try { + // response = await this.getBoardsData() + // } catch (error) { + // return this.getBoards(retryNum) + // } + // // console.log(response.body.data.contentItemList) + // if (response.statusCode !== 200 || response.body.code !== this.successCode) return this.getBoards(retryNum) + // const list = this.filterBoardsData(response.body.data.contentItemList) + // // console.log(list) + // // console.log(JSON.stringify(list)) + // this.list = list + // return { + // list, + // source: 'mg', + // } + this.list = boardList + return { + list: boardList, + source: 'mg', + } + }, + getList(bangid, page, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + return this.getData(this.getUrl(bangid, page)).then(({ statusCode, body }) => { + if (statusCode !== 200) return this.getList(bangid, page, retryNum) + let listData = body.match(this.regExps.listData) + if (!listData) return this.getList(bangid, page, retryNum) + const datas = JSON.parse(RegExp.$1) + // console.log(datas) + listData = this.filterData(datas.songs.items) + return { + total: datas.songs.itemTotal, + list: this.filterData(datas.songs.items), + limit: this.limit, + page, + source: 'mg', + } + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/tipSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/tipSearch.js new file mode 100644 index 000000000..fcd4951b3 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/tipSearch.js @@ -0,0 +1,25 @@ +import { createHttpFetch } from './utils' + +export default { + requestObj: null, + cancelTipSearch() { + if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp() + }, + tipSearchBySong(str) { + this.cancelTipSearch() + this.requestObj = createHttpFetch(`https://music.migu.cn/v3/api/search/suggest?keyword=${encodeURIComponent(str)}`, { + headers: { + referer: 'https://music.migu.cn/v3', + }, + }) + return this.requestObj.then(body => { + return body.songs + }) + }, + handleResult(rawData) { + return rawData.map(info => `${info.name} - ${info.singerName}`) + }, + async search(str) { + return this.tipSearchBySong(str).then(result => this.handleResult(result)) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/utils/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/utils/index.js new file mode 100644 index 000000000..503a85f63 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/utils/index.js @@ -0,0 +1,29 @@ +import { httpFetch } from '../../../request' + +/** + * 创建一个适用于MG的Http请求 + * @param {*} url + * @param {*} options + * @param {*} retryNum + */ +export const createHttpFetch = async(url, options, retryNum = 0) => { + if (retryNum > 2) throw new Error('try max num') + let result + try { + result = await httpFetch(url, options).promise + } catch (err) { + console.log(err) + return createHttpFetch(url, options, ++retryNum) + } + if (result.statusCode !== 200 || + ( + (result.body.code !== undefined + ? result.body.code + : result.body.returnCode !== undefined + ? result.body.returnCode + : result.body.code + ) !== '000000') + ) return createHttpFetch(url, options, ++retryNum) + if (result.body.data) return result.body.data + return result.body +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/utils/mrc.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/utils/mrc.js new file mode 100644 index 000000000..98ebb68f9 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/mg/utils/mrc.js @@ -0,0 +1,104 @@ + +// const key = 'karakal@123Qcomyidongtiantianhaoting' +const DELTA = 2654435769n +const MIN_LENGTH = 32 +// const SPECIAL_CHAR = '0' +const keyArr = [ + 27303562373562475n, + 18014862372307051n, + 22799692160172081n, + 34058940340699235n, + 30962724186095721n, + 27303523720101991n, + 27303523720101998n, + 31244139033526382n, + 28992395054481524n, +] + + +const teaDecrypt = (data, key) => { + const length = data.length + const lengthBitint = BigInt(length) + if (length >= 1) { + // let j = data[data.length - 1]; + let j2 = data[0] + let j3 = toLong((6n + (52n / lengthBitint)) * DELTA) + while (true) { + let j4 = j3 + if (j4 == 0n) break + let j5 = toLong(3n & toLong(j4 >> 2n)) + let j6 = lengthBitint + while (true) { + j6-- + if (j6 > 0n) { + let j7 = data[(j6 - 1n)] + let i = j6 + j2 = toLong(data[i] - (toLong(toLong(j2 ^ j4) + toLong(j7 ^ key[toLong(toLong(3n & j6) ^ j5)])) ^ toLong(toLong(toLong(j7 >> 5n) ^ toLong(j2 << 2n)) + toLong(toLong(j2 >> 3n) ^ toLong(j7 << 4n))))) + data[i] = j2 + } else break + } + let j8 = data[lengthBitint - 1n] + j2 = toLong(data[0n] - toLong(toLong(toLong(key[toLong(toLong(j6 & 3n) ^ j5)] ^ j8) + toLong(j2 ^ j4)) ^ toLong(toLong(toLong(j8 >> 5n) ^ toLong(j2 << 2n)) + toLong(toLong(j2 >> 3n) ^ toLong(j8 << 4n))))) + data[0] = j2 + j3 = toLong(j4 - DELTA) + } + } + return data +} + +const longArrToString = (data) => { + const arrayList = [] + for (const j of data) arrayList.push(longToBytes(j).toString('utf16le')) + return arrayList.join('') +} + +// https://stackoverflow.com/a/29132118 +const longToBytes = (l) => { + const result = Buffer.alloc(8) + for (let i = 0; i < 8; i++) { + result[i] = parseInt(l & 0xFFn) + l >>= 8n + } + return result +} + + +const toBigintArray = (data) => { + const length = Math.floor(data.length / 16) + const jArr = Array(length) + for (let i = 0; i < length; i++) { + jArr[i] = toLong(data.substring(i * 16, (i * 16) + 16)) + } + return jArr +} + +// https://github.com/lyswhut/lx-music-desktop/issues/445#issuecomment-1139338682 +const MAX = 9223372036854775807n +const MIN = -9223372036854775808n +const toLong = str => { + const num = typeof str == 'string' ? BigInt('0x' + str) : str + if (num > MAX) return toLong(num - (1n << 64n)) + else if (num < MIN) return toLong(num + (1n << 64n)) + return num +} + +export const decrypt = (data) => { + // console.log(data.length) + // -3551594764563790630 + // console.log(toLongArrayFromArr(Buffer.from(key))) + // console.log(teaDecrypt(toBigintArray(data), keyArr)) + // console.log(longArrToString(teaDecrypt(toBigintArray(data), keyArr))) + // console.log(toByteArray(teaDecrypt(toBigintArray(data), keyArr))) + return (data == null || data.length < MIN_LENGTH) + ? data + : longArrToString(teaDecrypt(toBigintArray(data), keyArr)) +} + +// console.log(14895149309145760986n - ) +// console.log(toLong('14895149309145760986')) +// console.log(decrypt(str)) +// console.log(decrypt(str)) +// console.log(toByteArray([6048138644744000495n])) +// console.log(toByteArray([16325999628386395n])) +// console.log(toLong(90994076459972177136n)) + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/options.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/options.js new file mode 100644 index 000000000..c6ce350cd --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/options.js @@ -0,0 +1,9 @@ +export const bHh = '624868746c' + +export const headers = { + 'User-Agent': 'lx-music request', + [bHh]: [bHh], +} + + +export const timeout = 15000 diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/api-test.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/api-test.js new file mode 100644 index 000000000..2fe66dd92 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/api-test.js @@ -0,0 +1,29 @@ +import { httpFetch } from '../../request' +import { requestMsg } from '../../message' +import { headers, timeout } from '../options' +import { dnsLookup } from '../utils' + +const api_messoer = { + getMusicUrl(songInfo, type) { + const requestObj = httpFetch(`http://ts.tempmusics.tk/url/tx/${songInfo.songmid}/${type}`, { + method: 'get', + timeout, + headers, + lookup: dnsLookup, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { + if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) + switch (body.code) { + case 0: return Promise.resolve({ type, url: body.data }) + default: return Promise.reject(new Error(requestMsg.fail)) + } + }) + return requestObj + }, + getPic(songInfo) { + return Promise.resolve(`https://y.gtimg.cn/music/photo_new/T002R500x500M000${songInfo.albumId}.jpg`) + }, +} + +export default api_messoer diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/comment.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/comment.js new file mode 100644 index 000000000..09745c438 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/comment.js @@ -0,0 +1,282 @@ +import { httpFetch } from '../../request' +import { dateFormat2 } from '../../index' +import getMusicInfo from './musicInfo' + +const emojis = { + e400846: '😘', + e400874: '😴', + e400825: '😃', + e400847: '😙', + e400835: '😍', + e400873: '😳', + e400836: '😎', + e400867: '😭', + e400832: '😊', + e400837: '😏', + e400875: '😫', + e400831: '😉', + e400855: '😡', + e400823: '😄', + e400862: '😨', + e400844: '😖', + e400841: '😓', + e400830: '😈', + e400828: '😆', + e400833: '😋', + e400822: '😀', + e400843: '😕', + e400829: '😇', + e400824: '😂', + e400834: '😌', + e400877: '😷', + e400132: '🍉', + e400181: '🍺', + e401067: '☕️', + e400186: '🥧', + e400343: '🐷', + e400116: '🌹', + e400126: '🍃', + e400613: '💋', + e401236: '❤️', + e400622: '💔', + e400637: '💣', + e400643: '💩', + e400773: '🔪', + e400102: '🌛', + e401328: '🌞', + e400420: '👏', + e400914: '🙌', + e400408: '👍', + e400414: '👎', + e401121: '✋', + e400396: '👋', + e400384: '👉', + e401115: '✊', + e400402: '👌', + e400905: '🙈', + e400906: '🙉', + e400907: '🙊', + e400562: '👻', + e400932: '🙏', + e400644: '💪', + e400611: '💉', + e400185: '🎁', + e400655: '💰', + e400325: '🐥', + e400612: '💊', + e400198: '🎉', + e401685: '⚡️', + e400631: '💝', + e400768: '🔥', + e400432: '👑', +} + +const songIdMap = new Map() +const promises = new Map() + +export default { + _requestObj: null, + _requestObj2: null, + async getSongId({ songId, songmid }) { + if (songId) return songId + if (songIdMap.has(songmid)) return songIdMap.get(songmid) + if (promises.has(songmid)) return (await promises.get(songmid)).songId + const promise = getMusicInfo(songmid) + promises.set(promise) + const info = await promise + songIdMap.set(songmid, info.songId) + promises.delete(songmid) + return info.songId + }, + async getComment(mInfo, page = 1, limit = 20) { + if (this._requestObj) this._requestObj.cancelHttp() + const songId = await this.getSongId(mInfo) + + const _requestObj = httpFetch('http://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg', { + method: 'POST', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + }, + form: { + uin: '0', + format: 'json', + cid: '205360772', + reqtype: '2', + biztype: '1', + topid: songId, + cmd: '8', + needmusiccrit: '1', + pagenum: page - 1, + pagesize: limit, + }, + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.code !== 0) throw new Error('获取评论失败') + // console.log(body, statusCode) + const comment = body.comment + return { + source: 'tx', + comments: this.filterNewComment(comment.commentlist), + total: comment.commenttotal, + page, + limit, + maxPage: Math.ceil(comment.commenttotal / limit) || 1, + } + }, + async getHotComment(mInfo, page = 1, limit = 20) { + // const _requestObj2 = httpFetch('http://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg', { + // method: 'POST', + // headers: { + // 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + // }, + // form: { + // uin: '0', + // format: 'json', + // cid: '205360772', + // reqtype: '2', + // biztype: '1', + // topid: songId, + // cmd: '9', + // needmusiccrit: '1', + // pagenum: page - 1, + // pagesize: limit, + // }, + // }) + if (this._requestObj2) this._requestObj2.cancelHttp() + const songId = await this.getSongId(mInfo) + + const _requestObj2 = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'POST', + body: { + comm: { + cv: 4747474, + ct: 24, + format: 'json', + inCharset: 'utf-8', + outCharset: 'utf-8', + notice: 0, + platform: 'yqq.json', + needNewCode: 1, + uin: 0, + }, + req: { + module: 'music.globalComment.CommentRead', + method: 'GetHotCommentList', + param: { + BizType: 1, + BizId: String(songId), + LastCommentSeqNo: '', + PageSize: limit, + PageNum: page - 1, + HotType: 1, + WithAirborne: 0, + PicEnable: 1, + }, + }, + }, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.0.0', + referer: 'https://y.qq.com/', + origin: 'https://y.qq.com', + }, + }) + const { body, statusCode } = await _requestObj2.promise + // console.log('body', body) + if (statusCode != 200 || body.code !== 0 || body.req.code !== 0) throw new Error('获取热门评论失败') + const comment = body.req.data.CommentList + return { + source: 'tx', + comments: this.filterHotComment(comment.Comments), + total: comment.Total, + page, + limit, + maxPage: Math.ceil(comment.Total / limit) || 1, + } + }, + filterNewComment(rawList) { + return rawList.map(item => { + let time = this.formatTime(item.time) + let timeStr = time ? dateFormat2(time) : null + if (item.middlecommentcontent) { + let firstItem = item.middlecommentcontent[0] + firstItem.avatarurl = item.avatarurl + firstItem.praisenum = item.praisenum + item.avatarurl = null + item.praisenum = null + item.middlecommentcontent.reverse() + } + return { + id: `${item.rootcommentid}_${item.commentid}`, + rootId: item.rootcommentid, + text: item.rootcommentcontent ? this.replaceEmoji(item.rootcommentcontent).replace(/\\n/g, '\n') : '', + time: item.rootcommentid == item.commentid ? time : null, + timeStr: item.rootcommentid == item.commentid ? timeStr : null, + userName: item.rootcommentnick ? item.rootcommentnick.substring(1) : '', + avatar: item.avatarurl, + userId: item.encrypt_rootcommentuin, + likedCount: item.praisenum, + reply: item.middlecommentcontent + ? item.middlecommentcontent.map(c => { + // let index = c.subcommentid.lastIndexOf('_') + return { + id: `sub_${item.rootcommentid}_${c.subcommentid}`, + text: this.replaceEmoji(c.subcommentcontent).replace(/\\n/g, '\n'), + time: c.subcommentid == item.commentid ? time : null, + timeStr: c.subcommentid == item.commentid ? timeStr : null, + userName: c.replynick.substring(1), + avatar: c.avatarurl, + userId: c.encrypt_replyuin, + likedCount: c.praisenum, + } + }) + : [], + } + }) + }, + filterHotComment(rawList) { + return rawList.map(item => { + return { + id: `${item.SeqNo}_${item.CmId}`, + rootId: item.SeqNo, + text: item.Content ? this.replaceEmoji(item.Content).replace(/\\n/g, '\n') : '', + time: item.PubTime ? this.formatTime(item.PubTime) : null, + timeStr: item.PubTime ? dateFormat2(this.formatTime(item.PubTime)) : null, + userName: item.Nick ?? '', + images: item.Pic ? [item.Pic] : [], + avatar: item.Avatar, + location: item.Location ? item.Location : '', + userId: item.EncryptUin, + likedCount: item.PraiseNum, + reply: item.SubComments + ? item.SubComments.map(c => { + return { + id: `sub_${c.SeqNo}_${c.CmId}`, + text: this.replaceEmoji(c.Content).replace(/\\n/g, '\n'), + time: c.PubTime ? this.formatTime(c.PubTime) : null, + timeStr: c.PubTime ? dateFormat2(this.formatTime(c.PubTime)) : null, + userName: c.Nick ?? '', + avatar: c.Avatar, + images: c.Pic ? [c.Pic] : [], + userId: c.EncryptUin, + likedCount: c.PraiseNum, + } + }) + : [], + } + }) + }, + replaceEmoji(msg) { + let rxp = /^\[em\](e\d+)\[\/em\]$/ + let result = msg.match(/\[em\]e\d+\[\/em\]/g) + if (!result) return msg + result = Array.from(new Set(result)) + for (let item of result) { + let code = item.replace(rxp, '$1') + msg = msg.replace(new RegExp(item.replace('[em]', '\\[em\\]').replace('[/em]', '\\[\\/em\\]'), 'g'), emojis[code] || '') + } + return msg + }, + formatTime(time) { + return String(time).length < 10 ? null : parseInt(time + '000') + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/hotSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/hotSearch.js new file mode 100644 index 000000000..25afd8cd8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/hotSearch.js @@ -0,0 +1,54 @@ +import { httpFetch } from '../../request' + +export default { + _requestObj: null, + async getList(retryNum = 0) { + if (this._requestObj) this._requestObj.cancelHttp() + if (retryNum > 2) return Promise.reject(new Error('try max num')) + + // const _requestObj = httpFetch('https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg', { + // method: 'get', + // headers: { + // Referer: 'https://y.qq.com/portal/player.html', + // }, + // }) + const _requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'post', + body: { + comm: { + ct: '19', + cv: '1803', + guid: '0', + patch: '118', + psrf_access_token_expiresAt: 0, + psrf_qqaccess_token: '', + psrf_qqopenid: '', + psrf_qqunionid: '', + tmeAppID: 'qqmusic', + tmeLoginType: 0, + uin: '0', + wid: '0', + }, + hotkey: { + method: 'GetHotkeyForQQMusicPC', + module: 'tencent_musicsoso_hotkey.HotkeyService', + param: { + search_id: '', + uin: 0, + }, + }, + }, + headers: { + Referer: 'https://y.qq.com/portal/player.html', + }, + }) + const { body, statusCode } = await _requestObj.promise + // console.log(body) + if (statusCode != 200 || body.code !== 0) throw new Error('获取热搜词失败') + // console.log(body) + return { source: 'tx', list: this.filterList(body.hotkey.data.vec_hotkey) } + }, + filterList(rawList) { + return rawList.map(item => item.query) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/index.js new file mode 100644 index 000000000..f30a9410a --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/index.js @@ -0,0 +1,33 @@ +import leaderboard from './leaderboard' +import lyric from './lyric' +import songList from './songList' +import musicSearch from './musicSearch' +import { apis } from '../api-source' +import hotSearch from './hotSearch' +import comment from './comment' +// import tipSearch from './tipSearch' + +const tx = { + // tipSearch, + leaderboard, + songList, + musicSearch, + hotSearch, + comment, + + getMusicUrl(songInfo, type) { + return apis('tx').getMusicUrl(songInfo, type) + }, + getLyric(songInfo) { + // let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer + return lyric.getLyric(songInfo) + }, + async getPic(songInfo) { + return `https://y.gtimg.cn/music/photo_new/T002R500x500M000${songInfo.albumId}.jpg` + }, + getMusicDetailPageUrl(songInfo) { + return `https://y.qq.com/n/yqq/song/${songInfo.songmid}.html` + }, +} + +export default tx diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/leaderboard.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/leaderboard.js new file mode 100644 index 000000000..2f3f4ebc5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/leaderboard.js @@ -0,0 +1,247 @@ +import { httpFetch } from '../../request' +import { formatPlayTime, sizeFormate } from '../../index' +import { formatSingerName } from '../utils' + +let boardList = [{ id: 'tx__4', name: '流行指数榜', bangid: '4' }, { id: 'tx__26', name: '热歌榜', bangid: '26' }, { id: 'tx__27', name: '新歌榜', bangid: '27' }, { id: 'tx__62', name: '飙升榜', bangid: '62' }, { id: 'tx__58', name: '说唱榜', bangid: '58' }, { id: 'tx__57', name: '喜力电音榜', bangid: '57' }, { id: 'tx__28', name: '网络歌曲榜', bangid: '28' }, { id: 'tx__5', name: '内地榜', bangid: '5' }, { id: 'tx__3', name: '欧美榜', bangid: '3' }, { id: 'tx__59', name: '香港地区榜', bangid: '59' }, { id: 'tx__16', name: '韩国榜', bangid: '16' }, { id: 'tx__60', name: '抖快榜', bangid: '60' }, { id: 'tx__29', name: '影视金曲榜', bangid: '29' }, { id: 'tx__17', name: '日本榜', bangid: '17' }, { id: 'tx__52', name: '腾讯音乐人原创榜', bangid: '52' }, { id: 'tx__36', name: 'K歌金曲榜', bangid: '36' }, { id: 'tx__61', name: '台湾地区榜', bangid: '61' }, { id: 'tx__63', name: 'DJ舞曲榜', bangid: '63' }, { id: 'tx__64', name: '综艺新歌榜', bangid: '64' }, { id: 'tx__65', name: '国风热歌榜', bangid: '65' }, { id: 'tx__67', name: '听歌识曲榜', bangid: '67' }, { id: 'tx__72', name: '动漫音乐榜', bangid: '72' }, { id: 'tx__73', name: '游戏音乐榜', bangid: '73' }, { id: 'tx__75', name: '有声榜', bangid: '75' }, { id: 'tx__131', name: '校园音乐人排行榜', bangid: '131' }] + +export default { + limit: 300, + list: [ + { + id: 'txlxzsb', + name: '流行榜', + bangid: 4, + }, + { + id: 'txrgb', + name: '热歌榜', + bangid: 26, + }, + { + id: 'txwlhgb', + name: '网络榜', + bangid: 28, + }, + { + id: 'txdyb', + name: '抖音榜', + bangid: 60, + }, + { + id: 'txndb', + name: '内地榜', + bangid: 5, + }, + { + id: 'txxgb', + name: '香港榜', + bangid: 59, + }, + { + id: 'txtwb', + name: '台湾榜', + bangid: 61, + }, + { + id: 'txoumb', + name: '欧美榜', + bangid: 3, + }, + { + id: 'txhgb', + name: '韩国榜', + bangid: 16, + }, + { + id: 'txrbb', + name: '日本榜', + bangid: 17, + }, + { + id: 'txtybb', + name: 'YouTube榜', + bangid: 128, + }, + ], + listDetailRequest(id, period, limit) { + // console.log(id, period, limit) + return httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'post', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + }, + body: { + toplist: { + module: 'musicToplist.ToplistInfoServer', + method: 'GetDetail', + param: { + topid: id, + num: limit, + period, + }, + }, + comm: { + uin: 0, + format: 'json', + ct: 20, + cv: 1859, + }, + }, + }).promise + }, + regExps: { + periodList: //g, + period: /data-listname="(.+?)" data-tid=".*?\/(.+?)" data-date="(.+?)" .+?<\/i>/, + }, + periods: {}, + periodUrl: 'https://c.y.qq.com/node/pc/wk_v15/top.html', + _requestBoardsObj: null, + getBoardsData() { + if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp() + this._requestBoardsObj = httpFetch('https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg?g_tk=1928093487&inCharset=utf-8&outCharset=utf-8¬ice=0&format=json&uin=0&needNewCode=1&platform=h5') + return this._requestBoardsObj.promise + }, + getData(url) { + const requestDataObj = httpFetch(url) + return requestDataObj.promise + }, + filterData(rawList) { + // console.log(rawList) + return rawList.map(item => { + let types = [] + let _types = {} + if (item.file.size_128mp3 !== 0) { + let size = sizeFormate(item.file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (item.file.size_320mp3 !== 0) { + let size = sizeFormate(item.file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (item.file.size_flac !== 0) { + let size = sizeFormate(item.file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + if (item.file.size_hires !== 0) { + let size = sizeFormate(item.file.size_hires) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + } + // types.reverse() + return { + singer: formatSingerName(item.singer, 'name'), + name: item.title, + albumName: item.album.name, + albumId: item.album.mid, + source: 'tx', + interval: formatPlayTime(item.interval), + songId: item.id, + albumMid: item.album.mid, + strMediaMid: item.file.media_mid, + songmid: item.mid, + img: (item.album.name === '' || item.album.name === '空') + ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.album.mid}.jpg`, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + getPeriods(bangid) { + return this.getData(this.periodUrl).then(({ body: html }) => { + let result = html.match(this.regExps.periodList) + if (!result) return Promise.reject(new Error('get data failed')) + result.forEach(item => { + let result = item.match(this.regExps.period) + if (!result) return + this.periods[result[2]] = { + name: result[1], + bangid: result[2], + period: result[3], + } + }) + const info = this.periods[bangid] + return info && info.period + }) + }, + filterBoardsData(rawList) { + // console.log(rawList) + let list = [] + for (const board of rawList) { + // 排除 MV榜 + if (board.id == 201) continue + + if (board.topTitle.startsWith('巅峰榜·')) { + board.topTitle = board.topTitle.substring(4, board.topTitle.length) + } + if (!board.topTitle.endsWith('榜')) board.topTitle += '榜' + list.push({ + id: 'tx__' + board.id, + name: board.topTitle, + bangid: String(board.id), + }) + } + return list + }, + async getBoards(retryNum = 0) { + // if (++retryNum > 3) return Promise.reject(new Error('try max num')) + // let response + // try { + // response = await this.getBoardsData() + // } catch (error) { + // return this.getBoards(retryNum) + // } + // // console.log(response.body) + // if (response.statusCode !== 200 || response.body.code !== 0) return this.getBoards(retryNum) + // const list = this.filterBoardsData(response.body.data.topList) + // console.log(list) + // console.log(JSON.stringify(list)) + // this.list = list + // return { + // list, + // source: 'tx', + // } + this.list = boardList + return { + list: boardList, + source: 'tx', + } + }, + getList(bangid, page, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + bangid = parseInt(bangid) + let info = this.periods[bangid] + let p = info ? Promise.resolve(info.period) : this.getPeriods(bangid) + return p.then(period => { + return this.listDetailRequest(bangid, period, this.limit).then(resp => { + if (resp.body.code !== 0) return this.getList(bangid, page, retryNum) + return { + total: resp.body.toplist.data.songInfoList.length, + list: this.filterData(resp.body.toplist.data.songInfoList), + limit: this.limit, + page: 1, + source: 'tx', + } + }) + }) + }, + + getDetailPageUrl(id) { + if (typeof id == 'string') id = id.replace('tx__', '') + return `https://y.qq.com/n/ryqq/toplist/${id}` + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/lyric.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/lyric.js new file mode 100644 index 000000000..3090f2f88 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/lyric.js @@ -0,0 +1,297 @@ +import { httpFetch } from '../../request' +import getMusicInfo from './musicInfo' +import { decodeQrc } from './qrcDecode' + +const songIdMap = new Map() +const promises = new Map() +export const decodeLyric = async(lrc, tlrc, rlrc) => ({ + lyric: await decodeQrc(lrc), + tlyric: await decodeQrc(tlrc), + rlyric: await decodeQrc(rlrc), +}) + + +const parseTools = { + rxps: { + info: /^{"/, + lineTime: /^\[(\d+),\d+\]/, + lineTime2: /^\[([\d:.]+)\]/, + wordTime: /\(\d+,\d+\)/, + wordTimeAll: /(\(\d+,\d+\))/g, + timeLabelFixRxp: /(?:\.0+|0+)$/, + }, + msFormat(timeMs) { + if (Number.isNaN(timeMs)) return '' + let ms = (timeMs % 1000).toString().padStart(3, '0') + timeMs /= 1000 + let m = parseInt(timeMs / 60).toString().padStart(2, '0') + timeMs %= 60 + let s = parseInt(timeMs).toString().padStart(2, '0') + return `[${m}:${s}.${ms}]` + }, + parseLyric(lrc) { + lrc = lrc.trim() + lrc = lrc.replace(/\r/g, '') + if (!lrc) return { lyric: '', lxlyric: '' } + const lines = lrc.split('\n') + + const lxlrcLines = [] + const lrcLines = [] + + for (let line of lines) { + line = line.trim() + let result = this.rxps.lineTime.exec(line) + if (!result) { + if (line.startsWith('[offset')) { + lxlrcLines.push(line) + lrcLines.push(line) + } + if (this.rxps.lineTime2.test(line)) { + // lxlrcLines.push(line) + lrcLines.push(line) + } + continue + } + + const startMsTime = parseInt(result[1]) + const startTimeStr = this.msFormat(startMsTime) + if (!startTimeStr) continue + + let words = line.replace(this.rxps.lineTime, '') + + lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`) + + let times = words.match(this.rxps.wordTimeAll) + if (!times) continue + times = times.map(time => { + const result = /\((\d+),(\d+)\)/.exec(time) + return `<${Math.trunc(Math.max(parseInt(result[1]) - startMsTime, 0))},${result[2]}>` + }) + const wordArr = words.split(this.rxps.wordTime) + const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('') + lxlrcLines.push(`${startTimeStr}${newWords}`) + } + return { + lyric: lrcLines.join('\n'), + lxlyric: lxlrcLines.join('\n'), + } + }, + parseRlyric(lrc) { + lrc = lrc.trim() + lrc = lrc.replace(/\r/g, '') + if (!lrc) return { lyric: '', lxlyric: '' } + const lines = lrc.split('\n') + + const lrcLines = [] + + for (let line of lines) { + line = line.trim() + let result = this.rxps.lineTime.exec(line) + if (!result) continue + + const startMsTime = parseInt(result[1]) + const startTimeStr = this.msFormat(startMsTime) + if (!startTimeStr) continue + + let words = line.replace(this.rxps.lineTime, '') + + lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`) + } + return lrcLines.join('\n') + }, + removeTag(str) { + return str.replace(/^[\S\s]*?LyricContent="/, '').replace(/"\/>[\S\s]*?$/, '') + }, + getIntv(interval) { + if (!interval) return 0 + if (!interval.includes('.')) interval += '.0' + let arr = interval.split(/:|\./) + while (arr.length < 3) arr.unshift('0') + const [m, s, ms] = arr + return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms) + }, + fixRlrcTimeTag(rlrc, lrc) { + // console.log(lrc) + // console.log(rlrc) + const rlrcLines = rlrc.split('\n') + let lrcLines = lrc.split('\n') + // let temp = [] + let newLrc = [] + rlrcLines.forEach((line) => { + const result = this.rxps.lineTime2.exec(line) + if (!result) return + const words = line.replace(this.rxps.lineTime2, '') + if (!words.trim()) return + const t1 = this.getIntv(result[1]) + + while (lrcLines.length) { + const lrcLine = lrcLines.shift() + const lrcLineResult = this.rxps.lineTime2.exec(lrcLine) + if (!lrcLineResult) continue + const t2 = this.getIntv(lrcLineResult[1]) + if (Math.abs(t1 - t2) < 100) { + newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0])) + break + } + // temp.push(line) + } + // lrcLines = [...temp, ...lrcLines] + // temp = [] + }) + return newLrc.join('\n') + }, + fixTlrcTimeTag(tlrc, lrc) { + // console.log(lrc) + // console.log(tlrc) + const tlrcLines = tlrc.split('\n') + let lrcLines = lrc.split('\n') + // let temp = [] + let newLrc = [] + tlrcLines.forEach((line) => { + const result = this.rxps.lineTime2.exec(line) + if (!result) return + const words = line.replace(this.rxps.lineTime2, '') + if (!words.trim()) return + let time = result[1] + if (time.includes('.')) { + time += ''.padStart(3 - time.split('.')[1].length, '0') + } + const t1 = this.getIntv(time) + + while (lrcLines.length) { + const lrcLine = lrcLines.shift() + const lrcLineResult = this.rxps.lineTime2.exec(lrcLine) + if (!lrcLineResult) continue + const t2 = this.getIntv(lrcLineResult[1]) + if (Math.abs(t1 - t2) < 100) { + newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0])) + break + } + // temp.push(line) + } + // lrcLines = [...temp, ...lrcLines] + // temp = [] + }) + return newLrc.join('\n') + }, + parse(lrc, tlrc, rlrc) { + const info = { + lyric: '', + tlyric: '', + rlyric: '', + lxlyric: '', + } + if (lrc) { + let { lyric, lxlyric } = this.parseLyric(this.removeTag(lrc)) + info.lyric = lyric + info.lxlyric = lxlyric + // console.log(lyric) + // console.log(lxlyric) + } + if (rlrc) info.rlyric = this.fixRlrcTimeTag(this.parseRlyric(this.removeTag(rlrc)), info.lyric) + if (tlrc) info.tlyric = this.fixTlrcTimeTag(tlrc, info.lyric) + // console.log(info.lyric) + // console.log(info.tlyric) + // console.log(info.rlyric) + + return info + }, +} + + +export default { + successCode: 0, + async getSongId({ songId, songmid }) { + if (songId) return songId + if (songIdMap.has(songmid)) return songIdMap.get(songmid) + if (promises.has(songmid)) return (await promises.get(songmid)).songId + const promise = getMusicInfo(songmid) + promises.set(promise) + const info = await promise + songIdMap.set(songmid, info.songId) + promises.delete(songmid) + return info.songId + }, + async parseLyric(lrc, tlrc, rlrc) { + const { lyric, tlyric, rlyric } = await decodeLyric(lrc, tlrc, rlrc) + // return { + + // } + // console.log(lyric) + // console.log(tlyric) + // console.log(rlyric) + return parseTools.parse(lyric, tlyric, rlyric) + }, + getLyric(mInfo, retryNum = 0) { + if (retryNum > 3) return Promise.reject(new Error('Get lyric failed')) + + return { + cancelHttp() { + + }, + promise: this.getSongId(mInfo).then(songId => { + const requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'post', + headers: { + referer: 'https://y.qq.com', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + }, + body: { + comm: { + ct: '19', + cv: '1859', + uin: '0', + }, + req: { + method: 'GetPlayLyricInfo', + module: 'music.musichallSong.PlayLyricInfo', + param: { + format: 'json', + crypt: 1, + ct: 19, + cv: 1873, + interval: 0, + lrc_t: 0, + qrc: 1, + qrc_t: 0, + roma: 1, + roma_t: 0, + songID: songId, + trans: 1, + trans_t: 0, + type: -1, + }, + }, + }, + }) + return requestObj.promise.then(({ body }) => { + // console.log(body) + if (body.code != this.successCode || body.req.code != this.successCode) return this.getLyric(songId, ++retryNum) + const data = body.req.data + return this.parseLyric(data.lyric, data.trans, data.roma) + }) + }), + } + }, +} + +// export default { +// regexps: { +// matchLrc: /.+"lyric":"([\w=+/]*)".+/, +// }, +// getLyric(songmid) { +// const requestObj = httpFetch(`https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?songmid=${songmid}&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&platform=yqq`, { +// headers: { +// Referer: 'https://y.qq.com/portal/player.html', +// }, +// }) +// requestObj.promise = requestObj.promise.then(({ body }) => { +// if (body.code != 0 || !body.lyric) return Promise.reject(new Error('Get lyric failed')) +// return { +// lyric: decodeName(b64DecodeUnicode(body.lyric)), +// tlyric: decodeName(b64DecodeUnicode(body.trans)), +// } +// }) +// return requestObj +// }, +// } diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/musicInfo.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/musicInfo.js new file mode 100644 index 000000000..b490a3c1b --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/musicInfo.js @@ -0,0 +1,98 @@ +import { httpFetch } from '../../request' +import { formatPlayTime, sizeFormate } from '../../index' + +const getSinger = (singers) => { + let arr = [] + singers.forEach(singer => { + arr.push(singer.name) + }) + return arr.join('、') +} + +export default (songmid) => { + const requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'post', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + }, + body: { + comm: { + ct: '19', + cv: '1859', + uin: '0', + }, + req: { + module: 'music.pf_song_detail_svr', + method: 'get_song_detail_yqq', + param: { + song_type: 0, + song_mid: songmid, + }, + }, + }, + }) + return requestObj.promise.then(({ body }) => { + // console.log(body) + if (body.code != 0 || body.req.code != 0) return Promise.reject(new Error('获取歌曲信息失败')) + const item = body.req.data.track_info + if (!item.file?.media_mid) return null + + let types = [] + let _types = {} + const file = item.file + if (file.size_128mp3 != 0) { + let size = sizeFormate(file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (file.size_320mp3 !== 0) { + let size = sizeFormate(file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (file.size_flac !== 0) { + let size = sizeFormate(file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + if (file.size_hires !== 0) { + let size = sizeFormate(file.size_hires) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + } + // types.reverse() + let albumId = '' + let albumName = '' + if (item.album) { + albumName = item.album.name + albumId = item.album.mid + } + return { + singer: getSinger(item.singer), + name: item.title, + albumName, + albumId, + source: 'tx', + interval: formatPlayTime(item.interval), + songId: item.id, + albumMid: item.album?.mid ?? '', + strMediaMid: item.file.media_mid, + songmid: item.mid, + img: (albumId === '' || albumId === '空') + ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg`, + types, + _types, + typeUrl: {}, + } + }) +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/musicSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/musicSearch.js new file mode 100644 index 000000000..bc06ab994 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/musicSearch.js @@ -0,0 +1,148 @@ +import { formatPlayTime, sizeFormate } from '../../index' +import { formatSingerName } from '../utils' +import { signRequest } from './utils' + +export default { + limit: 50, + total: 0, + page: 0, + allPage: 1, + successCode: 0, + musicSearch(str, page, limit, retryNum = 0) { + if (retryNum > 5) return Promise.reject(new Error('搜索失败')) + const searchRequest = signRequest({ + comm: { + _channelid: '0', + _os_version: '6.2.9200-2', + ct: '19', + cv: '2151', + guid: '1F70E520B2EAA7D25E11760783C53CA9', + patch: '118', + psrf_access_token_expiresAt: 0, + psrf_qqaccess_token: '', + psrf_qqopenid: '', + psrf_qqunionid: '', + tmeAppID: 'qqmusic', + tmeLoginType: 0, + uin: '0', + wid: '7223299733393904640', + }, + 'music.search.SearchCgiService': { + module: 'music.search.SearchCgiService', + method: 'DoSearchForQQMusicDesktop', + param: { + grp: 1, + num_per_page: limit, + page_num: page, + query: str, + remoteplace: 'txt.newclient.top', + search_type: 0, + searchid: this.getSearchId(), + }, + }, + }) + return searchRequest.then(({ body }) => { + // console.log(body) + const req = body?.['music.search.SearchCgiService'] ?? body?.req + if (!req || body.code != this.successCode || req.code != this.successCode) { + return this.musicSearch(str, page, limit, ++retryNum) + } + return req.data + }) + }, + /** + * PC 客户端版 searchid:32 位大写十六进制 GUID + 5 位补零随机数 = 37 字符。 + * 对应 QQ 音乐 PC 端 searchid 形状(服务端只需要唯一的会话 ID,形状一致即可)。 + */ + getSearchId() { + let guid = '' + for (let i = 0; i < 32; i++) guid += Math.floor(Math.random() * 16).toString(16) + return guid.toUpperCase() + String(Math.floor(Math.random() * 100000)).padStart(5, '0') + }, + handleResult(rawList) { + // console.log(rawList) + if (!rawList || !Array.isArray(rawList)) return [] + const list = [] + rawList.forEach(item => { + if (!item.file?.media_mid) return + + let types = [] + let _types = {} + const file = item.file + if (file.size_128mp3 != 0) { + let size = sizeFormate(file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (file.size_320mp3 !== 0) { + let size = sizeFormate(file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (file.size_flac !== 0) { + let size = sizeFormate(file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + if (file.size_hires !== 0) { + let size = sizeFormate(file.size_hires) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + } + // types.reverse() + let albumId = '' + let albumName = '' + if (item.album) { + albumName = item.album.name + albumId = item.album.mid + } + list.push({ + singer: formatSingerName(item.singer, 'name'), + // name: item.name + (item.title_extra ?? ''), + name: item.title, + albumName, + albumId, + source: 'tx', + interval: item.interval ? formatPlayTime(item.interval) : null, + songId: item.id, + albumMid: item.album?.mid ?? '', + strMediaMid: item.file.media_mid, + songmid: item.mid, + img: (albumId === '' || albumId === '空') + ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg`, + types, + _types, + typeUrl: {}, + }) + }) + // console.log(list) + return list + }, + search(str, page = 1, limit) { + if (limit == null) limit = this.limit + return this.musicSearch(str, page, limit).then(({ body, meta }) => { + let list = this.handleResult(body.song.list) + + this.total = meta.sum + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + limit, + total: this.total, + source: 'tx', + }) + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/qrcDecode.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/qrcDecode.js new file mode 100644 index 000000000..ae5e8176d --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/qrcDecode.js @@ -0,0 +1,252 @@ +import { inflate, constants } from 'zlib' + +/** + * QRC 歌词解密(3DES-ECB + zlib inflate) + * + * 由原 C++ native 插件(qrc_decode.node)1:1 移植为纯 JS 实现。 + * 加密算法为标准 3DES(DES-EDE3,ECB 模式、无填充),密钥固定。 + * 数据流程:hex 字符串 -> 字节 -> 3DES 解密 -> zlib inflate -> UTF-8 文本。 + */ + +const DES_ENCRYPT = 1 +const DES_DECRYPT = 0 + +// DES S-box。注意:QRC 使用的是非标准 DES 变体, +// 其中 S2[23]=15、S4[53]=10 两处与标准 DES 不同(标准为 14、1)。 +// 这两个值与原 native 插件(qrc_decode.node)二进制完全一致,不可改为标准值, +// 否则解密结果错误。下表逐字节对应原 C++ 实现。 +const sbox = [ + [ + 14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, + 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13, + ], + [ + 15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2, 8, 15, 12, 0, 1, 10, 6, 9, 11, 5, + 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9, + ], + [ + 10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, + 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12, + ], + [ + 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, + 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 3, 15, 0, 6, 10, 10, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14, + ], + [ + 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, + 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3, + ], + [ + 12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, + 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13, + ], + [ + 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, + 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12, + ], + [ + 13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, + 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11, + ], +] + +const key_rnd_shift = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1] +const key_perm_c = [ + 56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, +] +const key_perm_d = [ + 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3, +] +const key_compression = [ + 13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, 51, 30, 36, 46, + 54, 29, 39, 50, 44, 32, 47, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31, +] + +// 全部以无符号 32 位处理,>>> 0 保证溢出行为与 C++ uint32_t 一致 +const bitnum = (a, b, c) => (((a[((b / 32) | 0) * 4 + 3 - (((b % 32) / 8) | 0)] >>> (7 - (b % 8))) & 1) << c) >>> 0 + +const bitnum_intr = (a, b, c) => (((a >>> (31 - b)) & 1) << c) >>> 0 + +const bitnum_intl = (a, b, c) => (((((a << b) >>> 0) & 0x80000000) >>> 0) >>> c) >>> 0 + +const sbox_bit = a => (a & 32) | ((a & 31) >>> 1) | ((a & 1) << 4) + +// 初始置换:返回 [s0, s1] +const initial_permutation = (input) => { + const s0 = ( + bitnum(input, 57, 31) | bitnum(input, 49, 30) | bitnum(input, 41, 29) | bitnum(input, 33, 28) | + bitnum(input, 25, 27) | bitnum(input, 17, 26) | bitnum(input, 9, 25) | bitnum(input, 1, 24) | + bitnum(input, 59, 23) | bitnum(input, 51, 22) | bitnum(input, 43, 21) | bitnum(input, 35, 20) | + bitnum(input, 27, 19) | bitnum(input, 19, 18) | bitnum(input, 11, 17) | bitnum(input, 3, 16) | + bitnum(input, 61, 15) | bitnum(input, 53, 14) | bitnum(input, 45, 13) | bitnum(input, 37, 12) | + bitnum(input, 29, 11) | bitnum(input, 21, 10) | bitnum(input, 13, 9) | bitnum(input, 5, 8) | + bitnum(input, 63, 7) | bitnum(input, 55, 6) | bitnum(input, 47, 5) | bitnum(input, 39, 4) | + bitnum(input, 31, 3) | bitnum(input, 23, 2) | bitnum(input, 15, 1) | bitnum(input, 7, 0) + ) >>> 0 + const s1 = ( + bitnum(input, 56, 31) | bitnum(input, 48, 30) | bitnum(input, 40, 29) | bitnum(input, 32, 28) | + bitnum(input, 24, 27) | bitnum(input, 16, 26) | bitnum(input, 8, 25) | bitnum(input, 0, 24) | + bitnum(input, 58, 23) | bitnum(input, 50, 22) | bitnum(input, 42, 21) | bitnum(input, 34, 20) | + bitnum(input, 26, 19) | bitnum(input, 18, 18) | bitnum(input, 10, 17) | bitnum(input, 2, 16) | + bitnum(input, 60, 15) | bitnum(input, 52, 14) | bitnum(input, 44, 13) | bitnum(input, 36, 12) | + bitnum(input, 28, 11) | bitnum(input, 20, 10) | bitnum(input, 12, 9) | bitnum(input, 4, 8) | + bitnum(input, 62, 7) | bitnum(input, 54, 6) | bitnum(input, 46, 5) | bitnum(input, 38, 4) | + bitnum(input, 30, 3) | bitnum(input, 22, 2) | bitnum(input, 14, 1) | bitnum(input, 6, 0) + ) >>> 0 + return [s0, s1] +} + +// 逆置换:将 s0/s1 写回 8 字节 out +const inverse_permutation = (s0, s1, out) => { + out[3] = (bitnum_intr(s1, 7, 7) | bitnum_intr(s0, 7, 6) | bitnum_intr(s1, 15, 5) | bitnum_intr(s0, 15, 4) | bitnum_intr(s1, 23, 3) | bitnum_intr(s0, 23, 2) | bitnum_intr(s1, 31, 1) | bitnum_intr(s0, 31, 0)) & 0xff + out[2] = (bitnum_intr(s1, 6, 7) | bitnum_intr(s0, 6, 6) | bitnum_intr(s1, 14, 5) | bitnum_intr(s0, 14, 4) | bitnum_intr(s1, 22, 3) | bitnum_intr(s0, 22, 2) | bitnum_intr(s1, 30, 1) | bitnum_intr(s0, 30, 0)) & 0xff + out[1] = (bitnum_intr(s1, 5, 7) | bitnum_intr(s0, 5, 6) | bitnum_intr(s1, 13, 5) | bitnum_intr(s0, 13, 4) | bitnum_intr(s1, 21, 3) | bitnum_intr(s0, 21, 2) | bitnum_intr(s1, 29, 1) | bitnum_intr(s0, 29, 0)) & 0xff + out[0] = (bitnum_intr(s1, 4, 7) | bitnum_intr(s0, 4, 6) | bitnum_intr(s1, 12, 5) | bitnum_intr(s0, 12, 4) | bitnum_intr(s1, 20, 3) | bitnum_intr(s0, 20, 2) | bitnum_intr(s1, 28, 1) | bitnum_intr(s0, 28, 0)) & 0xff + out[7] = (bitnum_intr(s1, 3, 7) | bitnum_intr(s0, 3, 6) | bitnum_intr(s1, 11, 5) | bitnum_intr(s0, 11, 4) | bitnum_intr(s1, 19, 3) | bitnum_intr(s0, 19, 2) | bitnum_intr(s1, 27, 1) | bitnum_intr(s0, 27, 0)) & 0xff + out[6] = (bitnum_intr(s1, 2, 7) | bitnum_intr(s0, 2, 6) | bitnum_intr(s1, 10, 5) | bitnum_intr(s0, 10, 4) | bitnum_intr(s1, 18, 3) | bitnum_intr(s0, 18, 2) | bitnum_intr(s1, 26, 1) | bitnum_intr(s0, 26, 0)) & 0xff + out[5] = (bitnum_intr(s1, 1, 7) | bitnum_intr(s0, 1, 6) | bitnum_intr(s1, 9, 5) | bitnum_intr(s0, 9, 4) | bitnum_intr(s1, 17, 3) | bitnum_intr(s0, 17, 2) | bitnum_intr(s1, 25, 1) | bitnum_intr(s0, 25, 0)) & 0xff + out[4] = (bitnum_intr(s1, 0, 7) | bitnum_intr(s0, 0, 6) | bitnum_intr(s1, 8, 5) | bitnum_intr(s0, 8, 4) | bitnum_intr(s1, 16, 3) | bitnum_intr(s0, 16, 2) | bitnum_intr(s1, 24, 1) | bitnum_intr(s0, 24, 0)) & 0xff +} + +// Feistel 轮函数 f(state, key) +const des_f = (state, key) => { + const t1 = ( + bitnum_intl(state, 31, 0) | (((state & 0xF0000000) >>> 0) >>> 1) | bitnum_intl(state, 4, 5) | + bitnum_intl(state, 3, 6) | (((state & 0x0F000000) >>> 0) >>> 3) | bitnum_intl(state, 8, 11) | + bitnum_intl(state, 7, 12) | (((state & 0x00F00000) >>> 0) >>> 5) | bitnum_intl(state, 12, 17) | + bitnum_intl(state, 11, 18) | (((state & 0x000F0000) >>> 0) >>> 7) | bitnum_intl(state, 16, 23) + ) >>> 0 + const t2 = ( + bitnum_intl(state, 15, 0) | (((state & 0x0000F000) << 15) >>> 0) | bitnum_intl(state, 20, 5) | + bitnum_intl(state, 19, 6) | (((state & 0x00000F00) << 13) >>> 0) | bitnum_intl(state, 24, 11) | + bitnum_intl(state, 23, 12) | (((state & 0x000000F0) << 11) >>> 0) | bitnum_intl(state, 28, 17) | + bitnum_intl(state, 27, 18) | (((state & 0x0000000F) << 9) >>> 0) | bitnum_intl(state, 0, 23) + ) >>> 0 + + const lrgstate = [ + (t1 >>> 24) & 0xFF, (t1 >>> 16) & 0xFF, (t1 >>> 8) & 0xFF, + (t2 >>> 24) & 0xFF, (t2 >>> 16) & 0xFF, (t2 >>> 8) & 0xFF, + ] + for (let i = 0; i < 6; i++) lrgstate[i] ^= key[i] + + let s = ( + (sbox[0][sbox_bit(lrgstate[0] >>> 2)] << 28) | + (sbox[1][sbox_bit(((lrgstate[0] & 0x03) << 4) | (lrgstate[1] >>> 4))] << 24) | + (sbox[2][sbox_bit(((lrgstate[1] & 0x0F) << 2) | (lrgstate[2] >>> 6))] << 20) | + (sbox[3][sbox_bit(lrgstate[2] & 0x3F)] << 16) | + (sbox[4][sbox_bit(lrgstate[3] >>> 2)] << 12) | + (sbox[5][sbox_bit(((lrgstate[3] & 0x03) << 4) | (lrgstate[4] >>> 4))] << 8) | + (sbox[6][sbox_bit(((lrgstate[4] & 0x0F) << 2) | (lrgstate[5] >>> 6))] << 4) | + sbox[7][sbox_bit(lrgstate[5] & 0x3F)] + ) >>> 0 + + return ( + bitnum_intl(s, 15, 0) | bitnum_intl(s, 6, 1) | bitnum_intl(s, 19, 2) | bitnum_intl(s, 20, 3) | + bitnum_intl(s, 28, 4) | bitnum_intl(s, 11, 5) | bitnum_intl(s, 27, 6) | bitnum_intl(s, 16, 7) | + bitnum_intl(s, 0, 8) | bitnum_intl(s, 14, 9) | bitnum_intl(s, 22, 10) | bitnum_intl(s, 25, 11) | + bitnum_intl(s, 4, 12) | bitnum_intl(s, 17, 13) | bitnum_intl(s, 30, 14) | bitnum_intl(s, 9, 15) | + bitnum_intl(s, 1, 16) | bitnum_intl(s, 7, 17) | bitnum_intl(s, 23, 18) | bitnum_intl(s, 13, 19) | + bitnum_intl(s, 31, 20) | bitnum_intl(s, 26, 21) | bitnum_intl(s, 2, 22) | bitnum_intl(s, 8, 23) | + bitnum_intl(s, 18, 24) | bitnum_intl(s, 12, 25) | bitnum_intl(s, 29, 26) | bitnum_intl(s, 5, 27) | + bitnum_intl(s, 21, 28) | bitnum_intl(s, 10, 29) | bitnum_intl(s, 3, 30) | bitnum_intl(s, 24, 31) + ) >>> 0 +} + +// 单次 DES 加/解密一个 8 字节分组(轮密钥 schedule 为 16x6) +const des_crypt = (input, schedule, output) => { + let [s0, s1] = initial_permutation(input) + for (let i = 0; i < 15; i++) { + const prev = s1 + s1 = (des_f(s1, schedule[i]) ^ s0) >>> 0 + s0 = prev + } + s0 = (des_f(s1, schedule[15]) ^ s0) >>> 0 + inverse_permutation(s0, s1, output) +} + +// 生成 16 轮子密钥 +const key_schedule = (key, mode) => { + const schedule = Array.from({ length: 16 }, () => new Uint8Array(6)) + let c = 0 + let d = 0 + for (let i = 0; i < 28; i++) { + c = (c | bitnum(key, key_perm_c[i], 31 - i)) >>> 0 + d = (d | bitnum(key, key_perm_d[i], 31 - i)) >>> 0 + } + for (let i = 0; i < 16; i++) { + c = ((((c << key_rnd_shift[i]) >>> 0) | (c >>> (28 - key_rnd_shift[i]))) & 0xFFFFFFF0) >>> 0 + d = ((((d << key_rnd_shift[i]) >>> 0) | (d >>> (28 - key_rnd_shift[i]))) & 0xFFFFFFF0) >>> 0 + const togen = mode === DES_DECRYPT ? 15 - i : i + for (let j = 0; j < 24; j++) { + schedule[togen][(j / 8) | 0] |= bitnum_intr(c, key_compression[j], 7 - (j % 8)) + } + for (let j = 24; j < 48; j++) { + schedule[togen][(j / 8) | 0] |= bitnum_intr(d, key_compression[j] - 27, 7 - (j % 8)) + } + } + return schedule +} + +// 3DES(EDE3)密钥编排,返回 3 组 schedule +const tripledes_key_setup = (key, mode) => { + if (mode === DES_ENCRYPT) { + return [ + key_schedule(key.subarray(0, 8), DES_ENCRYPT), + key_schedule(key.subarray(8, 16), DES_DECRYPT), + key_schedule(key.subarray(16, 24), DES_ENCRYPT), + ] + } + return [ + key_schedule(key.subarray(16, 24), DES_DECRYPT), + key_schedule(key.subarray(8, 16), DES_ENCRYPT), + key_schedule(key.subarray(0, 8), DES_DECRYPT), + ] +} + +// 3DES 加/解密一个 8 字节分组 +const tripledes_crypt = (input, schedule, output) => { + const buf = new Uint8Array(8) + des_crypt(input, schedule[0], buf) + des_crypt(buf, schedule[1], output) + des_crypt(output, schedule[2], buf) + output.set(buf) +} + +// QRC 固定密钥:!@#)(*$%123ZXC!@!@#)(NHL +const QRC_KEY = Buffer.from([ + 0x21, 0x40, 0x23, 0x29, 0x28, 0x2a, 0x24, 0x25, 0x31, 0x32, 0x33, 0x5a, + 0x58, 0x43, 0x21, 0x40, 0x21, 0x40, 0x23, 0x29, 0x28, 0x4e, 0x48, 0x4c, +]) + +const handleInflate = (data) => new Promise((resolve, reject) => { + // Z_SYNC_FLUSH:容忍尾部不完整的 zlib 流(对应原实现忽略 Z_BUF_ERROR 的行为) + inflate(data, { finishFlush: constants.Z_SYNC_FLUSH }, (err, result) => { + if (err) reject(err) + else resolve(result) + }) +}) +/** + * 解密腾讯 QRC 歌词。 + * @param {string} hexData 服务端返回的十六进制字符串 + * @returns {Promise} 解密并解压后的歌词文本(解析失败时返回空字符串) + */ +export const decodeQrc = async(hexData) => { + if (!hexData || hexData.length % 2 !== 0) return '' + const encrypted = Buffer.from(hexData, 'hex') + if (encrypted.length === 0) return '' + + const schedule = tripledes_key_setup(QRC_KEY, DES_DECRYPT) + + const block = new Uint8Array(8) + for (let i = 0; i + 8 <= encrypted.length; i += 8) { + tripledes_crypt(encrypted.subarray(i, i + 8), schedule, block) + encrypted.set(block, i) + } + + try { + const result = await handleInflate(encrypted) + return result.toString('utf8') + } catch { + return '' + } +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/singer.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/singer.js new file mode 100644 index 000000000..4d9899abf --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/singer.js @@ -0,0 +1,249 @@ +import { httpFetch } from '../../request' + +import { formatPlayTime, sizeFormate } from '../../index' +import { formatSingerName } from '../utils' + +export const filterMusicInfoItem = item => { + const types = [] + const _types = {} + if (item.file.size_128mp3 != 0) { + let size = sizeFormate(item.file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (item.file.size_320mp3 !== 0) { + let size = sizeFormate(item.file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (item.file.size_flac !== 0) { + let size = sizeFormate(item.file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + if (item.file.size_hires !== 0) { + let size = sizeFormate(item.file.size_hires) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + } + + const albumId = item.album.id ?? '' + const albumMid = item.album.mid ?? '' + const albumName = item.album.name ?? '' + return { + source: 'tx', + singer: formatSingerName(item.singer, 'name'), + name: item.title, + albumName, + albumId, + albumMid, + interval: formatPlayTime(item.interval), + songId: item.id, + songmid: item.mid, + strMediaMid: item.file.media_mid, + img: (albumId === '' || albumId === '空') + ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumMid}.jpg`, + types, + _types, + typeUrl: {}, + } +} + + +/** + * 创建一个适用于TX的Http请求 + * @param {*} url + * @param {*} options + * @param {*} retryNum + */ +const createMusicuFetch = async(data, options, retryNum = 0) => { + if (retryNum > 2) throw new Error('try max num') + + let result + try { + result = await httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'POST', + body: { + comm: { + cv: 4747474, + ct: 24, + format: 'json', + inCharset: 'utf-8', + outCharset: 'utf-8', + uin: 0, + }, + ...data, + }, + headers: { + 'User-Angent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + }, + }).promise + } catch (err) { + console.log(err) + return createMusicuFetch(data, options, ++retryNum) + } + if (result.statusCode !== 200 || result.body.code != 0) return createMusicuFetch(data, options, ++retryNum) + + return result.body +} + +export default { + /** + * 获取歌手信息 + * @param {*} id + */ + getInfo(id) { + return createMusicuFetch({ + req_1: { + module: 'music.musichallSinger.SingerInfoInter', + method: 'GetSingerDetail', + param: { + singer_mid: [id], + ex_singer: 1, + wiki_singer: 1, + group_singer: 0, + pic: 1, + photos: 0, + }, + }, + req_2: { + module: 'music.musichallAlbum.AlbumListServer', + method: 'GetAlbumList', + param: { + singerMid: id, + order: 0, + begin: 0, + num: 1, + songNumTag: 0, + singerID: 0, + }, + }, + req_3: { + module: 'musichall.song_list_server', + method: 'GetSingerSongList', + param: { + singerMid: id, + order: 1, + begin: 0, + num: 1, + }, + }, + }).then(body => { + if (body.req_1.code != 0 || body.req_2 != 0 || body.req_3 != 0) throw new Error('get singer info faild.') + + const info = body.req_1.data.singer_list[0] + const music = body.req_3.data + const album = body.req_3.data + return { + source: 'tx', + id: info.basic_info.singer_mid, + info: { + name: info.basic_info.name, + desc: info.ex_info.desc, + avatar: info.pic.pic, + gender: info.ex_info.genre === 1 ? 'man' : 'woman', + }, + count: { + music: music.totalNum, + album: album.total, + }, + } + }) + }, + /** + * 获取歌手专辑列表 + * @param {*} id + * @param {*} page + * @param {*} limit + */ + getAlbumList(id, page = 1, limit = 10) { + if (page === 1) page = 0 + return createMusicuFetch({ + req: { + module: 'music.musichallAlbum.AlbumListServer', + method: 'GetAlbumList', + param: { + singerMid: id, + order: 0, + begin: page * limit, + num: limit, + songNumTag: 0, + singerID: 0, + }, + }, + }).then(body => { + if (body.req.code != 0) throw new Error('get singer album faild.') + + const list = this.filterAlbumList(body.req.data.albumList) + return { + source: 'tx', + list, + limit, + page, + total: body.req.data.total, + } + }) + }, + /** + * 获取歌手歌曲列表 + * @param {*} id + * @param {*} page + * @param {*} limit + */ + async getSongList(id, page = 1, limit = 100) { + if (page === 1) page = 0 + return createMusicuFetch({ + req: { + module: 'musichall.song_list_server', + method: 'GetSingerSongList', + param: { + singerMid: id, + order: 1, + begin: page * limit, + num: limit, + }, + }, + }).then(body => { + if (body.req.code != 0) throw new Error('get singer song list faild.') + + const list = this.filterSongList(body.req.data.songList) + return { + source: 'tx', + list, + limit, + page, + total: body.req.data.totalNum, + } + }) + }, + filterAlbumList(raw) { + return raw.map(item => { + return { + id: item.albumID, + mid: item.albumMid, + count: item.totalNum, + info: { + name: item.albumName, + author: item.singerName, + img: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.albumMid}.jpg`, + desc: null, + }, + } + }) + }, + filterSongList(raw) { + raw.map(item => { + return filterMusicInfoItem(item.songInfo) + }) + }, +} + diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/songList.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/songList.js new file mode 100644 index 000000000..a146909e5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/songList.js @@ -0,0 +1,435 @@ +import { httpFetch } from '../../request' +import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../index' +import { formatSingerName } from '../utils' + +export default { + _requestObj_tags: null, + _requestObj_hotTags: null, + _requestObj_list: null, + limit_list: 36, + limit_song: 100000, + successCode: 0, + sortList: [ + { + name: '推荐', + id: -1, + }, + { + name: '最热', + id: 3, + }, + { + name: '最新', + id: 2, + }, + ], + regExps: { + hotTagHtml: /class="c_bg_link js_tag_item" data-id="\w+">.+?<\/a>/g, + hotTag: /data-id="(\w+)">(.+?)<\/a>/, + + // https://y.qq.com/n/yqq/playlist/7217720898.html + // https://i.y.qq.com/n2/m/share/details/taoge.html?platform=11&appshare=android_qq&appversion=9050006&id=7217720898&ADTAG=qfshare + listDetailLink: /\/playlist\/(\d+)/, + listDetailLink2: /id=(\d+)/, + }, + tagsUrl: 'https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8¬ice=0&platform=wk_v15.json&needNewCode=0&data=%7B%22tags%22%3A%7B%22method%22%3A%22get_all_categories%22%2C%22param%22%3A%7B%22qq%22%3A%22%22%7D%2C%22module%22%3A%22playlist.PlaylistAllCategoriesServer%22%7D%7D', + hotTagUrl: 'https://c.y.qq.com/node/pc/wk_v15/category_playlist.html', + getListUrl(id, page) { + id = parseInt(id) + return `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8¬ice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({ + comm: { cv: 1602, ct: 20 }, + playlist: { + method: 'get_category_content', + param: { + titleid: id, + caller: '0', + category_id: id, + size: this.limit_list, + page: page - 1, + use_page: 1, + }, + module: 'playlist.PlayListCategoryServer', + }, + }))}` + // return `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&data=${encodeURIComponent(JSON.stringify({ + // comm: { cv: 1602, ct: 20 }, + // playlist: { + // method: 'get_playlist_by_tag', + // param: { id: 10000000, sin: this.limit_list * (page - 1), size: this.limit_list, order: sortId, cur_page: page }, + // module: 'playlist.PlayListPlazaServer', + // }, + // }))}` + }, + getListDetailUrl(id) { + return `https://c.y.qq.com/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg?type=1&json=1&utf8=1&onlysong=0&new_format=1&disstid=${id}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0` + }, + + // http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=2849349915&pn=0&rn=100&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1 + // 获取标签 + getTag(tryNum = 0) { + if (this._requestObj_tags) this._requestObj_tags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_tags = httpFetch(this.tagsUrl) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getTag(++tryNum) + return this.filterTagInfo(body.tags.data.v_group) + }) + }, + // 获取标签 + getHotTag(tryNum = 0) { + if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_hotTags = httpFetch(this.hotTagUrl) + return this._requestObj_hotTags.promise.then(({ statusCode, body }) => { + if (statusCode !== 200) return this.getHotTag(++tryNum) + return this.filterInfoHotTag(body) + }) + }, + filterInfoHotTag(html) { + let hotTag = html.match(this.regExps.hotTagHtml) + const hotTags = [] + if (!hotTag) return hotTags + + hotTag.forEach(tagHtml => { + let result = tagHtml.match(this.regExps.hotTag) + if (!result) return + hotTags.push({ + id: parseInt(result[1]), + name: result[2], + source: 'tx', + }) + }) + return hotTags + }, + filterTagInfo(rawList) { + return rawList.map(type => ({ + name: type.group_name, + list: type.v_item.map(item => ({ + parent_id: type.group_id, + parent_name: type.group_name, + id: item.id, + name: item.name, + source: 'tx', + })), + })) + }, + + async getRecommendList(page) { + const { body } = await httpFetch( + `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8¬ice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent( + JSON.stringify({ + comm: { cv: 1602, ct: 20 }, + playlist: { + module: 'music.playlist.PlaylistSquare', + method: 'GetRecommendWhole', + param: { + IsReqFeed: true, + FeedReq: { + From: (page - 1) * this.limit_list, + Size: this.limit_list, + }, + }, + }, + }), + )}`, + ).promise + if (body.code !== this.successCode) throw new Error('tx getRecommendList failed') + return { + list: + body.playlist.data.FeedRsp.List?.map(({ Playlist }) => ({ + play_count: formatPlayCount(Playlist.basic.play_cnt), + id: String(Playlist.basic.tid), + author: decodeName(Playlist.basic.creator.nick), + name: decodeName(Playlist.basic.title), + time: Playlist.basic.modify_time ? dateFormat(Playlist.basic.modify_time * 1000, 'Y-M-D') : '', + img: Playlist.basic.cover.medium_url || Playlist.basic.cover.default_url, + total: Playlist.basic.song_cnt, + desc: decodeName(Playlist.basic.desc).replace(/
    /g, '\n'), + source: 'tx', + })) || [], + total: body.playlist.data.FeedRsp.FromLimit, + limit: this.limit_list, + page, + source: 'tx', + } + }, + + // 获取列表数据 + getList(sortId, tagId, page, tryNum = 0) { + if (this._requestObj_list) this._requestObj_list.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + if (!tagId) { + if (!tagId && sortId == -1) return this.getRecommendList(page) + tagId = sortId + } + this._requestObj_list = httpFetch( + this.getListUrl(tagId, page), + ) + // console.log(this.getListUrl(sortId, tagId, page)) + return this._requestObj_list.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) + return this.filterList(body.playlist.data, page) + }) + }, + + // filterList(data, page) { + // return { + // list: data.v_playlist.map(item => ({ + // play_count: formatPlayCount(item.access_num), + // id: String(item.tid), + // author: item.creator_info.nick, + // name: item.title, + // time: item.modify_time ? dateFormat(item.modify_time * 1000, 'Y-M-D') : '', + // img: item.cover_url_medium, + // // grade: item.favorcnt / 10, + // total: item.song_ids?.length, + // desc: decodeName(item.desc).replace(/
    /g, '\n'), + // source: 'tx', + // })), + // total: data.total, + // page, + // limit: this.limit_list, + // source: 'tx', + // } + // }, + filterList({ content }, page) { + // console.log(content.v_item) + return { + list: content.v_item.map(({ basic }) => ({ + play_count: formatPlayCount(basic.play_cnt), + id: String(basic.tid), + author: basic.creator.nick, + name: basic.title, + // time: basic.publish_time, + img: basic.cover.medium_url || basic.cover.default_url, + // grade: basic.favorcnt / 10, + desc: decodeName(basic.desc).replace(/
    /g, '\n'), + source: 'tx', + })), + total: content.total_cnt, + page, + limit: this.limit_list, + source: 'tx', + } + }, + + async handleParseId(link, retryNum = 0) { + if (retryNum > 2) return Promise.reject(new Error('link try max num')) + + const requestObj_listDetailLink = httpFetch(link) + const { headers: { location }, statusCode } = await requestObj_listDetailLink.promise + // console.log(headers) + if (statusCode > 400) return this.handleParseId(link, ++retryNum) + return location == null ? link : location + }, + + async getListId(id) { + if ((/[?&:/]/.test(id))) { + if (!this.regExps.listDetailLink.test(id)) { + id = await this.handleParseId(id) + } + let result = this.regExps.listDetailLink.exec(id) + if (!result) { + result = this.regExps.listDetailLink2.exec(id) + if (!result) throw new Error('failed') + } + id = result[1] + // console.log(id) + } + return id + }, + // 获取歌曲列表内的音乐 + async getListDetail2(id, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + + const requestObj_listDetail = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'post', + headers: { + Origin: 'https://y.qq.com', + Referer: `https://y.qq.com/n/yqq/playsquare/${id}.html`, + }, + body: { + comm: { + cv: 4747474, + ct: 24, + format: 'json', + inCharset: 'utf-8', + outCharset: 'utf-8', + platform: 'yqq.json', + needNewCode: 1, + uin: 0, + }, + req_1: { + module: 'music.srfDissInfo.aiDissInfo', + method: 'uniform_get_Dissinfo', + param: { + disstid: parseInt(id), + userinfo: 1, + tag: 1, + orderlist: 1, + song_begin: 0, + song_num: this.limit_song, + onlysonglist: 0, + enc_host_uin: '', + }, + }, + }, + }) + const { body } = await requestObj_listDetail.promise + // console.log(body) + if (body.code !== this.successCode) return this.getListDetail2(id, ++tryNum) + if (body.req_1.code !== this.successCode) throw new Error('failed') + + const result = body.req_1.data + const dirinfo = result.dirinfo + return { + list: this.filterListDetail(result.songlist), + page: 1, + limit: this.limit_song, + total: result.total_song_num, + source: 'tx', + info: { + name: dirinfo.title, + img: dirinfo.picurl, + desc: decodeName(dirinfo.desc ?? '').replace(/
    /g, '\n'), + author: dirinfo.host_nick, + play_count: formatPlayCount(dirinfo.listennum), + }, + } + }, + // 获取歌曲列表内的音乐 + async getListDetail(id, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + + id = await this.getListId(id) + + const requestObj_listDetail = httpFetch(this.getListDetailUrl(id), { + headers: { + Origin: 'https://y.qq.com', + Referer: `https://y.qq.com/n/yqq/playsquare/${id}.html`, + }, + }) + const { body } = await requestObj_listDetail.promise + + // console.log(body) + if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum) + if (body.subcode !== this.successCode || !body.cdlist) return this.getListDetail2(id) + const cdlist = body.cdlist[0] + return { + list: this.filterListDetail(cdlist.songlist), + page: 1, + limit: cdlist.songlist.length + 1, + total: cdlist.songlist.length, + source: 'tx', + info: { + name: cdlist.dissname, + img: cdlist.logo, + desc: decodeName(cdlist.desc).replace(/
    /g, '\n'), + author: cdlist.nickname, + play_count: formatPlayCount(cdlist.visitnum), + }, + } + }, + filterListDetail(rawList) { + // console.log(rawList) + return rawList.map(item => { + let types = [] + let _types = {} + if (item.file.size_128mp3 !== 0) { + let size = sizeFormate(item.file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (item.file.size_320mp3 !== 0) { + let size = sizeFormate(item.file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (item.file.size_flac !== 0) { + let size = sizeFormate(item.file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + if (item.file.size_hires !== 0) { + let size = sizeFormate(item.file.size_hires) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + } + // types.reverse() + return { + singer: formatSingerName(item.singer, 'name'), + name: item.title, + albumName: item.album.name, + albumId: item.album.mid, + source: 'tx', + interval: formatPlayTime(item.interval), + songId: item.id, + albumMid: item.album.mid, + strMediaMid: item.file.media_mid, + songmid: item.mid, + img: (item.album.name === '' || item.album.name === '空') + ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.album.mid}.jpg`, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + getTags() { + return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'tx' })) + }, + + async getDetailPageUrl(id) { + id = await this.getListId(id) + + return `https://y.qq.com/n/ryqq/playlist/${id}` + }, + + search(text, page, limit = 20, retryNum = 0) { + if (retryNum > 5) throw new Error('max retry') + return httpFetch(`http://c.y.qq.com/soso/fcgi-bin/client_music_search_songlist?page_no=${page - 1}&num_per_page=${limit}&format=json&query=${encodeURIComponent(text)}&remoteplace=txt.yqq.playlist&inCharset=utf8&outCharset=utf-8`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + Referer: 'http://y.qq.com/portal/search.html', + }, + }) + .promise.then(({ body }) => { + if (body.code != 0) return this.search(text, page, limit, ++retryNum) + // console.log(body.data.list) + return { + list: body.data.list.map(item => { + return { + play_count: formatPlayCount(item.listennum), + id: String(item.dissid), + author: decodeName(item.creator.name), + name: decodeName(item.dissname), + time: dateFormat(item.createtime, 'Y-M-D'), + img: item.imgurl, + // grade: item.favorcnt / 10, + total: item.song_count, + desc: decodeName(decodeName(item.introduction)).replace(/
    /g, '\n'), + source: 'tx', + } + }), + limit, + total: body.data.sum, + source: 'tx', + } + }) + }, +} + +// getList +// getTags +// getListDetail diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/tipSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/tipSearch.js new file mode 100644 index 000000000..a844da9e4 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/tipSearch.js @@ -0,0 +1,29 @@ +import { httpFetch } from '../../request' + +export default { + // regExps: { + // relWord: /RELWORD=(.+)/, + // }, + requestObj: null, + tipSearch(str) { + this.cancelTipSearch() + this.requestObj = httpFetch(`https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg?is_xml=0&format=json&key=${encodeURIComponent(str)}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0`, { + headers: { + Referer: 'https://y.qq.com/portal/player.html', + }, + }) + return this.requestObj.promise.then(({ statusCode, body }) => { + if (statusCode != 200 || body.code != 0) return Promise.reject(new Error('请求失败')) + return body.data + }) + }, + handleResult(rawData) { + return rawData.map(info => `${info.name} - ${info.singer}`) + }, + cancelTipSearch() { + if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp() + }, + async search(str) { + return this.tipSearch(str).then(result => this.handleResult(result.song.itemlist)) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/utils/crypto.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/utils/crypto.js new file mode 100644 index 000000000..78618d12c --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/utils/crypto.js @@ -0,0 +1,28 @@ +import crypto from 'node:crypto' + +const PART_1_INDEXES = [23, 14, 6, 36, 16, 40, 7, 19] +const PART_2_INDEXES = [16, 1, 32, 12, 19, 27, 8, 5] +const SCRAMBLE_VALUES = [89, 39, 179, 150, 218, 82, 58, 252, 177, 52, 186, 123, 120, 64, 242, 133, 143, 161, 121, 179] + +async function hashSHA1(data) { + return crypto.createHash('sha1').update(data).digest('hex') +} + +function pickHashByIdx(hash, indexes) { + return indexes.map((idx) => hash[idx]).join('') +} + +function base64Encode(data) { + return Buffer.from(data) + .toString('base64') + .replace(/[\\/+=]/g, '') +} + +export async function zzcSign(text) { + const hash = await hashSHA1(text) + const part1 = pickHashByIdx(hash, PART_1_INDEXES) + const part2 = pickHashByIdx(hash, PART_2_INDEXES) + const part3 = SCRAMBLE_VALUES.map((value, i) => value ^ parseInt(hash.slice(i * 2, i * 2 + 2), 16)) + const b64Part = base64Encode(part3).replace(/[\\/+=]/g, '') + return `zzc${part1}${b64Part}${part2}`.toLowerCase() +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/utils/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/utils/index.js new file mode 100644 index 000000000..70ee425c8 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/tx/utils/index.js @@ -0,0 +1,16 @@ + +import { httpFetch } from '../../../request' +import { zzcSign } from './crypto' + +export const signRequest = async(data) => { + // console.log(data) + const sign = await zzcSign(JSON.stringify(data)) + // console.log('sign', sign) + return httpFetch(`https://u.y.qq.com/cgi-bin/musics.fcg?sign=${sign}`, { + method: 'post', + headers: { + 'User-Agent': 'QQMusic 14090508(android 12)', + }, + body: data, + }).promise +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/utils.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/utils.js new file mode 100644 index 000000000..dc2a671b5 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/utils.js @@ -0,0 +1,50 @@ +import crypto from 'crypto' +import dns from 'dns' +import { decodeName } from '@renderer/utils' + +export const toMD5 = str => crypto.createHash('md5').update(str).digest('hex') + + +const ipMap = new Map() +export const getHostIp = hostname => { + const result = ipMap.get(hostname) + if (typeof result === 'object') return result + if (result === true) return + ipMap.set(hostname, true) + // console.log(hostname) + dns.lookup(hostname, { + // family: 4, + all: false, + }, (err, address, family) => { + if (err) return console.log(err) + // console.log(address, family) + ipMap.set(hostname, { address, family }) + }) +} + +export const dnsLookup = (hostname, options, callback) => { + const result = getHostIp(hostname) + if (result) return callback(null, result.address, result.family) + + dns.lookup(hostname, options, callback) +} + + +/** + * 格式化歌手 + * @param singers 歌手数组 + * @param nameKey 歌手名键值 + * @param join 歌手分割字符 + */ +export const formatSingerName = (singers, nameKey = 'name', join = '、') => { + if (Array.isArray(singers)) { + const singer = [] + singers.forEach(item => { + let name = item[nameKey] + if (!name) return + singer.push(name) + }) + return decodeName(singer.join(join)) + } + return decodeName(String(singers ?? '')) +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/versionChars.ts b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/versionChars.ts new file mode 100644 index 000000000..e1e389f4e --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/versionChars.ts @@ -0,0 +1,124 @@ +export const versionChars = [ + // 伴奏 / 无人声 + // instrumental: [ + 'instrumental', + 'inst', + 'karaoke', + 'backing track', + 'minus one', + 'minus vocal', + 'no vocal', + 'off vocal', + 'vocal removed', + '伴奏', + '伴唱', + '卡拉ok', + '纯音乐', + '无人声', + '去人声', + '消音', + 'オフボーカル', + 'インスト', + '반주', + '노래방', + // ], + + // 现场 + // live: [ + 'live', + 'concert', + 'tour', + 'festival', + 'stage', + 'session', + '现场', + '演唱会', + '巡演', + '音乐会', + 'ライブ', + '공연', + '라이브', + // ], + + // 翻唱 + // cover: [ + 'cover', + 'tribute', + 'rendition', + 'remake', + '翻唱', + '致敬', + '改编', + 'カバー', + '커버', + // ], + + // 混音 + // remix: [ + 'remix', + 'mix', + 'club', + 'dance', + 'extended', + 'house', + 'edm', + 'dj', + 'dub', + 'trap', + '混音', + '重混', + '电音', + 'リミックス', + '리믹스', + // ], + + // 原声 / 不插电 + // acoustic: [ + 'acoustic', + 'unplugged', + 'stripped', + '原声', + '不插电', + 'アコースティック', + '어쿠스틱', + // ], + + // Demo + // demo: [ + 'demo', + '小样', + '试听', + 'デモ', + '데모', + // ], + + // 速度变化 + // speed: [ + 'slowed', + 'slowed reverb', + 'sped up', + 'speed up', + 'nightcore', + '慢速', + '加速', + '变速', + 'ナイトコア', + // ], + + // 特殊乐器版 + // special_instrumental: [ + 'piano', + 'guitar', + 'orchestra', + 'symphony', + 'violin', + '钢琴', + '吉他', + '管弦乐', + '小提琴', + 'ピアノ', + 'ギター', + '피아노', + '기타', + // ] +] diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/api-test.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/api-test.js new file mode 100644 index 000000000..cf62ca051 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/api-test.js @@ -0,0 +1,50 @@ +import { httpFetch } from '../../request' +import { requestMsg } from '../../message' +import { headers, timeout } from '../options' +import { dnsLookup } from '../utils' + +const api_test = { + getMusicUrl(songInfo, type) { + const requestObj = httpFetch(`http://ts.tempmusics.tk/url/wy/${songInfo.songmid}/${type}`, { + method: 'get', + timeout, + headers, + lookup: dnsLookup, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { + if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) + switch (body.code) { + case 0: return Promise.resolve({ type, url: body.data }) + default: return Promise.reject(new Error(requestMsg.fail)) + } + }) + return requestObj + }, +/* getPic(songInfo) { + const requestObj = httpFetch(`http://localhost:3100/pic/wy/${songInfo.songmid}`, { + method: 'get', + timeout, + headers, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ body }) => { + return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) + }) + return requestObj + }, + getLyric(songInfo) { + const requestObj = httpFetch(`http://localhost:3100/lrc/wy/${songInfo.songmid}`, { + method: 'get', + timeout, + headers, + family: 4, + }) + requestObj.promise = requestObj.promise.then(({ body }) => { + return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) + }) + return requestObj + }, */ +} + +export default api_test diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/comment.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/comment.js new file mode 100644 index 000000000..9785f3199 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/comment.js @@ -0,0 +1,208 @@ +import { httpFetch } from '../../request' +import { weapi } from './utils/crypto' +import { dateFormat2 } from '../../index' + +const emojis = [ + ['大笑', '😃'], + ['可爱', '😊'], + ['憨笑', '☺️'], + ['色', '😍'], + ['亲亲', '😙'], + ['惊恐', '😱'], + ['流泪', '😭'], + ['亲', '😚'], + ['呆', '😳'], + ['哀伤', '😔'], + ['呲牙', '😁'], + ['吐舌', '😝'], + ['撇嘴', '😒'], + ['怒', '😡'], + ['奸笑', '😏'], + ['汗', '😓'], + ['痛苦', '😖'], + ['惶恐', '😰'], + ['生病', '😨'], + ['口罩', '😷'], + ['大哭', '😂'], + ['晕', '😵'], + ['发怒', '👿'], + ['开心', '😄'], + ['鬼脸', '😜'], + ['皱眉', '😞'], + ['流感', '😢'], + ['爱心', '❤️'], + ['心碎', '💔'], + ['钟情', '💘'], + ['星星', '⭐️'], + ['生气', '💢'], + ['便便', '💩'], + ['强', '👍'], + ['弱', '👎'], + ['拜', '🙏'], + ['牵手', '👫'], + ['跳舞', '👯‍♀️'], + ['禁止', '🙅‍♀️'], + ['这边', '💁‍♀️'], + ['爱意', '💏'], + ['示爱', '👩‍❤️‍👨'], + ['嘴唇', '👄'], + ['狗', '🐶'], + ['猫', '🐱'], + ['猪', '🐷'], + ['兔子', '🐰'], + ['小鸡', '🐤'], + ['公鸡', '🐔'], + ['幽灵', '👻'], + ['圣诞', '🎅'], + ['外星', '👽'], + ['钻石', '💎'], + ['礼物', '🎁'], + ['男孩', '👦'], + ['女孩', '👧'], + ['蛋糕', '🎂'], + ['18', '🔞'], + ['圈', '⭕'], + ['叉', '❌'], +] + +const applyEmoji = text => { + for (const e of emojis) text = text.replaceAll(`[${e[0]}]`, e[1]) + return text +} + +let cursorTools = { + cache: {}, + getCursor(id, page, limit) { + let cacheData = this.cache[id] + if (!cacheData) cacheData = this.cache[id] = {} + let orderType + let cursor + let offset + if (page == 1) { + cacheData.page = 1 + cursor = cacheData.cursor = cacheData.prevCursor = Date.now() + orderType = 1 + offset = 0 + } else if (cacheData.page) { + cursor = cacheData.cursor + if (page > cacheData.page) { + orderType = 1 + offset = (page - cacheData.page - 1) * limit + } else if (page < cacheData.page) { + orderType = 0 + offset = (cacheData.page - page - 1) * limit + } else { + cursor = cacheData.cursor = cacheData.prevCursor + offset = cacheData.offset + orderType = cacheData.orderType + } + } + return { + orderType, + cursor, + offset, + } + }, + setCursor(id, cursor, orderType, offset, page) { + let cacheData = this.cache[id] + if (!cacheData) cacheData = this.cache[id] = {} + cacheData.prevCursor = cacheData.cursor + cacheData.cursor = cursor + cacheData.orderType = orderType + cacheData.offset = offset + cacheData.page = page + }, +} + +export default { + _requestObj: null, + _requestObj2: null, + async getComment({ songmid }, page = 1, limit = 20) { + if (this._requestObj) this._requestObj.cancelHttp() + + const id = 'R_SO_4_' + songmid + + const cursorInfo = cursorTools.getCursor(songmid, page, limit) + + const _requestObj = httpFetch('https://music.163.com/weapi/comment/resource/comments/get', { + method: 'post', + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36', + origin: 'https://music.163.com', + Refere: 'http://music.163.com/', + }, + form: weapi({ + cursor: cursorInfo.cursor, + offset: cursorInfo.offset, + orderType: cursorInfo.orderType, + pageNo: page, + pageSize: limit, + rid: id, + threadId: id, + }), + }) + const { body, statusCode } = await _requestObj.promise + // console.log(body) + if (statusCode != 200 || body.code !== 200) throw new Error('获取评论失败') + cursorTools.setCursor(songmid, body.data.cursor, cursorInfo.orderType, cursorInfo.offset, page) + return { source: 'wy', comments: this.filterComment(body.data.comments), total: body.data.totalCount, page, limit, maxPage: Math.ceil(body.data.totalCount / limit) || 1 } + }, + async getHotComment({ songmid }, page = 1, limit = 100) { + if (this._requestObj2) this._requestObj2.cancelHttp() + + const id = 'R_SO_4_' + songmid + page = page - 1 + + const _requestObj2 = httpFetch(`https://music.163.com/weapi/v1/resource/hotcomments/${id}`, { + method: 'post', + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36', + origin: 'https://music.163.com', + Refere: 'http://music.163.com/', + }, + form: weapi({ + rid: id, + limit, + offset: limit * page, + beforeTime: Date.now().toString(), + }), + }) + const { body, statusCode } = await _requestObj2.promise + if (statusCode != 200 || body.code !== 200) throw new Error('获取热门评论失败') + const total = body.total ?? 0 + return { source: 'wy', comments: this.filterComment(body.hotComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 } + }, + filterComment(rawList) { + return rawList.map(item => { + let data = { + id: item.commentId, + text: item.content ? applyEmoji(item.content) : '', + time: item.time ? item.time : '', + timeStr: item.time ? dateFormat2(item.time) : '', + location: item.ipLocation?.location, + userName: item.user.nickname, + avatar: item.user.avatarUrl, + userId: item.user.userId, + likedCount: item.likedCount, + reply: [], + } + + let replyData = item.beReplied && item.beReplied[0] + return replyData + ? { + id: item.commentId, + rootId: replyData.beRepliedCommentId, + text: replyData.content ? applyEmoji(replyData.content) : '', + time: item.time, + timeStr: null, + location: replyData.ipLocation?.location, + userName: replyData.user.nickname, + avatar: replyData.user.avatarUrl, + userId: replyData.user.userId, + likedCount: null, + reply: [data], + } + : data + }) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/hotSearch.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/hotSearch.js new file mode 100644 index 000000000..ee1f9d204 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/hotSearch.js @@ -0,0 +1,20 @@ +import { eapiRequest } from './utils/index' + +export default { + _requestObj: null, + async getList(retryNum = 0) { + if (this._requestObj) this._requestObj.cancelHttp() + if (retryNum > 2) return Promise.reject(new Error('try max num')) + + const _requestObj = eapiRequest('/api/search/chart/detail', { + id: 'HOT_SEARCH_SONG#@#', + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.code !== 200) throw new Error('获取热搜词失败') + + return { source: 'wy', list: this.filterList(body.data.itemList) } + }, + filterList(rawList) { + return rawList.map(item => item.searchWord) + }, +} diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/index.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/index.js new file mode 100644 index 000000000..a28295f50 --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/index.js @@ -0,0 +1,33 @@ +import leaderboard from './leaderboard' +import { apis } from '../api-source' +import getLyric from './lyric' +import getMusicInfo from './musicInfo' +import musicSearch from './musicSearch' +import songList from './songList' +import hotSearch from './hotSearch' +import comment from './comment' +// import tipSearch from './tipSearch' + +const wy = { + // tipSearch, + leaderboard, + musicSearch, + songList, + hotSearch, + comment, + getMusicUrl(songInfo, type) { + return apis('wy').getMusicUrl(songInfo, type) + }, + getLyric(songInfo) { + return getLyric(songInfo.songmid) + }, + getPic(songInfo) { + const requestObj = getMusicInfo(songInfo.songmid) + return requestObj.promise.then(info => info.al.picUrl) + }, + getMusicDetailPageUrl(songInfo) { + return `https://music.163.com/#/song?id=${songInfo.songmid}` + }, +} + +export default wy diff --git a/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/leaderboard.js b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/leaderboard.js new file mode 100644 index 000000000..e55023fba --- /dev/null +++ b/plugins/lx-music-ztools/vendor/lx/src/renderer/utils/musicSdk/wy/leaderboard.js @@ -0,0 +1,208 @@ +import { weapi } from './utils/crypto' +import { httpFetch } from '../../request' +import musicDetailApi from './musicDetail' + +const topList = [{ id: 'wy__19723756', name: '飙升榜', bangid: '19723756' }, + { id: 'wy__3779629', name: '新歌榜', bangid: '3779629' }, + { id: 'wy__2884035', name: '原创榜', bangid: '2884035' }, + { id: 'wy__3778678', name: '热歌榜', bangid: '3778678' }, + { id: 'wy__991319590', name: '说唱榜', bangid: '991319590' }, + { id: 'wy__71384707', name: '古典榜', bangid: '71384707' }, + { id: 'wy__1978921795', name: '电音榜', bangid: '1978921795' }, + { id: 'wy__5453912201', name: '黑胶VIP爱听榜', bangid: '5453912201' }, + { id: 'wy__71385702', name: 'ACG榜', bangid: '71385702' }, + { id: 'wy__745956260', name: '韩语榜', bangid: '745956260' }, + { id: 'wy__10520166', name: '国电榜', bangid: '10520166' }, + { id: 'wy__180106', name: 'UK排行榜周榜', bangid: '180106' }, + { id: 'wy__60198', name: '美国Billboard榜', bangid: '60198' }, + { id: 'wy__3812895', name: 'Beatport全球电子舞曲榜', bangid: '3812895' }, + { id: 'wy__21845217', name: 'KTV唛榜', bangid: '21845217' }, + { id: 'wy__60131', name: '日本Oricon榜', bangid: '60131' }, + { id: 'wy__2809513713', name: '欧美热歌榜', bangid: '2809513713' }, + { id: 'wy__2809577409', name: '欧美新歌榜', bangid: '2809577409' }, + { id: 'wy__27135204', name: '法国 NRJ Vos Hits 周榜', bangid: '27135204' }, + { id: 'wy__3001835560', name: 'ACG动画榜', bangid: '3001835560' }, + { id: 'wy__3001795926', name: 'ACG游戏榜', bangid: '3001795926' }, + { id: 'wy__3001890046', name: 'ACG VOCALOID榜', bangid: '3001890046' }, + { id: 'wy__3112516681', name: '中国新乡村音乐排行榜', bangid: '3112516681' }, + { id: 'wy__5059644681', name: '日语榜', bangid: '5059644681' }, + { id: 'wy__5059633707', name: '摇滚榜', bangid: '5059633707' }, + { id: 'wy__5059642708', name: '国风榜', bangid: '5059642708' }, + { id: 'wy__5338990334', name: '潜力爆款榜', bangid: '5338990334' }, + { id: 'wy__5059661515', name: '民谣榜', bangid: '5059661515' }, + { id: 'wy__6688069460', name: '听歌识曲榜', bangid: '6688069460' }, + { id: 'wy__6723173524', name: '网络热歌榜', bangid: '6723173524' }, + { id: 'wy__6732051320', name: '俄语榜', bangid: '6732051320' }, + { id: 'wy__6732014811', name: '越南语榜', bangid: '6732014811' }, + { id: 'wy__6886768100', name: '中文DJ榜', bangid: '6886768100' }, + { id: 'wy__6939992364', name: '俄罗斯top hit流行音乐榜', bangid: '6939992364' }, + { id: 'wy__7095271308', name: '泰语榜', bangid: '7095271308' }, + { id: 'wy__7356827205', name: 'BEAT排行榜', bangid: '7356827205' }, + { id: 'wy__7325478166', name: '编辑推荐榜VOL.44 天才女子摇滚乐队boygenius剖白卑微心迹', bangid: '7325478166' }, + { id: 'wy__7603212484', name: 'LOOK直播歌曲榜', bangid: '7603212484' }, + { id: 'wy__7775163417', name: '赏音榜', bangid: '7775163417' }, + { id: 'wy__7785123708', name: '黑胶VIP新歌榜', bangid: '7785123708' }, + { id: 'wy__7785066739', name: '黑胶VIP热歌榜', bangid: '7785066739' }, + { id: 'wy__7785091694', name: '黑胶VIP爱搜榜', bangid: '7785091694' }, +] + +export default { + limit: 100000, + list: [ + { + id: 'wybsb', + name: '飙升榜', + bangid: '19723756', + }, + { + id: 'wyrgb', + name: '热歌榜', + bangid: '3778678', + }, + { + id: 'wyxgb', + name: '新歌榜', + bangid: '3779629', + }, + { + id: 'wyycb', + name: '原创榜', + bangid: '2884035', + }, + { + id: 'wygdb', + name: '古典榜', + bangid: '71384707', + }, + { + id: 'wydouyb', + name: '抖音榜', + bangid: '2250011882', + }, + { + id: 'wyhyb', + name: '韩语榜', + bangid: '745956260', + }, + { + id: 'wydianyb', + name: '电音榜', + bangid: '1978921795', + }, + { + id: 'wydjb', + name: '电竞榜', + bangid: '2006508653', + }, + { + id: 'wyktvbb', + name: 'KTV唛榜', + bangid: '21845217', + }, + ], + getUrl(id) { + return `https://music.163.com/discover/toplist?id=${id}` + }, + regExps: { + list: /